한양과학기술고등학교

에듀테크 프로젝트 대시보드

디바운스 & 스로틀

디바운스 & 스로틀

스크롤·입력·리사이즈처럼 1초에 수십 번 쏟아지는 이벤트를, 실제 처리 횟수만 똑똑하게 줄이는 두 가지 타이밍 기법

한 줄 요약 — 디바운스는 "이벤트가 멈출 때까지 기다렸다가 마지막에 한 번", 스로틀은 "아무리 자주 와도 일정 간격에 한 번"만 실행한다.

왜 필요할까?

브라우저 이벤트는 생각보다 훨씬 자주 발생한다. 검색창에 "안녕하세요"를 입력하면 input 이벤트가 6번, 마우스로 페이지를 한 번 훑으면 mousemove가 수백 번, 스크롤 한 번에 scroll이 수십 번 터진다. 이때마다 서버에 요청을 보내거나 무거운 레이아웃 계산을 하면 화면이 버벅인다. 이벤트가 오는 빈도와 실제로 일을 하는 빈도를 분리하는 것이 핵심이다.

직접 만져 보기 — 이벤트 폭우 시뮬레이터

대기 시간 500ms
원시 이벤트  ·  디바운스 실행  ·  스로틀 실행  ·  오른쪽이 "지금"
원시 이벤트
0
디바운스 실행
0
스로틀 실행
0

연타를 멈추면 디바운스는 대기 시간이 지난 뒤 딱 한 번 실행된다(trailing). 스로틀은 연타하는 내내 대기 시간 간격마다 규칙적으로 실행된다. leading 모드로 바꾸면 첫 이벤트에서 즉시 실행된다.

타임라인으로 비교

이벤트가 | | | |   | | 처럼 들어올 때 (대기 = 3칸):

이벤트   :  X  X  X  X  .  .  X  X  .  .  .  .  X  .  .  .
디바운스 :  .  .  .  .  .  .  .  .  .  D  .  .  .  .  .  D   ← 조용해지고 3칸 뒤
스로틀   :  T  .  .  T  .  .  T  .  .  T  .  .  T  .  .  .   ← 3칸마다 규칙적으로

디바운스는 입력이 끝났는지가 중요할 때, 스로틀은 진행 중에도 주기적인 갱신이 필요할 때 쓴다.

구현 코드

디바운스 (trailing)

function debounce(fn, wait) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);              // 이전 예약 취소
    timer = setTimeout(() => {
      fn.apply(this, args);           // wait 동안 조용하면 실행
    }, wait);
  };
}

// 사용: 입력이 400ms 멈추면 검색
input.addEventListener('input', debounce(search, 400));

스로틀 (timestamp 방식)

function throttle(fn, interval) {
  let last = 0;
  return function (...args) {
    const now = Date.now();
    if (now - last >= interval) {       // 마지막 실행 후 interval 지났으면
      last = now;
      fn.apply(this, args);
    }
  };
}

// 사용: 스크롤 위치를 최대 100ms마다 갱신
window.addEventListener('scroll', throttle(updateHeader, 100));
leading vs trailing — 디바운스에서 leading은 "폭우가 시작될 때 한 번", trailing은 "폭우가 끝난 뒤 한 번" 실행한다. 버튼 중복 클릭 방지에는 leading, 검색어 자동완성에는 trailing이 자연스럽다. 라이브러리(lodash)는 두 옵션을 동시에 지원한다.

실전 예제 — 검색어 자동완성 (디바운스 400ms)

키 입력을 기다리는 중…

회색 줄은 매 키 입력(원시 이벤트), 초록 줄은 실제로 "서버 요청"이 나가는 순간이다. 빠르게 타이핑하는 동안에는 요청이 나가지 않다가, 손을 멈추면 마지막 검색어로 한 번만 나간다.

언제 무엇을 쓸까

상황선택이유
검색어 자동완성 / 실시간 유효성 검사디바운스입력이 끝난 최종 값만 있으면 된다. 중간 값 요청은 낭비.
창 리사이즈 후 레이아웃 재계산디바운스드래그하는 내내가 아니라 놓았을 때 한 번만 다시 그리면 된다.
무한 스크롤 / 스크롤 위치에 따른 헤더 변형스로틀스크롤 도중에도 주기적으로 화면을 갱신해야 부드럽다.
마우스 드래그로 도형 이동, 게임 입력스로틀프레임마다(~16ms) 반응하되 그 이상은 버린다.
버튼 연타로 인한 폼 중복 제출디바운스 (leading)첫 클릭만 처리하고 이후 연타는 무시.
API 호출 속도 제한(rate limit) 준수스로틀"초당 N회" 같은 상한을 코드로 강제.
함정 — 디바운스·스로틀이 반환하는 함수는 매번 새로 만들면 소용없다. React에서 onChange={debounce(fn, 300)}처럼 렌더마다 새 함수를 넘기면 타이머가 계속 초기화된다. useMemo/useRef로 같은 함수 인스턴스를 유지해야 한다. 또한 컴포넌트가 사라질 때 예약된 setTimeout을 clearTimeout으로 정리하자.

정리