하루살이 개발일지

React) Debounce와 Throttle의 개념과 예시 및 사용법 (+lodash) 본문

웹개발/React

React) Debounce와 Throttle의 개념과 예시 및 사용법 (+lodash)

harusari 2023. 6. 24. 15:59

디바운스 & 스로틀이란 ?

scroll, resize, input, mousemove 같은 이벤트는 짧은 시간 간격으로 연속해서 발생함. 이러한 이벤트에 바인딩한 이벤트 핸들러는 과도하게 호출되어 성능에 문제를 일으킬 수 있음. 디바운스와 스로틀은 짧은 시간 간격으로 연속해서 발생하는 이벤트를 그룹화하여 과도한 이벤트 핸들러의 호출을 방지하는 프로그래밍 기법


디바운스

 

[디바운스란?]

  • 짧은 시간 간격으로 이벤트 연속해 발생하면 이벤트 핸들러를 호출하지 않다가 일정 시간이 경과한 이후에 이벤트 핸들러가 한 번만 호출되도록 하는 것
  • 즉 짧은 시간 간격으로 발생하는 이벤트를 그룹화하여 마지막에 한 번만 이벤트 핸들러가 호출되도록 함

[사용 예시]

  • 텍스트 입력 필드에서 input 이벤트가 짧은 시간 간격으로 연속해서 발생하는 경우
    • input 이벤트는 사용자가 입력할 때마다 연속해서 발생
    • 만약 input 이벤트 핸들러에서 사용자가 입력 필드에 요청한 값으로 ajax 요청과 같은 무거운 처리 수행한다면 사용자가 입력을 완료 못해도 ajax 요청이 전송될 것
    • 이는 서버에도 부담가는 불필요한 처리 ⇒ 사용자가 입력 완료했을 때 한 번만 ajax 요청 전송하는 것이 바람직
  • 사용자가 입력 완료했는지 정확히 알 수 없으므로 일정 시간 동안 텍스트 입력 필드에 값을 입력하지 않으면 입력이 완료된 것으로 간주
  • 이를 위해 debounce 함수를 활용할 수 있음
  • delay보다 짧은 시간 간격으로 이벤트 발생 시 이전 타이머를 취소하고 새로운 타이머를 재설정
  • 즉 delay보다 짧은 간격으로 이벤트 연속 발생 시 debounce 함수의 첫 번째 인자인 콜백 함수를 호출하지 않다가, delay동안 input 이벤트가 더 이상 발생하지 않으면 한 번만 호출됨

[주로 사용할 때]

  • resize 이벤트 처리
  • input 요소에 입력된 값으로 ajax 요청하는 입력 필드 자동완성 UI 구현
  • 버튼 중복 클릭 방지 처리

실무에서는 Underscore의 debounce 함수나 Lodash의 debounce 함수를 사용하는 것을 권장


스로틀

[쓰로틀이란?]

  • 짧은 시간 간격으로 이벤트 연속해 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출되도록 하는 것
  • 즉 짧은 시간 간격으로 연속해 발생하는 이벤트를 그룹화해서 일정 시간 단위로 이벤트 핸들러가 호출되도록 호출 주기를 만드는 것

[주로 사용할 때]

  • scroll 이벤트 처리
  • 무한 스크롤 UI 구현

마찬가지로 실무에서는 Underscore의 throttle 함수나 Lodash의 throttle 함수를 사용하는 것이 권장됨


Debounce의 예시

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

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

  // Debounce 사용 시
  const debouncedCountUp = useCallback(
    _.debounce(() => setCount((prevCount) => prevCount + 1), 1000),
    []
  );

  const debouncedCountDown = useCallback(
    _.debounce(() => setCount((prevCount) => prevCount - 1), 1000),
    []
  );

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

  useEffect(() => {
    return () => {
      debouncedCountUp.cancel();
      debouncedCountDown.cancel();
    };
  }, []);

  return (
    <div>
      <h2>Debounce</h2>
      {/* Debounce를 사용했을 때 */}
      <button onClick={debouncedCountUp}>Count Up</button>
      <button onClick={debouncedCountDown}>Count Down</button>

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

CountUp 버튼과 CountDown 버튼을 아무리 많이 클릭해도 1000ms(=1초) 기다리지 않으면 count가 증가하거나 감소하지 않는다.

참고로 debounce나 throttle 함수를 사용하면 꼭 useEffect의 clean-up 함수(return 부분)로 감싸주어야 한다.

 

그 이유는 컴포넌트가 언마운트된 이후에도 두 함수에 의해 예약된 함수 호출이 존재하면, 그 함수는 계속해서 실행될 수 있기 때문이다.

이러한 상황은 메모리 누수로 이어질 수 있다.

 

만약에 컴포넌트가 언마운트되었는데도 컴포넌트의 상태를 변경하는 작업이 이루어진다면 (이 경우에는 debounce나 throttle) 이는 Can't perform a React state update on an unmounted component"와 같은 경고를 일으킬 수 있다.


Throttle의 예시

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

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

  // Throttle 사용 시
  const throttledCountUp = useCallback(
    _.throttle(() => setCount((prevCount) => prevCount + 1), 1000),
    []
  );
  const throttledCountDown = useCallback(
    _.throttle(() => {
      console.log(123);
      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>
  );
};

이제는 버튼을 아무리 많이 클릭해도 내가 설정한 시간(현재는 1000ms) 동안에는 최대 딱 1번만 카운트가 바뀐다.

나머지 기능은 debounce와 동일하다.