자주 사용하지 않아서 잘 모르는 Built-in React Hooks 들
# Built-in React Hooks의 종류
- State Hooks : useState, useReducer
- Context Hooks : useContext
- Ref Hooks : useRef, useImperativeHandle
- Effect Hooks : useEffect, useLayoutEffect, useInsertionEffect
- Performance Hooks : useMemo, useCallback, useTransition, useDeferredValue
- Resource Hooks : use
- Other Hooks : useDebugValue, useId, useSyncExternalStore
이 중에서 굵게 표시한 Hook에 대해 알아보겠습니다.
# useLayoutEffect
Notes:
1. 업데이트는 상위 다시 렌더링, 상태 변경 또는 컨텍스트 변경으로 인해 발생합니다.
2. 지연 초기화 프로그램은 useState 및 useReducer에 전달되는 함수입니다.
- useEffect와 형태와 사용방법은 완전히 동일하나, 호출 순서에 차이가 있습니다.
- DOM 업데이트 -> LayoutEffect 실행 -> DOM이 화면에 그려짐 -> Effect 실행
- 따라서 화면이 그려지기 전 state가 설정되어야 할 경우, useEffect보다는 useLayoutEffect를 쓰는 것이 사용자 경험 상 좋습니다.
# useInsertionEffect
- React 18에서 추가된 Hook
- CSS-in-JS 방식을 사용하는 라이브러리를 위한 Hook
- 이 경우가 아니라면, useEffect나 useLayoutEffect를 사용하는 것이 권장됨.
- DOM 업데이트 이전에 호출되는 useEffect의 한 형태이다.
- 일반적으로 DOM 변경 전 동기적으로 스타일을 주입하는 데 사용된다.
- useEffect와 마찬가지로, 서버사이드에선 실행되지 않고 클라이언트사이드에서만 실행된다.
- useInsertionEffect는 useLayoutEffect가 동작하기 전에 스타일을 먼저 조작하게 해주는 훅이다.
- css 라이브러리를 개발하는 케이스거나 특이 케이스가 아니면 자주 사용할 일은 없다.
- React 18은 동시 렌더링을 위한 기반을 제공합니다.
사용자가 React의 동시 렌더링 기능을 최대한 활용할 수 있도록 몇 가지 새로운 API가 도입되었습니다.
새로운 useInsertionEffect 후크를 다룰 것입니다.
import { useInsertionEffect } from 'react';
// 컴포넌트
function MyButton() {
function useCSS(rule) {
useInsertionEffect(() => {
// ... <style> 태그를 여기에서 주입하세요 ...
});
return rule;
}
const className = useCSS('...');
return <div className={className} />;
}
## CSS-in-JS의 문제점
CSS 라이브러리는 즉시 새 규칙을 생성하고 <style> 태그와 함께 문서에 삽입합니다.
이러한 라이브러리의 경우 성능에 영향을 줄 수 있으므로 <style> 태그를 삽입할 시기를 알아야 합니다.
- CSS 규칙을 추가하거나 제거할 때 브라우저는 모든 규칙을 거의 다시 계산해야 합니다.
- 그런 다음 변경된 규칙뿐만 아니라 이미 존재하는 모든 규칙을 같이 다시 적용해야 합니다.
즉, React가 렌더링되는 동안 모든 프레임의 모든 DOM 노드에 대해 모든 CSS 규칙을 다시 계산합니다.
이것은 매우 느립니다. 브라우저의 최적화는 이 문제를 방지하는 데 도움이 되지 않습니다.
이를 피하는 한 가지 방법은 DOM 업데이트 시기를 관리하는 것입니다.
DOM 변경과 동시에 CSS 규칙을 조작해야 합니다.
이는 React가 DOM을 변경할 때,
레이아웃에서 (스타일)정보를 읽기 전과 브라우저에서 콘텐츠를 보이기 전 시점일 수 있습니다.
이 동작은 useLayoutEffect 훅과 유사합니다. 하지만 해당 훅은 스타일을 삽입하는 데는 사용할 수 없습니다.
## 왜 useLayoutEffect는 안되나요?
useLayoutEffect 훅은 DOM에서 레이아웃을 읽고 동기적으로 다시 렌더링하는 데 사용됩니다.
스타일을 삽입하는 어떤 컴포넌트와 레이아웃을 읽는 어떤 컴포넌트가 있다고 가정합니다.
- useLayoutEffect 훅을 사용하여 스타일을 삽입하면 레이아웃이 단일 렌더링 패스에서 여러 번 계산됩니다.
- 또한 CSS가 삽입되기 전에 하나의 훅이 레이아웃을 읽으려고 하면 잘못된 레이아웃을 읽게 됩니다.
해당 문제를 해결하기 위해 useInsertionEffect라는 새로운 훅이 도입되었습니다.
## The useInsertionEffect hook
useInsertionEffect(didUpdate);
useInsertionEffect의 시그니처는 useEffect와 동일합니다. 그러나 모든 DOM 조작 전에 동기적으로 실행됩니다.
useLayoutEffect에서 레이아웃을 읽기 전에 <style> 또는 SVG <defs>와 같은 전역 DOM 노드를 삽입하는 데 사용해야 합니다.
실제로 이러한 CSS 라이브러리 이외의 다른 용도로 사용되는 것은 아닙니다.
이 훅은는 스코프가 제한되어 있어 ref에 액세스할 수 없으며 업데이트를 예약할 수 없습니다. (useTransition)
//Source code - https://github.com/reactwg/react-18/discussions/110
function useCSS(rule) {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}
function App() {
let className = useCSS(rule);
return <div className={className} />;
}
## 사용하는 경우
- CSS-in-JS 라이브러리를 사용하는 경우.
- 렌더링 전 다크모드 여부를 결정해야 할 경우.
## 왜 쓰는가
- 렌더링 중에 스타일을 주입하고 React가 non-blocking update를 처리하는 경우, 브라우저는 컴포넌트 트리를 렌더링하는 동안 매 프레임마다 스타일을 다시 계산하므로 느릴 수 있음.
- useInsertionEffect는 컴포넌트에서 다른 Effect가 실행될 때 이미 실행되어 있음이 보장되므로 style이 주입된 상태에서 Effect 실행 가능.
# useTransition
React 18에서 concurrent rendering을 위해 새로 나온 useDeferredValue와 useTransition이 무엇인지 알아봅니다.
서비스에서 무거운 계산을 하는 로직이 실행되면 메인 스레드가 거기서 블록되기 때문에 다음 작업을 처리하지 못하게 된다. 극단적으로 매우 무거운 작업을 하게 될 때 다음 입력을 받지 못할 정도로 프레임이 저하되는 현상이 발생합니다.
이럴 경우 유저와 상호작용이 불가능해지는 상태가 발생하고, 유저에게 좋은 경험을 제공하지 못하게 됩니다.
이 문제를 근본적으로 해결하기 위해서 React 팀에서는 사용자의 상호작용이 있으면 무거운 작업은 메인 스레드가 놀고 있을 때 처리하고, 유저 입력이 들어오면 다시 유저와의 상호작용에 집중하게 만들었습니다. 즉 상태 변화의 우선순위를 나누고, 우선순위가 높은 이벤트가 발생하면 그 작업을 먼저 핸들링하고, 이후에 우선순위가 낮은 상태를 핸들링하게 됩니다.
useTransition
- useTransition hook은 상태 변화의 우선순위를 지정하기 위한 hook입니다.
- useTransition은 [isPending, startTransition] 값을 반환하는데, isPending은 상태 변화가 지연되고 있음을 알리는 boolean 타입이고, startTransition은 상태 변화를 일으키는 콜백함수를 전달받고 해당 콜백함수는 낮은 우선순위로 실행되게 됩니다.
import { useState, useTransition } from "react";
export default function Home() {
const [text, setText] = useState("");
const [value, setValue] = useState("");
const [isPending, startTransition] = useTransition();
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
startTransition(() => {
setText(e.target.value);
});
setValue(e.target.value);
};
console.log({ text, isPending });
console.log({ value });
return <input type="text" onChange={onChange} />;
}
onChange 함수가 실행되면 setText와 setValue가 실행되면서 상태가 변하게 됩니다.
하지만 setText는 startTransition 함수로 래핑 되어있습니다.
이렇게 래핑 되면 상태변화의 우선순위가 낮아지고, 다른 상태변화가 전부 일어난 후 setText가 실행되어 text 상태가 변하게 됩니다.
# useDeferredValue
useDeferredValue
- useDeferredValue는 상태 값 변화에 낮은 우선순위를 지정하기 위한 훅입니다.
- 우선순위가 높은 작업을 실행하는 동안 useMemo와 유사하게 이전 값을 계속 들고 있으면서 업데이트를 지연시킵니다.
- 이 훅은 useMemo와 함께 사용하면 더 효과가 좋습니다. 종속된 값들을 memoize 시키면 불필요한 재 랜더링을 막으면서 하위 컴포넌트나 상태의 업데이트를 지연시킬 수 있습니다.
import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';
export default function App() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<>
<label>
Search albums:
<input value={query} onChange={e => setQuery(e.target.value)} />
</label>
<Suspense fallback={<h2>Loading...</h2>}>
<SearchResults query={deferredQuery} />
</Suspense>
</>
);
}
useDeferredValue는 <Suspense>와 통합됩니다. 새 값으로 인한 백그라운드 업데이트로 인해 UI가 일시 중단되면 사용자에게 대체 항목이 표시되지 않습니다. 데이터가 로드될 때까지 이전에 지연된 값이 표시됩니다.
/* React.memo와 함께 사용하는 경우 */
const SlowList = memo(function SlowList({ text }) {
// ...
});
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text);
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
</>
);
}
# useDeferredValue vs useTransition
useDeferredValue와 useTransition hook은 상태변화의 우선순위를 낮게 하는 hook이다. 그렇다면 두 hook의 차이점은 무엇일까요?
useDeferredValue는 값을 래핑해서 사용합니다.
const deferredValue = useDeferredValue(count2);
useTransition은 useDeferredValue와 다르게 값이 아닌, 함수를 래핑합니다.
startTransition(() => {
setText(e.target.value);
});
참고
- https://react.dev/reference/react
- https://velog.io/@win9612/React-Hooks-useEffect%EC%99%80-useLayoutEffect-%EA%B7%B8%EB%A6%AC%EA%B3%A0-useInsertionEffect
- https://itchallenger.tistory.com/653
- https://github.com/reactwg/react-18/discussions/110
- https://react.dev/reference/react/useDeferredValue#usage
- https://velog.io/@ktthee/React-18-%EC%97%90-%EC%B6%94%EA%B0%80%EB%90%9C-useDeferredValue-%EB%A5%BC-%EC%8D%A8-%EB%B3%B4%EC%9E%90
- https://react.dev/reference/react/useDeferredValue
- https://doiler.tistory.com/83
- https://www.freecodecamp.org/korean/news/riaegteu-18yi-singineung-dongsiseong-rendeoring-concurrent-rendering-jadong-ilgwal-ceori-automatic-batching-deung/
- useSyncExternalStore