개발자
류준열

동시성 렌더링

23.3.4 에 썼던 글을 보완한 글입니다.

브라우저의 JS 실행환경은 싱글쓰레드이기 때문에 두 가지 일을 동시에 할 수 없다.

그래서 동시성은 두가지 일을 동시에 한다는게 아니라, 동시에 하는 것 처럼 보이게 한다는 말이다.

리액트에서의 동시성 렌더링

리액트 18의 동시성 렌더링은 렌더링 작업을 쪼개어 긴급한 작업과 덜 긴급한 작업을 구분하고 우선순위에 따라 작업을 처리할 수 있도록 해주는 기능이다.

  • 긴급한 작업: 사용자 입력, 클릭, 애니메이션 등 즉각적 반응이 필요한 작업
  • 덜 긴급한 작업: 데이터 필터링, 목록 렌더링 등 조금 기다릴 수 있는 작업

리액트에서는 덜 긴급한 작업을 중단하고 우선순위가 높은 작업을 먼저 처리하여 유저 응답성을 높힌다.

리액트 18에서 동시성 렌더링을 위해 추가된 훅 useTransitionuseDeferredValueuseDeferredValue를 이용하여 실습을 해보자.

실습을 위해 인풋에 타이핑을 할때마다 1만개의 박스에 색깔을 랜덤하게 색칠하는 화면을 만들어보았다.
(코드는 글 하단에 걸어두었다.)

리렌더링이 너무 무거워서 렉걸리는 상황

아래 gif는 매 타이핑마다 1만개의 다른 색깔을 가진 박스를 그리느라 렉이 걸린다. 렉걸리는화면

타이핑 후 박스 1만개를 그리는 중에 또다시 타이핑이 들어오면서 렌더링이 blocking 된다.

useDeferredValue 사용해서 우선순위 재조정

defer의 의미

리액트에 useDeferredValue는 값의 연산을 지연 시키는 훅이다.

쉽게 말하면 중요하지 않은 것보다 더 중요한 것을 먼저 처리 하는 것이다.

사용법은 아래와 같다.

  // text의 연산을 후순위로 뺀다.
	
  const [text, setText] = useState("");
  const deferredText = useDeferredValue(text);

이제 박스 1만개를 그리는 연산의 의존성에 deferredText를 넣어서 메모이제이션 할 것이다.

  const [text, setText] = useState("");
  const deferredText = useDeferredValue(text);

  const boxes = useMemo(() => {
    // deferredText가 변경될때만 1만개의 박스가 렌더링된다.
    const x = Math.floor(Math.random() * 256);
    const y = Math.floor(Math.random() * 256);
    const z = Math.floor(Math.random() * 256);
    const backgroundColor = "rgb(" + x + "," + y + "," + z + ")";
    return (
      <TestWrapper>
        {new Array(10000).fill(null).map((_, i) => {
          const x = Math.floor(Math.random() * 256);
          const y = Math.floor(Math.random() * 256);
          const z = Math.floor(Math.random() * 256);
          const backgroundColor = "rgb(" + x + "," + y + "," + z + ")";
          return (
            <div key={i} style={{ width: 100, height: 100, backgroundColor }} />
          );
        })}

        <TestChild color={backgroundColor} />
      </TestWrapper>
    );
  }, [deferredText]);

이렇게 되면 박스 1만개는 deferredText가 변경될때만 렌더링 될 것이다.

deferred Text 이용한 박스 1만개 렌더링

e=>setText(e.target.value) 가 방해하지 않으면서 부드러운 렌더링이 이루어지고 있다.

text와 deferredText 동시 확인

text와 deferredText의 변화를 동시에 확인해보자.

text, deferredText

타이핑을 쉴새없이 몰아치는 동안에는 deferredValue가 업데이트 되지 않고 text만 업데이트 되다가, 타이핑이 멈추면 deferredText가 업데이트 된다.

debounce와 useDeferredValue의 차이

이걸 처음 알게 되었을때 바로 검색창에 debounce를 빼고 useDeferredValue를 넣어보았다.

하지만 검색 api 요청수가 줄지 않았는데, 인풋에 타이핑이 발생 할 때 api 요청을 지연시킬 만큼 blocking 하는 요소가 없기 때문이다. 그래서 검색창에 useDeferredValue를 사용했을때는 모든 타이핑에 검색 api call이 발생했다.

언뜻보면 debounce같기도 하다. 하지만 delay time에 의존하여 이벤트 실행 빈도를 제한하는 것이 아니라, 유저 인터렉션에 의한 응답성과 렌더링 우선순위를 동적으로 관리하여 더 부드러운 사용자 경험을 제공한다는 것에 차이가 있다.

더불어 댄 아브라모브 형님께서는 state를 props로 받는 등, 제어 할 수 없는 값에 사용하면 좋다고 하셨다.

It's useful when the value comes "from above" and you don't actually have control over the corresponding setState call. (링크)

코드

useDeferredValue 미적용 코드
useDeferredValue 적용 코드