하루살이 개발일지

React에서 Debounce, Throttle이 작동하지 않는 이유 본문

웹개발/React

React에서 Debounce, Throttle이 작동하지 않는 이유

harusari 2023. 6. 24. 16:01

전개 - 버그 발생

React로 debounce와 throttle 함수를 공부하다가 갑자기 throttle 함수가 동작하지 않는 문제가 발생하였다.

문제가 되었던 코드는 다음과 같다.

 

import { useEffect, useState } from "react";
import _ from "lodash";

export const Throttle = () => {
  const [count, setCount] = useState(0);

  // Throttle 사용 시
  const throttledCountUp = _.throttle(
    () => setCount((prevCount) => prevCount + 1),
    1000
  );
  const throttledCountDown = _.throttle(() => {
    setCount((prevCount) => prevCount - 1);
  }, 1000);

  // Throttle를 사용하지 않았을 때
  // const countUp = () => setCount(prevCount => prevCount + 1);
  // const countDown = () => setCount(prevCount => prevCount - 1);

  useEffect(() => {
    return () => {
      throttledCountUp.cancel();
      throttledCountDown.cancel();
    };
  }, []);

  return (
    <div>
      <h2>Throttle</h2>
      <button onClick={throttledCountUp}>Count Up</button>
      <button onClick={throttledCountDown}>Count Down</button>
      {/* Throttle를 사용하지 않았을 때 */}
      {/* <button onClick={countUp}>Count Up</button> */}
      {/* <button onClick={countDown}>Count Down</button> */}
      <p>{count}</p>
    </div>
  );
};

원래 기대대로라면, Throttle 컴포넌트 안의 Count Up 버튼과 Count Down 버튼을 빠르게 아무리 많이 눌러도 1초에 최대 1번까지만 눌리는 것이었다.

하지만, 기대와는 다르게 버튼을 누른 횟수만큼 count가 빠르게 증가하였다.

즉, throttle 함수가 제대로 동작하지 않았던 것이다.

 

버그 원인

이는 Throttle의 개념과 관련이 깊은데, throttle은 타이머(=delay)에 지정된 시간 사이에 아무리 이벤트가 많이 발생하여도, 이벤트 핸들러가 ‘최대 한 번만’ 발생하도록 하는 함수이다.

즉, 쓰로틀은 어떠한 타이머 주기를 가지고 있는데, 만약 500ms(= 0.5초)라고 한다면 500ms만큼의 간격을 가지고 있는 것이다.

하지만 위 코드에서 문제가 되는 부분은,

const throttledCountUp = _.throttle(
    () => setCount((prevCount) => prevCount + 1),
    100
  );

const throttledCountDown = _.throttle(() => {
    setCount((prevCount) => prevCount - 1);
  }, 100);

throttleCountUp과 throttleCountDown이 실행되면, 즉 이벤트가 발생하면 내부의 setCount 함수가 실행되고 이는 컴포넌트의 state를 변경하게 된다.

컴포넌트의 state가 변경되게 되면 리렌더링이 발생하고, 리렌더링이 발생하면 함수는 새로 생성되게 된다. (함수의 주소값이 바뀐다는 말)

이렇게 함수가 새롭게 생성되면, throttle이 가지고 있던 타이머는 초기화되고, 매번 이벤트가 발생할 때마다 타이머가 초기화되어서 throttle이 제대로 기능하지 못하게 되는 것이다.


디버깅

결론부터 말하면 이는 useCallback을 사용해 해결할 수 있다.

const throttledCountUp = useCallback(
    _.throttle(() => setCount((prevCount) => prevCount + 1), 1000),
    []
  );

const throttledCountDown = useCallback(
    _.throttle(() => {
      setCount((prevCount) => prevCount - 1);
    }, 1000),
    []
  );

useCallback은 주어진 함수와 dependency를 받아 함수가 재생성되는 것을 막고 최적화를 돕는다.

즉 useCallback은 메모이제이션된 콜백을 반환하여 동일한 의존성이라면 메모이제이션된 함수를 다시 사용한다.

lodash의 debounce나 throttle 함수를 사용할 때, 이 함수들이 생성하는 새로운 함수를 각각 컴포넌트의 렌더링 사이에 유지하기 위해서는 useCallback을 사용해야 한다.

만약 useCallback을 사용하지 않는다면 컴포넌트가 리렌더링 될 때마다 새로운 throttle이나 debounce가 생성되어, 이전에 생성된 함수의 타이머가 초기화된다.

이로 인해 debounce나 throttle의 기능이 제대로 작동하지 않게 된다.

따라서, useCallback을 사용해 이러한 문제를 제대로 작동하게 할 수 있다.