[번역] React.useDeferredValue() 는 어떻게 동작하나요?

[번역] React.useDeferredValue() 는 어떻게 동작하나요?

React Internals Deep Dive - EP17

·

3 min read

영문 블로그 글을 번역했습니다. 허가를 받으면 시리즈를 이어갈 예정입니다.
원문링크:
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;
}

잠깐만요, 기본적으로 우리가 작성한 것과 동일합니다! mountStateupdateStateuseState에 대한 구현일 뿐입니다.

도대체 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)