react 에서 debounce를 대신할 수 있는 api가 있다?

YEONGHUN KO·2024년 5월 9일
0

REACT JS - BASIC

목록 보기
31/31
post-thumbnail

리액트 18버전에서 concurrent rendering을 소개했다.

여러가지 컴포넌트가 동시에 랜더링 될때 우선순위를 매기게 할 수 있는 api이다.

구체적으로는 useTransition , useDeferredValue 를 통해 가능하다.

위 두 api를 이용하여 랜더링을 한다면 랜더링을 후순위로 뺄 수 있다.

이는 가장먼저 랜더링을 해야하는 컴포넌트가 랜더링되는데 오래걸리는 무거운 컴포넌트에 의해 랜더링이 늦춰지는 것을 방지 할 수 있다.

가장 대표적인 예시가, debounce를사용해서 랜더링되는데 시간이 오래걸리는 것을 한 번만 수행하는 것일것이다.

그러나 이제는 debounce를 사용하지 않고 리액트에서 제공하는 useTransition , useDeferredValue를 통해 구현가능하다.

아래 코드를 살펴보자. input에 숫자를 입력하면 30000개의 div가 계산되어 출력된다고 하자.

import { ChangeEvent, useState } from "react";

const CalResult = ({ value }: { value: number | string }) => {
  return Number(value) > 0
    ? Array.from(Array(30000).keys()).map((i) => (
        <div key={i} className={"m-0 p-0 col-1"}>
          {Number(value) * (i + 1)}
        </div>
      ))
    : null;
};

export default function Slow() {
  const [num, setNum] = useState(0);

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setNum(Number(e.target.value));
  };

  return (
    <main className=" container my-5">
      <input type="text" name="num" id="num" value={num} onChange={onChange} />
      <span className="ms-5 mt-3 h3">100,000 multiples of number: {num}</span>
      <div className="flex flex-wrap gap-[10px] mt-5">
        <CalResult value={num} />
      </div>
    </main>
  );
}

그럼 input에 숫자를 입력할때마다 아래 영상 처럼 될것이다.

여기서 랜더링 우선순위를 정리해보자.
가장먼저 랜더링되어야하는것은 input태그일것이고,
가장 나중에 랜더링되어야하는 것은 CalResult 컴포넌트일것이다.

그럼 CalResult에 우선 useDeferredValue를 적용하여 우선순위를 늦춰주자.(useTransition을 이용한 방법은 추후에 설명하겠다)

export default function App() {
  const [isPending, startTransition] = useTransition();
  const [num, setNum] = useState(0);
  const [delayedNum, setDelayedNum] = useState<string | number>(0);
  const value = useDeferredValue(num);

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setNum(Number(e.target.value));
    
  };

  const HeavyComponent = useMemo(() => <CalResult value={value} />, [value]);

  return (
    <main className=" container my-5">
      <input type="text" name="num" id="num" value={num} onChange={onChange} />
      <span className="ms-5 mt-3 h3">100,000 multiples of number: {num}</span>
      <div className="flex flex-wrap gap-[10px] mt-5">
       
        {HeavyComponent}
      </div>
    </main>
  );
}

사용방법은 useDeferredValue에 자주 바뀌는 변수값을 인자로 pass시킨다.

그럼 리턴되는 값 value는 가장 먼저 바뀌는 num 이후에 변경이 된다.

이 개념을 useMemo에 적용하여 HeavyComponent를 만들었다.

이렇게 되면, 일반적은 setState 즉 setNum에 의한 랜더링이 가장 먼저 일어나고 이후에 value의 값이 num에 따라 바뀔 것이다.

그럼 useTransition은 무엇이냐? useDeferredValue는 값을 넣었지만 useTransition는 함수를 pass한다.

그리고 isPending이라는 값을 사용할 수 있다. 즉 랜더링되기 전까지는 isPending이 true이다. 이 값으로 로딩바를 구현할 수 있다.

export default function App() {
  const [isPending, startTransition] = useTransition();
  const [num, setNum] = useState(0);
  const [delayedNum, setDelayedNum] = useState<string | number>(0);

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    setNum(Number(e.target.value));
    startTransition(() => {
      setDelayedNum(e.target.value);
    });
  };

  return (
    <main className=" container my-5">
      <input type="text" name="num" id="num" value={num} onChange={onChange} />
      <span className="ms-5 mt-3 h3">100,000 multiples of number: {num}</span>
      <div className="flex flex-wrap gap-[10px] mt-5">
        {isPending ? "Loading..." : <CalResult value={delayedNum} />}
      </div>
    </main>
  );
}

useTransition이 리턴하는 startTransition에 setState를 넣으면 된다.

기억해야할 점은 리액트는 setState가 여러번 같은 스코프에서 호출될지 batch를 통해 랜더링을 최소화한다는 것이다. startTransition안에서도 마찬가지이다.

또한 startTransition안에서 스코프를 하나 더 만들어 setState를 호출하면 리액트가 랜더링 우선순위를 감지하는 콜스택에 쌓이지 않아 의도하지 않은 효과를 낼 수 있다. 아래와 같은 경우이다.

startTransition(() => {
  setTimeout(() => {
    // By the time setTimeout's callback is called
    // we're already in another call stack
    // This will be marked as a high priority update
    // instead
    setCount((count) => count + 1);
  }, 1000);
});

startTransition(() => {
  asyncWork().then(() => {
    // Different call stack
    setCount((count) => count + 1);
  });
});

그래서 아래와 같이 변경해야한다.

setTimeout(() => {
  startTransition(() => {
    setCount((count) => count + 1);
  });
}, 1000);

asyncWork().then(() => {
  startTransition(() => {
    setCount((count) => count + 1);
  });
});

끝!

출처

  1. https://blog.codeminer42.com/everything-you-need-to-know-about-concurrent-react-with-a-little-bit-of-suspense/

  2. https://doiler.tistory.com/83

  3. https://dev.to/sameer1612/react-v18-usetransition-hook-why-3bml

  4. https://velog.io/@ktthee/React-18-%EC%97%90-%EC%B6%94%EA%B0%80%EB%90%9C-useDeferredValue-%EB%A5%BC-%EC%8D%A8-%EB%B3%B4%EC%9E%90

profile
'과연 이게 최선일까?' 끊임없이 생각하기

0개의 댓글