debounce 및 throttle를 react에서 사용할 때 주의할 점

Sep 14, 2023

debounce, throttle이란?

debouncethrottle은 성능 향상을 위해 함수 실행을 지연시켜 함수의 빈번한 호출을 제한하는 프로그래밍 패턴 또는 기술이다. 아래와 같은 차이점이 존재한다.

  • 디바운싱
    • 함수를 다시 호출하기 전에 일정 시간 동안 기다림
    • 이벤트가 여러 번 트리거 되더라도 함수가 한 번만 호출되도록 함
    • 특정 비활성 기간이 지날 때까지 함수 호출을 지연하려는 경우 유용함
  • 쓰로틀링
    • 특정 시간 동안 함수가 호출될 수 있는 횟수를 제한함
    • 이벤트가 여러 번 트리거 되는 경우에도 함수가 정기적으로 호출되도록 함
    • 함수 호출 빈도를 나열하려는 경우 유용함

lodash 등으로 쉽게 사용이 가능하다.

문제

import { debounce } from 'lodash-es';
import { useState } from 'react';

function App() {
  const [count, setCount] = useState(0);

  const debouncedFn = debounce(
    () => {
      setCount((c) => c + 1);
    },
    1000,
    { trailing: false, leading: true },
  );

  return (
    <div>
      <button type="button" onClick={debouncedFn}>
        add count
      </button>
    </div>
  );
}

위의 코드는 1s 동안 setCount 실행을 방지하기 위해 debounce를 적용하였지만, 의도한 대로 작동하지 않는다.
이유는 함수가 실행될 때 count가 증가하면서 리렌더링이 발생하고, 그때마다 debounce 함수가 재실행되면서 새로운 함수가 생성되어 debounce의 타이머 처리가 무의미해지기 때문이다.
리렌더링이 발생하지 않는 컴포넌트라면 위와 같이 사용해도 대응이 가능하지만, 대부분의 리액트 컴포넌트는 렌더링을 가정하고 작성하기 때문에 의도하지 않는 동작이 될 수 있다.

해결방법

따라서 아래와 같은 훅을 작성해서 대응이 가능하다. 이렇게 작성하면 useLayoutEffect에 의해서 함수 인자가 변경되더라도 debounce에서 변경된 함수를 실행하지만, 리렌더링으로 인한 debounce 함수의 재호출은 방지할 수 있다.

import { debounce } from 'lodash-es';

// 함수를 debounce 함수로 만들어주는 훅
function useDebounce(func, delay, leading = true) {
  const funcRef = useRef(func);

  useLayoutEffect(() => {
    funcRef.current = func;
  });

  return useMemo(
    () =>
      debounce((...args) => funcRef.current(...args), delay, {
        leading,
        trailing: !leading,
      }),
    [delay, leading],
  );
}

function App() {
  const [count, setCount] = useState(0);

  const debouncedFn = useDebounce(
    () => {
      setCount((c) => c + 1);
    },
    1000,
    { trailing: false, leading: true },
  );

  return (
    <div>
      <button type="button" onClick={debouncedFn}>
        add count
      </button>
    </div>
  );
}

참고