리액트 v18.0에서는 오랫동안 기다려온 동시성(Concurrency) 기능을 도입하며 새로운 지평을 열었습니다.
아쉽게도 어떻게 사용하는지를 설명하는 자료는 넘쳐나지만, 어떻게 작동하는지에 대한 설명은 드뭅니다.
로우레벨의 기능이기 때문에 리액트의 동시성 개념을 이해하는 것이 중요하지는 않지만, 알아두어서 나쁠 것은 없습니다!
이 글은 리액트의 Concurrent API와 모범 사례를 문서화하기 위해 쓴 글이 아닙니다. 글에 링크된 리액트 공식 문서와 함께 읽는 것을 추천드립니다.
리액트 동시성의 기본 전제는 다음 뷰를 렌더링하는 동안 현재 뷰의 반응성을 유지하도록 렌더링 프로세스를 재작업하는 것입니다.
Concurrent Mode는 애플리케이션 성능을 개선하기 위해 리액트팀이 제안했던 기능입니다. 렌더링 프로세스를 중단할 수 있는 작업 단위로 나누자는 아이디어였습니다.
내부적으로는 컴포넌트 렌더링을 requestIdleCallback()
호출로 래핑하여 렌더링 프로세스 중에 애플리케이션의 응답성을 유지하는 방식으로 구현되었을 것입니다.
따라서 만약 Blocking Mode가 구현되었다면 이론상으론 다음과 같이 구현되었을 것입니다.
function renderBlocking(Component) {
for (let Child of Component) {
renderBlocking(Child);
}
}
그리고 Concurrent Mode는 다음과 같이 구현되었겠죠.
function renderConcurrent(Component) {
// 만약 상태가 오래되었다면 렌더링 프로세스를 중단합니다
if (isCancelled) return;
for (let Child of Component) {
// 브라우저가 바쁘지 않을 때까지 기다립니다(수행할 인풋이 없을 때까지)
requestIdleCallback(() => renderConcurrent(Child));
}
}
왜 이러한 방법이 애플리케이션의 반응성을 유지할 수 있게 해주는지 궁금하다면 이벤트 루프를 막지 않는 방법에 대한 실용적인 가이드를 읽어보세요!
리액트가 실제로 이 작업을 어떻게 수행하는지 궁금하다면 리액트의 scheduler
패키지의 구현을 확인해보세요. 처음에 requestIdleCallback
을 사용한 후 requestAnimationFrame
으로 전환하고, 나중에는 사용자 공간 타이머로 전환했습니다.
Concurrent Mode 계획은 이전 버전과의 호환성 이슈로 실현되지 않았습니다.
대신 리액트팀은 동시 렌더링을 선택적으로 활성화할 수 있는 새로운 API 세트인 Concurrent Features로 방향을 전환했습니다. 지금까지 리액트는 동시 렌더링을 선택할 수 있는 두 가지 새로운 훅을 도입했습니다.
useTransition
useTransition
훅은 두 개의 항목을 반환합니다.
true
인, 불리언 플래그 isPending
startTransition
이 함수를 사용하려면 startTransition
콜백에서 setState
호출을 래핑해야합니다.
function MyCounter() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
const increment = useCallback(() => {
startTransition(() => {
// 이 업데이트를 동시에 실행합니다
setCount((count) => count + 1);
});
}, []);
return (
<>
<button onClick={increment}>Count {count}</button>
<span>{isPending ? "Pending" : "Not Pending"}</span>
// 동시성으로 인한 효과를 받는 컴포넌트
<ManySlowComponents count={count} />
</>
);
}
개념적으로 상태 업데이트는 startTransition
에 래핑되어 있는지 감지하여 블로킹 렌더링을 예약할지 동시 렌더링을 예약할지 결정합니다.
function startTransition(stateUpdates) {
isInsideTransition = true;
stateUpdates();
isInsideTransition = false;
}
function setState() {
if (isInsideTransition) {
// 동시 렌더링을 예약합니다
} else {
// 블로킹 렌더링을 예약합니다
}
}
useTransition
의 중요한 주의사항은 제어된 입력에는 사용할 수 없다는 것입니다. 이러한 경우에는 useDeferredValue
를 사용하는 것이 가장 좋습니다.
useDeferredValue
useDeferredValue
훅은 상태 업데이트를 startTransition
에 감쌀 수 없지만 동시 업데이트를 실행하고 싶은 경우에 편리한 훅입니다.
이런 경우의 예시로는 부모로부터 새로운 값을 받는 자식 컴포넌트를 들 수 있습니다.
개념적으로 useDeferredValue
는 디바운스 효과이며 다음과 같이 구현할 수 있습니다.
function useDeferredValue<T>(value: T) {
const [isPending, startTransition] = useTransition();
const [state, setState] = useState(value);
useEffect(() => {
// 인풋이 변경될 때
// 동시 렌더링을 실행시킵니다
startTransition(() => {
setState(value);
});
}, [value]);
return state;
}
입력 디바운스 훅과 같은 방식으로 사용됩니다.
function Child({ value }) {
const deferredValue = useDeferredValue(value);
// ...
}
useTransition
과 useDeferredValue
훅은 동시 렌더링의 목적 외에도 서스펜스 컴포넌트가 완료되기를 기다리는 목적도 있습니다.
이후 포스트에서는 서스펜스와 그 역할에 대해 다룰 예정이니 뉴스레터를 구독하고 가장 먼저 읽어보세요.
굿