영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다.
원문링크:https://jser.dev/react/2022/01/26/how-does-react-usedeferredvalue-work
ℹ️React Internals Deep Dive 에피소드 17, 유튜브에서 제가 설명하는 것을 시청해주세요.
⚠React@18.2.0기준, 최신 버전에서는 구현이 변경되었을 수 있습니다.
💬 역자 주석: JSer의 코멘트는 ❗❗로 표시 해뒀습니다.
그 외 주석은 리액트 소스 코드 자체의 주석입니다.
... 은 생략된 코드입니다.
React 동시 모드에서는 useTransition()
과 Suspense
외에 useDeferredValue()
라는 API가 하나 더 있는데, 이것이 무엇이고 어떻게 작동하는지 알아보겠습니다.
useDeferredValue()
는 어떤 문제를 해결하려고 하나요?
React 홈페이지에 자세한 답변이 있지만 데모가 깨져 있습니다.
여기서 간단히 시연해 보겠습니다.
useDeferredValue()가 없는 경우
이 데모는 useDeferredValue()가 없는 경우입니다.
Next 버튼을 클릭하면, 제목과 게시물에 대한 두 개의 API 목(Mock)이 실행되며, 제목 API는 더 빠르고(300ms) 게시물 API는 느리게(1000ms) 실행됩니다. 다음 버튼을 클릭하면 약 1000ms 후에 제목과 글이 모두 전환되는 것을 확인할 수 있습니다.
이는 버튼 클릭 핸들러가 useTransition()
을 사용하고 있기 때문인데, 게시글 API가 데이터를 반환할 때까지 에러가 발생해서, 제목과 글이 모두 지연되기 때문입니다.
이것은 좋지 않은데, 제목이 더 빨리 표시되지 않는 이유는 무엇인가요?
useDeferredValue()가 있는 경우
이 데모는 useDeferredValue()가 있는 경우입니다.
Next 버튼을 다시 한번 클릭하면, 제목이 먼저 보여지고, 게시글이 따라오는 것을 볼 수 있습니다.
이게 훨씬 낫습니다.
useDeferredValue()를 직접 생성해보자
useTransition()의 작동 방식은 이미 다루었습니다. 간단히 말해서 에러가 발생하면 커밋을 중지하는 것입니다.
여기서 우리의 문제는, 제목 API가 데이터를 반환하면 React가 리렌더링을 시도하지만 게시물이 렌더링될 때 데이터가 준비되지 않았기 때문에 에러를 던져서 발생하는 문제입니다.
function ProfilePage({ resource }) {
return (
<React.Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<React.Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</React.Suspense>
</React.Suspense>
);
}
이 문제를 해결하기 위해 게시물 섹션의 리소스
를 state별로 캐시하고 useEffect()
에서 업데이트할 수 있습니다.
function useDeferredValue(value) {
const [state, setState] = React.useState(value);
React.useEffect(() => {
// since value might be promise which causes suspension
// we should wrap it with startTransition
React.startTransition(() => {
setState(value);
});
}, [value]);
return state;
}
됐습니다. 이제 코드를 우리가 구현한 곳으로 변경합니다.
function ProfilePage({ resource }) {
const deferredResource = useDeferredValue(resource);
return (
<React.Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<React.Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={deferredResource} />
</React.Suspense>
</React.Suspense>
);
}
열려있는 데모에서 우리가 만든 useDeferredValue() 함수를 사용하면 React.useDeferredValue()
와 동일하게 작동합니다.
React.useDeferredValue()는 어떻게 동작 하나요?
debugger를 설정하면, 이전에 해왔던 것처럼 마운트용과 업데이트용 소스 코드를 찾을 수 있습니다.
function mountDeferredValue<T>(value: T): T {
const [prevValue, setValue] = mountState(value);
mountEffect(() => {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
setValue(value);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}, [value]);
return prevValue;
}
function updateDeferredValue<T>(value: T): T {
const [prevValue, setValue] = updateState(value);
updateEffect(() => {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
setValue(value);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}, [value]);
return prevValue;
}
잠깐만요, 기본적으로 우리가 작성한 것과 동일합니다! mountState
와 updateState
는 useState
에 대한 구현일 뿐입니다.
도대체 ReactCurrentBatchConfig.transition이
무엇인지 궁금하실 텐데요, startTransition()
의 소스를 보겠습니다. (소스)
export function startTransition(scope: () => void) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
scope();
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}
똑같습니다!!! 하지만 ReactCurrentBatchConfig.transition
은 도대체 무엇을 하는 것일까요?
이 함수 내부의 업데이트는 트랜지션 lanes에서 예약되어야 하며, 일시 중단 시 커밋되지 않도록 React에 알려주는 내부 구현입니다.
자세한 내용은 제 동영상에서 확인할 수 있습니다.
(원본 게시일: 2022-01-26)