ref 훅을 사용한 DOM 노드 접근
- ref 내장 브라우저 API와 같이 외부 시스템(또는 리액트가 아닌 시스템)을 이용할 때 유용하다. 함수 컴포넌트에서 사용 가능한 두 가지 내장 ref 훅이 있다. useRef 훅은 모든 종류의 값을 보유하는 ref를 선언하는 데 사용되지만 주로 DOM 노드에 사용된다. useImperativeHandle은 필요한 메서드만 노출시키는 데 사용된다.
ref 콘텐츠가 재생성되는 것을 어떻게 막는가?
- useRef 훅은 초기값(또는 기본값)을 useState 훅처럼 인자로 받는다. 이 훅은 포함된 컴포넌트의 맨 위에 선언해야 한다.
- 리액트는 이 초기값을 첫 번째 렌더링에서 저장하고 그 다음 렌더링에서는 무시하지만, 초기 ref 값으로 비용이 많이 드는 객체를 만든다면 이 객체가 불필요하게 렌더링마다 호출될 수 있다. 이는 애플리케이션의 성능에 영향을 미칠 수 있다.
- 초기 ref 값을 선언하고 ref 콘텐츠가 어떻게 다시 생성되는지는 다음 예제를 통해 더 나은 방식으로 설명할 수 있다.
// /Chapter03/refContentRecreated.jsx
function CreateBlogArticle() {
// This is an expensive object to create the article
}
function Blog() {
const articleRef = useRef(new CreateBlogArticle())
//...
}
- 이 코드에서 CreateBlogArticle() 함수는 리액트가 이 객체를 두 번재 렌더링부터 무시하더라도 항상 호출된다. 이 문제는 다음과 같이 CreateBlogArticle() 함수의 호출을 후속 렌더링에서 제한함으로써 해결할 수 있다.
// /Chapter03/refContentSkipRecreated.jsx
function Blog() {
const articleRef = useRef(null)
if (articleRef.current === null) {
articleRef.current = new CreateBlogArticle()
}
//...
}
- 이제 블로그 글 객체는 초기 렌더링 과정에서 한 번만 계산된다.
랜더링 메서드에서 ref에 접근하는 것이 가능한가?
- 물론이다. ref의 현재 값을 렌더링 메서드에서 접근할 수 있지만 렌더링 프로세스에서 ref.current 값을 읽거나 쓰는 것은 권장하지 않는다. 이는 이미 알고 있는 사실과 마찬가지로, state 변수와 달리 ref는 리렌더링을 트리거하지 않기 때문에 ref 값이 화면에 나타나는 동안은 어떤 이벤트에도 업데이트되지 않을 수도 있기 때문이다.
ref 인스턴스에서 일부 메서드를 노출하는 방법은 무엇인가?
- useImperativeHandle 훅을 사용해 자식 컴포넌트에서 부모 컴포넌트로 DOM 노드의 사용자 정의 메서드는 또는 기존 메서드의 일부만 노출할 수 있다. 이것은 부모 ref가 전체 ref에 접근하지 않고 특정 함수 또는 속성에만 접근할 수 있게 하는 데 유용하다. 일반적인 사용 사례는 라이브러리 아래에서 컴포넌트를 만들고 소비자가 노출된 API에만 접근할 수 있게 하는 것이다.
- 다이얼로그 컴포넌트를 만들고 해당 다이얼로그의 기본 기능을 일부 상위 수준 부모 컴포넌트에서 공유하고 싶다고 가정해 보자. 이 경우 전체 다이얼로그 DOM 노드에 접근하지 않고 자식 컴포넌트 내에서 open, close, reset 메서드를 노출할 수 있다.
// /Chapter03/useImperativeHandleDemo.jsx
useImperativeHandle(ref, () => ({
open: () => ref.current.invokeDialog(),
close: () => ref.current.closeDilaog(),
reset: () => ref.current.clearData(),
}))
- useImperativeHandle 훅을 사용하는 컴포넌트는 forwardRef로 래핑해야 하며, ref는 forwardRef 랜더링 메서드에서 두 번째 인수로 받아야 한다.
- 여기서 다루지 않은 몇 가지 내장 훅이 있다. useId, useDeferred, useTransition, useSyncExternalStore 같은 훅은 최소한으로 사용되기 때문에 여기에서 다루지 않았다. 간략히 살펴보겠다.
- useId : HTML 접근성 속성을 위한 고유한 ID를 생성하는 데 사용되는 훅
- useDeferred : 최신 데이터를 사용할 수 있을 때까지 UI 일부의 업데이트를 지연시키는 데 사용되는 훅
- useTransition : 일부 상태 수정을 낮은 우선순위로 표시해서 사용자 응답성을 향상시키는 데 도움이되는 훅
- useSyncExternalStore : 리액트 시스템 외부에 존재하는 외부 데이터 저장소를 구독하는 데 사용되는 훅
- 리액트에서는 여러 내장 훅을 제공하지만 비즈니스 요구사항에 기반해서 리액트 커뮤니티에서 제공하는 서드파티 훅을 사용해 이러한 훅의 사용 범위를 넘어설 수 있다.
인기있는 서드 파티 훅
- Hooks API는 개발자 커뮤니티에서 널리 사용되고 있으며, 이러한 리액트 내장 훅은 2019년부터 존재했다. 개발자들은 useImmer, useFetch, useDebounce, useForm, useLocalStorage, 리덕스 훅 등 많은 서드파티 훅을 생성하고 웹 개발에서 접하는 일반적인 사용 사례를 해결하기 위해 노력했다. 훅 개념을 정복하고 싶다면 서드파티 훅과 그것이 일반적인 문제를 어떻게 해결하는 지에 대해 잘 이해해야 한다.
useImmer 훅은 무엇인가? 그 목적은 무엇인가?
- useImmer 훅은 useState 훅과 유사하지만 중첩된 데이터 수준의 복잡한 상태를 관리하는 데 유용하다. 이 훅은 state를 직접 변경 하능한 것처럼 업데이트하며 일반 자바스크립트와 유사한 방식으로 동작한다. 이 훅은 state의 새로운 복사본을 생성해서 변경 가능하게 만드는 Immer 라이브러리를 기반으로 한다.
- useImmer 훅은 use-immer npm 라이브러리를 통해 설치할 수 있다. useState와 마찬가지로 튜플을 반환한다. 튜플의 첫번 째 값은 현재 상태이고, 두 번째 값은 updater 함수다.
- UserProfile 컴포넌트의 주소 세부 정보를 직접 업데이트하는 예제를 살펴보자.
// /Chapter03/useImmerDemo.jsx
import { useImmer } from 'use-immer'
function UserProfile() {
const [user, setUser] = useImmer({
name: 'Tom',
address: {
country: 'United States',
city: 'Austin',
postalCode: 73301,
},
})
function updatePostalCode(code) {
setUser((draft) => {
draft.address.postalCode = code
})
}
return (
<div className="profile">
<h1>
Hello {user.name}, your latest postal code is ({user.address.postalCode}
)
</h1>
<input
onChange={(e) => {
updatePostalCode(e.target.value)
}}
value={user.address.postalCode}
/>
</div>
)
}
- 내부적으로 Immer는 임시 draft 객체를 생성하고 모든 변경 사항이 해당 객체에 적용된다. 모든 변경이 완료되면 Immer는 다음 state 객체를 생성한다.
- 내장된 훅 또는 어떤 서드파티 훅으로도 사용 사례를 충족시킬 수 없다면 사용자 정의 훅을 작성해서 필요에 맞는 해결책을 제공할 수 있다.
사용자 정의 훅 구축
- 리액트는 useState, useEffect, useContext 등과 같은 내장 훅을 제공하지만 때로는 특정 요구사항을 해결하기 위해 내장 훅이나 서드파티 라이브러리로는 충분하지 않을 때가 있다. 이번 절을 다 읽고 나면 사용자 정의 훅과 그 목적, 그리고 컴포넌트 로직을 공유하는 전통적인 접근 방식을 피하는 방법에 관한 질문에 답할 수 있게 될 것이다.
사용자 정의 훅이란 무엇인가? 어떻게 생성하는가?
- 리액트는 여러 내장 훅을 제공하지만 제한된 시나리오에서만 훅을 사용하도록 제한하지는 않았다. 컴포넌트 로직을 추출해서 재사용 가능한 함수로 분히나는 나만을 위한 훅을 생성하는 것도 가능한데, 이를 사용자 정의 훅이라고 한다. 이러한 훅은 데이터를 불러오는 것, 폼 핸들링, 온라인 또는 오프라인 상태 구독, 채팅방 접속, 애니메이션 등과 같은 다양한 상황에 대응할 수 있다.
- 실제 예제를 사용자 정의 훅의 생성 및 사용법을 더 잘 이해할 수 있다. 특정 사용자의 모든 게시물을 나열하고 동시에 특정 게시물에 대한 댓글을 표시해야 하는 블로그 사이트 애플리케이션을 생각해보자. 여기서는 Posts 및 Comments 라는 두 개의 컴포넌트를 만들어야 한다. 이 두 컴포넌트는 각각 URL 및 선택적 쿼리 파라미터를 기반으로 서버에서 데이터를 가져와 응답을 받으면 화면에 데이터를 표시해야 한다.
- 데이터 불러오기, 로딩, 에러 처리 등에 관한 로직을 두 개의 컴포넌트로 분리하는 대신, 코드를 별도의 재사용 가능한 use 접두사가 잇는 사용자 정의 훅으로 옮길 수 있다. 데이터를 불러오는 훅은 다음과 같이 useFetchData.js 파일에 위치하는 useFetchData라는 이름으로 생성할 수 있다.
// /Chapter03/useFetchCustomHook.jsx
import { useState, useEffect } from 'react'
const useFetchData = (url, initialData) => {
const [data, setData] = useState(initialData)
const [loading, setLoading] = useState(false)
useEffect(() => {
setLoading(true)
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => console.log(err))
.finally(() => setLoading(false))
}, [url])
return { data, loading }
}
export default useFetchData
- 그러고 나면 Post.jsx 및 Comments.jsx 파일의 일부로 생성된 Consumer 컴포넌트에서 이 사용자 정의 훅을 사용할 수 있다. Posts 컴포넌트에서 사용하는 예시는 다음과 같다.
// /Chapter03/useFetchHookConsumer.jsx
import useFetchData from './useFetchData.js'
export default function Posts() {
const url = 'https://jsonplaceholder.typicode.com/posts?userId=1'
const { data, loading } = useFetchData(url, [])
return (
<>
{loading && <p>Loading posts... </p>}
{data &&
data.map((item) => (
<div key={item?.title}>
<p>
{item?.title}
<br />
{item?.body}
</p>
</div>
))}
</>
)
}
- 같은 방식으로 useFetchData를 Comments 컴포넌트에서 사용할 수 있다. 이렇게 변경하면 Post와 Comments 컴포넌트의 코드는 훨씬 단순해지고 더 정확하고 가독성도 좋아진다.
사용자 정의 훅의 장점은 무엇인가?
- 사용자 정의 훅을 사용했을 때 주요 이점은 여러 컴포넌트에 중복 로직을 작성하지 않으면서 코드 재사용성을 높일 수 있다는 것이다. 사용자 정의 훅의 몇 가지 다른 이점도 있다.
- 유지보수성 : 사용자 정의 훅을 사용하면 코드를 더 쉽게 유지할 수 있다. 미래에 훅의 로직을 변경해야 할 경우 코드를 한 곳에서만 변경하면 되며, 다른 부분, 즉 컴포넌트나 파일을 건드리지 않아도 된다.
- 가독성 : 실제 UI에 표시되는 프레젠테이션 컴포넌트 주위에 고차 컴포넌트, 프로바이더와 컨슈머, 랜더 props를 래핑하는 대신 사용자 정의 훅을 사용하면 애플리케이션 코드가 더 깔끔하고 가독성이 높아진다. 또한 특정 컴포넌트 로직을 별도의 훅으로 분리해서 컴포넌트 코드가 훨씬 더 깔끔해진다.
- 테스트 가능성 : 리액트 애플리케이션에서는 테스트 컨테이너와 프레젠테이션 컴포넌트에 대한 별도의 테스트를 작성해야 한다. 특히, 컨테이너가 많은 고차 컴포넌트를 사용하는 경우 통합 테스트에 대한 어려움이 있는데, 사용자 정의 훅을 사용하면 컨테이너와 컴포넌트를 단일 컴포넌트로 결합할 수 있어 이러한 문제를 해결할 수 있다.
또한 고차 컴포넌트보다 쉽게 훅을 대상으로 단위 테스트 및 모킹할 수 있다.
- 커뮤니티 기반 훅 : 리액트 커뮤니티는 이미 유명하고, 수많은 특정 사용 사례에 대한 훅이 만들어졌다. 찾고 있는 훅이 이미 누군가에 의해 만들어졌는지 직접 만들기 전에 확인하는 것이 좋다. 커뮤니티 기반 혹은 https://userhooks.com이나 https://github/imbhargav5/rooks에서 확인할 수 있다.
- 이러한 이점들은 많은 리액트 개발자에게 자신의 리액트 애플리케이션에서 만날 수 있는 고유한 기능에 대한 사용자 정의 훅을 만들기 위한 동기를 부여한다. 특정 시나리오를 처리하기 위해 이미 서드파티 오픈소스 라이브러리에서 해당 훅을 제공하고 있다면 사용자 정의 훅을 만들기보다는 기존 훅을 재사용하는 것이 좋다.
랜더 props와 고차 컴포넌트를 사용해야 하는가?
- 랜더 props와 고차 컴포넌트는 리액트 생태계에서 컴포넌트 간에 상태 로직을 공유하기 위해 사용되는 전통적인 고급 패턴이다. 그러나 훅은 이러한 두 가지 전통적인 패턴에 비해 다소 간단하며, 대부분의 사용 사례를 충분히 해결할 수 있다. 게다가 훅을 사용하면 기존 컴포넌트 구조를 변경하거나 중첩된 트리를 생성할 필요가 있다.
effect를 사용자 정의 훅으로 옮기는 것을 추천하는가?
- effect 훅은 리액트의 범위를 벗어나 외부와 상호 작용해야 할 때 사용된다. 일부 리액트가 아닌 시스템은 웹 API에 접근하거나 외부 API를 호출하는 등의 작업을 수행할 수 있다. 시간이 지남에 따라 코드의 effect 개수는 특정 사용 사례에 대한 구체적인 솔루션을 구현함으로써 줄어들 것이다. 일반적인 지침은 내장된 해결책이 없을 때 effect 훅을 사용하는 것이다. 왜냐하면 effect를 피하면 애플리케이션이 간단해지고 실행이 빨라지며 오류가 적어지기 때문이다. effect 사용자 정의 훅으로 옮기면 솔루션이 만들어졌을 때 코드를 업그레이드하기가 더 쉬워진다.
- 비즈니스 요구사항을 충족하기 위한 여러 사용자 정의 훅과 함께 애플리케이션의 규모가 커지면 애플리케이션의 복잡성이 증가하고 버그가 발생할 가능성이 높아진다.
훅 문제 해결과 디버깅
- 전통적인 디버깅 방법, 즉 IDE와 브라우저 개발자 도구를 사용하는 디버깅은 사용자 정의 훅을 디버깅 하는 데 효과적이지 않다. 리액트는 useDebugValue 훅을 제공함으로써 개발자가 사용자가 정의 훅을 디버깅할 수 있게 하며, 이를 통해 사용자 지정 형식의 레이블을 할당할 수 있다.
어떻게 사용자 정의 훅을 디버깅하는가?
- useDebugValue 훅은 리액트 개발자 도구 내에서 사용자 정의 훅의 내부 로직에 관련된 데이터를 시각화하는 데 사용된다. 이 정보는 리액트 개발자 도구 확장의 Component Inspector 탭 내에서 나타난다.
- 현재 디버그 정보는 사용자 정의 훅 내에서 사용된 내장 훅에 관한 정보만 표시한다. 코드 내에서 호출된 훅에 해당하는 각 엔트리 뱁을 일일이 확인해서 정보를 읽는 것은 개발자에게 어려울 수 있다. 이 문제는 사용자 정의 훅이 리액트 개발자 도구 출력 결과에 추가 엔트리를 추가함으로써 해결할 수 있다.
- 예시를 통해 좀 더 이해해보자. useDebugValue 훅을 이용해 사용자 정의 useFetchData 훅 내에서 필요한 세부 정보를 다양한 위치에 기록할 수 있다. 이는 '사용자 정의 훅이란 무엇인가? 어떻게 생성하는가?' 절에서 생성한 사용자 정의 useFetchData 훅을 기준으로 한다.
// /Chapter03/useDebugDemo.jsx
const useFetchData = (url, initialData) => {
useDebugValue(url)
const [data, setData] = useState(initialData)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useDebugValue(error, (err) =>
err ? `fetch is failed with ${err.message}` : 'fetch is successful',
)
useEffect(() => {
setLoading(true)
fetch(url)
.then((res) => res.json())
.then((data) => setData(data))
.catch((err) => setError(err))
.finally(() => setLoading(false))
}, [url])
useDebugValue(data, (items) =>
items.length > 0 ? items.map((item) => item.title) : 'No posts available',
)
return { data, loading }
}
- 이 코드에서 두 번째와 세 번째 디버그 호출에서는 표시된 값의 형식을 지정하기 위해 선택적으로 두 번재 인수를 사용한다.
- 리액트 개발자 도구는 FetchData 사용자 정의 훅 내에서 DebugValue 레이블 아래에 모든 추가 항목을 나열한다. 예를 들어, Posts 컴포넌트 위로 마우스를 올리면 오른쪽에 있는 Hooks 섹션이 나타난다.
- 마찬가지로 서비스 장애로 인해 API에서 오류가 발생하면 해당 문제의 근본적인 원인을 DebugValue 레이블을 통해 추적할 수 있다.
- 지금까지 내장 훅에 관련된 여러 질문에 대해 살펴보고, 서드파티 훅 및 사용자 정의 훅과 관련된 주제를 알아봤다. 이러한 모든 주제는 서로의 관계를 고려해서 특정한 순서로 다뤘다.
정리
- 이번 장에서는 리액트 애플리케이션의 훅에 대해 자세히 알아봤다. 먼저, 훅을 소개하고 훅을 사용할 때 준수해야 할 규칙을 소개했다. 다음으로, useState와 useReducer 훅을 사용해 컴포넌트 내에서 상태를 관리하는 방법과 useContext 훅을 사용해 컴포넌트 간에 데이터를 공유하는 전역 상태를 관리를 살펴봤다. 그 다음 effect 훅을 사용해 애플리케이션에서 부수 효과를 수행하는 방법을 다뤘다.
- 자주 사용되는 내장 훅 외에도 ref 훅을 사용해 DOM 노드에 접근하는 방법, 훅을 통한 성능 최적화, 서드파티 훅 사용, 비즈니스 요구에 맞는 사용자 정의 훅 생성에 대해 알아봤다.