React의 18 버전이 나오면서 새로 도입된 몇 가지 피쳐들이 있다.
동시성(concurrent mode)부터 batch 업데이트 방식 변경, 지난번에 블로그에 쓴 createRoot 등이 있지만,
오늘은 공식 문서를 보면서 이번에 도입된 Hook에 어떤 것이 있는지 먼저 알아보려고 한다.
const id = useId();
useId
는 클라이언트와 서버 간의 hydration에서 mismatch를 피하면서 고유한 id값을 생성해주는 훅이다. 이것은 유니크한 ID가 필요한 API를 사용하는 컴포넌트에서 유용하게 쓰일 수 있다. 특히 React 18에서는 새로운 스트리밍 렌더러가 HTML을 순서에 어긋나지 않게 전달해 줄 수 있기 때문에 더욱 중요하다.
공식 문서에서는 useId
훅이 list에서의 key로서 사용되기 위해 도입된 것은 아니라고 설명한다.
const [isPending, startTransition] = useTransition();
useTransition
은 트렌지션의 펜딩 상태 여부를 나타내는 값과, 트랜지션을 실행시킬 함수를 리턴한다. useTransition
과 startTransition
은 일부 상태 업데이트를 긴급하지 않은 것으로 간주해 관리가 가능하다. React에서 상태값의 변화는 기본적으로 바로 업데이트를 일으키지만, 이렇게 바로 이루어져야 하는 업데이트가 급하지 않은 상태 업데이트에 의해 인터럽트 되는 것을 막기 위해 사용할 수 있다.
예를 들어 input에 text가 입력되며 UI가 업데이트되는 부분과,
매우 많은 리스트가 화면에 보여져야 하는 부분이 있다고 한다면,
사용자 입력이 리스트 UI의 업데이트 때문에 버벅이지 않도록 덜 급한 트랜지션으로 관리할 수 있다.
function App() {
// isPending은 현재 트랜지션이 활성화되고 있는지 여부를 나타내는 boolean 값
// startTransition은 리액트에 어떤 상태변화를 지연시킬 것인지 지정하는 함수
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
// handleClick에 의한 상태값 업데이트가 우선순위가 낮다는 것을 알려준다.
function handleClick() {
startTransition(() => {
setCount(c => c + 1);
})
}
return (
<div>
{isPending && <Spinner />}
<button onClick={handleClick}>{count}</button>
</div>
);
}
const deferredValue = useDeferredValue(value);
useDeferredValue
는 DOM 트리에서 긴급하지 않은 부분의 리렌더링을 지연시킨다. 이것은 debounce와 비슷하지만 몇 가지 추가적인 이점이 있다. 고정된 딜레이 시간이 있는 것이 아니라, 긴급한 렌더링이 앞서 화면에 반영되고 나면 이어서 즉시 지연된 렌더링을 시도한다. useDeferredValue
의 두번째 매개변수로 최대 지연 시간을 전달해줄 수도 있다.
useDeferredValue
는 변경되더라도 렌더링을 지연시킬 값(value)를 받고, 이 값의 복사본을 리턴한다. value가 바뀌어도 사용자 입력과 같은 긴급한 업데이트가 있다면 거기에 렌더링이 먼저 실행되며, 그동안 React는 바뀌기 전의 value값을 리턴하다가 긴급한 렌더링이 모두 끝나고 나면 value의 변경을 업데이트한다.
이 훅은 전달한 value에 대한 리렌더링만을 지연시키기 때문에, 만약 자식 컴포넌트에 대한 리렌더링을 지연시키고 싶다면 React.memo
를 사용하라고 공식문서에서는 안내하고 있다.
function Typeahead() {
const query = useSearchQuery('');
// query의 값이 변경되더라도 이전의 값을 기억하고, 렌더링이 지연된다.
const deferredQuery = useDeferredValue(query);
// 컴포넌트의 리렌더링 시점을 지연시키고 싶다면
// 지연된 deferredQuery 값을 의존성 배열에 추가하여 useMemo를 활용한다.
const suggestions = useMemo(() =>
<SearchSuggestions query={deferredQuery} />,
[deferredQuery]
);
return (
<>
<SearchInput query={query} />
<Suspense fallback="Loading results...">
{suggestions}
</Suspense>
</>
);
}
(.... 👍)
const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);
useSyncExternalStore
는 외부 스토어에 대한 업데이트를 강제로 동기화 하여 외부 스토어가 concurrent read를 지원할 수 있도록 하는 새로운 훅이다. 외부의 데이터를 읽고 구독할 때, 동시성 모드의 렌더링과 타임 슬라이싱을 병행하기 위한 방법을 지원한다.
이 메소드는 스토어의 값을 리턴하고, 매개변수로 아래의 정보들을 받는다.
간단한 형태로는 아래처럼 사용할 수 있다.
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
서버 렌더링이 이루어질 때 React는 스냅샷에 저장된 값을 사용하여 hydration 과정에서 미스매치 문제가 발생하지 않도록 한다. (아래 예시에서 세 번째 인자)
const selectedField = useSyncExternalStore(
store.subscribe,
() => store.getSnapshot().selectedField,
() => INITIAL_SERVER_SNAPSHOT.selectedField,
);
useInsertionEffect
는 CSS-in-JS 라이브러리의 경우에 렌더링 도중에 스타일을 주입해줄 때 성능 문제를 해결할 수 있는 Hook이다. useEffect
와 동일한데, 모든 DOM의 스타일 변경이 있기 전에 동기적으로 실행된다. 리액트가 DOM을 변환한 경우, layout effect가 일어나기전에 새 레이아웃을 한번 읽는다.
useLayoutEffect에서 레이아웃을 읽기 전에 DOM에 스타일을 주입하려면 이 옵션을 사용한다. 이 Hook은 제한된 스코프를 가지기 때문에 ref에의 접근이나 업데이트를 예약할 수 없다.
CSS-in-JS 라이브러리를 사용하지 않는다면 문제될게 없지만, 클라이언트 사이드에서 <style>
태그를 생성해서 삽입할 때는 성능 이슈에 대한 고려가 필요하다.
전반적으로 새로 도입된 Hook들에 대한 설명을 읽으면서 동시성 모드를 지원하기 위한 기능들이 많이 들어가 있다는 느낌을 받았다. 일단 훑어봤는데, 동시성 모드의 개념에 대해서 좀더 자세하게 이해하고 나서 Hook을 다시 읽어보면 좀더 이해가 될 듯!
참고 :
React 공식 문서 (https://reactjs.org/docs/hooks-reference.html)
리액트 v18 버전 톺아보기 (https://yceffort.kr/2022/04/react-18-changelog#useid)