디바운싱(Debouncing)과 쓰로틀링(Throttling) 개념 및 Lodash 실전 적용

Kingmo·2022년 4월 9일

웹 프론트엔드에서 input, scroll, resize와 같은 이벤트는 짧은 시간 동안 수십, 수백 번 연속해서 발생한다.

이를 최적화 없이 매번 핸들러나 API 호출로 연결하면 아래 사진처럼 브라우저 성능 저하와 서버 트래픽 과부하를 초래한다. 이러한 빈번한 이벤트 호출을 제어하는 대표적인 두 가지 기법이 디바운싱(Debouncing)과 쓰로틀링(Throttling)이다.

1. 디바운싱 (Debouncing)

연이어 호출되는 이벤트들을 하나의 그룹으로 묶어, 마지막 이벤트가 발생한 후 일정 시간 동안 추가 입력이 없을 때 딱 한 번만 실행하는 방식이다.

  • 동작 원리: 이벤트가 발생할 때마다 타이머를 초기화하고 다시 시간을 잰다. 사용자가 행동을 멈추고 지정된 시간이 지나야 비로소 콜백 함수가 실행된다.
  • 대표적인 활용 사례: 검색창 자동 완성, 실시간 검색어 API 호출, 윈도우 리사이즈(resize) 후 레이아웃 재계산
이벤트 발생:  | | | |||| |   (입력 멈춤) --------> [실행!]
타이머 리셋:  R R R RRRR R                       (지정 시간 경과)

디바운싱을 적용하지 않고 검색창 onChange마다 API를 호출하면, 사용자가 '리액트'를 칠 때 ㄹ, 리, 리ㅇ, 리애, 리액, 리액ㅌ, 리액트처럼 글자 조합마다 불필요한 네트워크 요청이 쏟아진다. 디바운싱을 적용하면 타이핑이 완전히 멈췄을 때 한 번만 요청을 보낼 수 있다.

2. 쓰로틀링 (Throttling)

연이어 발생하는 이벤트에 대해 일정한 시간 간격(delay)을 두고 최대 한 번만 실행되도록 제한하는 방식이다.

  • 동작 원리: 이벤트가 계속 발생하더라도, 설정한 주기(예: 300ms) 동안에는 추가 호출을 무시하고 주기마다 정해진 횟수만큼만 실행을 보장한다.
  • 대표적인 활용 사례: 무한 스크롤(scroll), 드래그 앤 드롭(drag), 마우스 움직임 추적(mousemove)
이벤트 발생:  |||||||||||||||||||||||||||||||||||||||||||||||||
실행 주기:    [실행] ---- 300ms ---- [실행] ---- 300ms ---- [실행]

무한 스크롤 구현 시 스크롤을 한 번 휠 조작하는 것만으로도 수십 개의 스크롤 이벤트가 트리거된다. 이때 쓰로틀링을 걸어두면 0.3초마다 한 번씩만 스크롤 위치를 계산하므로 브라우저 버벅임을 방지하고 안정적으로 다음 목록 데이터를 호출할 수 있다.

3. 디바운싱 vs 쓰로틀링 비교

구분디바운싱 (Debouncing)쓰로틀링 (Throttling)
실행 타이밍연속된 이벤트가 완전히 끝난 후 일정 시간 뒤 1회연속된 이벤트가 진행되는 도중에도 일정 주기마다 지속 실행
목적마지막 최종 결과물 하나만 필요할 때실행 빈도를 줄이되 중간 진행 상태를 계속 반영해야 할 때
적합한 작업검색어 자동완성, 폼 유효성 검증무한 스크롤 위치 감지, 애니메이션 스크롤 인터랙션

4. Lodash를 활용한 디바운싱 구현

유틸리티 라이브러리인 Lodash의 _.debounce 함수를 활용하면 복잡한 타이머 로직 없이 간단하게 구현할 수 있다.

패키지 설치

yarn add lodash
yarn add --dev @types/lodash # TypeScript 사용 시

기본 사용 예제

_.debounce(callback, waitTime) 구조로, 첫 번째 인자에는 지연 후 실행할 콜백을, 두 번째 인자에는 대기 시간(ms 단위)을 넘긴다.

import _ from "lodash";

export default function SearchInput() {
  // 500ms 동안 추가 입력이 없을 때 최종 value를 출력
  const handleDebounceSearch = _.debounce((value) => {
    console.log("서버로 검색 요청 전송:", value);
  }, 500);

  const onChangeInput = (event) => {
    // 이벤트 객체 대신 실제 문자열 값을 전달하는 것이 안전하다.
    handleDebounceSearch(event.target.value);
  };

  return <input type="text" onChange={onChangeInput} placeholder="검색어를 입력하세요" />;
}

React 환경에서 사용할 때 주의할 점: 인스턴스 재생성 문제

위 컴포넌트 내부에서 단순히 const handleDebounceSearch = _.debounce(...) 형태로 선언하면, 컴포넌트가 리렌더링될 때마다 새로운 디바운스 함수 인스턴스가 계속 생성된다.

그 결과 이전 타이머가 정상적으로 초기화되지 않고 매 입력마다 타이머가 따로 돌아 디바운싱이 무력화될 수 있다.

이를 방지하려면 useCallback이나 useMemo로 디바운스 함수 인스턴스를 메모이제이션해 두어야 리렌더링 사이에서도 단일 타이머가 유지된다.

import { useCallback } from "react";
import _ from "lodash";

export default function SearchInput() {
  // useCallback을 활용해 디바운스 함수의 참조값을 유지
  const handleDebounceSearch = useCallback(
    _.debounce((value) => {
      console.log("최적화된 검색 요청:", value);
    }, 500),
    []
  );

  const onChangeInput = (event) => {
    handleDebounceSearch(event.target.value);
  };

  return <input type="text" onChange={onChangeInput} />;
}
profile
Developer

0개의 댓글