에듀테크 프로젝트 대시보드
디바운스 & 스로틀
스크롤·입력·리사이즈처럼 1초에 수십 번 쏟아지는 이벤트를, 실제 처리 횟수만 똑똑하게 줄이는 두 가지 타이밍 기법
브라우저 이벤트는 생각보다 훨씬 자주 발생한다. 검색창에 "안녕하세요"를 입력하면 input 이벤트가
6번, 마우스로 페이지를 한 번 훑으면 mousemove가 수백 번, 스크롤 한 번에 scroll이
수십 번 터진다. 이때마다 서버에 요청을 보내거나 무거운 레이아웃 계산을 하면 화면이 버벅인다.
이벤트가 오는 빈도와 실제로 일을 하는 빈도를 분리하는 것이 핵심이다.
연타를 멈추면 디바운스는 대기 시간이 지난 뒤 딱 한 번 실행된다(trailing). 스로틀은 연타하는 내내 대기 시간 간격마다 규칙적으로 실행된다. leading 모드로 바꾸면 첫 이벤트에서 즉시 실행된다.
이벤트가 | | | | | | 처럼 들어올 때 (대기 = 3칸):
이벤트 : X X X X . . X X . . . . X . . . 디바운스 : . . . . . . . . . D . . . . . D ← 조용해지고 3칸 뒤 스로틀 : T . . T . . T . . T . . T . . . ← 3칸마다 규칙적으로
디바운스는 입력이 끝났는지가 중요할 때, 스로틀은 진행 중에도 주기적인 갱신이 필요할 때 쓴다.
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));
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));
회색 줄은 매 키 입력(원시 이벤트), 초록 줄은 실제로 "서버 요청"이 나가는 순간이다. 빠르게 타이핑하는 동안에는 요청이 나가지 않다가, 손을 멈추면 마지막 검색어로 한 번만 나간다.
| 상황 | 선택 | 이유 |
|---|---|---|
| 검색어 자동완성 / 실시간 유효성 검사 | 디바운스 | 입력이 끝난 최종 값만 있으면 된다. 중간 값 요청은 낭비. |
| 창 리사이즈 후 레이아웃 재계산 | 디바운스 | 드래그하는 내내가 아니라 놓았을 때 한 번만 다시 그리면 된다. |
| 무한 스크롤 / 스크롤 위치에 따른 헤더 변형 | 스로틀 | 스크롤 도중에도 주기적으로 화면을 갱신해야 부드럽다. |
| 마우스 드래그로 도형 이동, 게임 입력 | 스로틀 | 프레임마다(~16ms) 반응하되 그 이상은 버린다. |
| 버튼 연타로 인한 폼 중복 제출 | 디바운스 (leading) | 첫 클릭만 처리하고 이후 연타는 무시. |
| API 호출 속도 제한(rate limit) 준수 | 스로틀 | "초당 N회" 같은 상한을 코드로 강제. |
onChange={debounce(fn, 300)}처럼 렌더마다 새 함수를 넘기면 타이머가 계속
초기화된다. useMemo/useRef로 같은 함수 인스턴스를 유지해야 한다.
또한 컴포넌트가 사라질 때 예약된 setTimeout을 clearTimeout으로 정리하자.
waitms 동안 잠잠하면 그때 1번 실행 → "멈췄니?"intervalms에 최대 1번 실행 → "천천히 해."