useDeferredValue,useTransition

김대은·2022년 11월 3일
0

리액트가 18버전이 되면서, 많은 기능이 추가되었다.
그중 가장 돋보이는 변화는 '동시성' 개념이다.
React v18.0 Blog
기존에 디바운싱,스로트링과 같이 미묘한 처리로 우회하던 무제를
react hooks로 직접 해결할 수 있게 되었다.

디바운싱,스로틀링

자바스크립트에서는 짧은 시간에 유저 입력 등의 이벤트가 많이 일어나는 경우 화면이 끊길 수 있다. 그래서 이런 경우 특별한 최적화가 필요하다.

대표적인 예가 바로 자동완성 기능이다.
만약 유저 입력 한번에 추천검색어 이벤트 한 번이 일어난다면, 불필요한 리랜더링이 여러번 발생할 것이다.
그래서 정작 필요한 '진짜 검색어' 에 대한 자동완성이 느려질 수 있다.
이러한 상황을 방지하기 위해
디바운싱 이라는 처리를 한다.

디바운싱은 특정 함수의 호출이 너무 자주 일어나지 않도록 일정 시간 동안 함수 실행을 지연시키는 것을 의미한다.

import React, { useState, useEffect } from "react";

const useQueryDebounce = (value, delay) => {
  const [debounceValue, setDevounceValue] = useState("");

  useEffect(() => {
    const handler = setTimeout(() => {
      setDevounceValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);
  return debounceValue;
};

export default useQueryDebounce;

모든 문제가 해결되었을까?

디바운싱과 스로틀링에서 적절한 딜레이를 선택하는 것은 꽤 어렵다.
딜레이가 너무 길면 유저 입력에 대한 반응도 그만큼 느려지지만,
너무 짧으면 적용하는 의미가 없어진다.
유저가 빠르게 입력할 경우, 분명히 그걸 전부 처리하는것은 비효율적이다.
그러나 디바운싱을 적용한다면 delay를 얼마로 주어야 할까?
그건 컴퓨터 사양에 따라 다를것이다.

게다가 디바운싱을 적용한다고 해도, 일단 계산을 시작하면 메인 스레드가 거기서 블록되기 때문에 다음 입력을 처리하지 못한다.
한 순간 입력을 멈추면 다음 입력을 받지 못할 정도로 프레임이 저하된다.

사실, 디바운싱과 스로틀링은 문제를 근본적으로 해결하는 방법은 아니다.
이 문제를 근본적으로 해결하려면 반응속도는 최대한 빠르게 하되,
사용자의 상호작용이 있으면 그걸 우선적으로 처리하여 화면이 멈춘것처럼 보이지 않게 해야한다.
무거운 연산은 메인 스레드가 놀고 있을때만 처리하고, 유저 입력이 들어오면 다시 거기에 집중하는 것이다.

즉, 이벤트의 우선순위를 나누고, 우선순위가 높은 이벤트가 발생하면
context switch하여 그 작업을 먼저 핸들링하면 된다.
그러나 자바스크립트에는 병령 스레드나, 스케쥴러가 없다.

useTransition

상태 변화의 우선순위를 지정하기 위해 useTranstion훅이 새로 도입되었다.
[inpending,startTransition]을 반환하는데,
ispending은 작업이 지연되고 있음을 알리는 boolean이며,
startTransition은 낮은 우선순위로 실행할 함수를 인자로 받는다.

function App() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);
  
  function handleClick() {
    startTransition(() => {
      setCount(c => c + 1);
    })
  }
  

  return (
    <div>
      {isPending && <Spinner />}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
}

클릭할 때마다 일어나는 count에 대한 상태 업데이트를 낮은 우선순위로 실행한다.
그래서 더 중요한 이벤트가 있는 경우, count의 업데이트를 지연시키고 대신 이전의 값을 보여준다.
inpending을 이용하여 업데이터가 지연되는동안 스피너를 보여줄 수도 있다.

useDeferredValue

이 훅 또한 useTransition과 유사하게 낮은 우선순위를 지정한다.
다만, useTransition은 함수 실행의 우선순위를 지정하는 반면,
useDefferdValue는 값의 업데이트 우선순위를 지정한다.
우선순위가 높은 작업을 실행하는 동안 useMemo와 유사하게 이전의 값을 가지고 있으면서 업데이트를 지연시킨다.

이 훅은 useMemo와 함께 사용하면 효과가 더 좋다.
종속된 값들을 memoize 시키면 불필요한 재렌더링을 막으면서 하위 컴포넌트나 상태의 업데이트를 지연시킬 수 있다.

아래는 검색 자동완성을 처리하는 코드이다.
사용자 입력 query의 업데이트에 대해 추천 검색어인 suggetions을 띄워준다.
추천 검색어의 업데이트는 우선 순위 높은 작업이 없을때만 실행되기 때문에
디바운싱을 적용하지 않아도 좋은 유저경험을 줄 수 있다.

function App() {
  const [isPending, startTransition] = useTransition();
  const [count, setCount] = useState(0);
  
  function handleClick() {
    startTransition(() => {
      setCount(c => c + 1);
    })
  }
  

  return (
    <div>
      {isPending && <Spinner />}
      <button onClick={handleClick}>{count}</button>
    </div>
  );
}

참고

profile
매일 1% 이상 씩 성장하기

0개의 댓글