웹 애플리케이션을 개발하면서 실시간 검색 입력창이나 창 크기 조절(Resize), 스크롤 이벤트를 구현할 때 매 밀리초마다 이벤트가 수십, 수백 번 폭주하는 현상을 경험해봤을 것이다.
다만 이벤트가 과도하게 실행되면서 브라우저가 버벅이거나 서버 API 호출 비용이 낭비되고 있음에도 정확한 원인이나 제어 방법을 모르는 경우가 많다.
이번에는 자바스크립트의 대표적인 성능 최적화 기법인 디바운스(Debounce)와 쓰로틀링(Throttle)이 정확히 뭔지, 왜 필요한지, 그리고 실무에서 어떻게 구현하고 적용하는지 완벽하게 정리해서 소개하겠다.
사용자가 이벤트를 발생시킬 때 브라우저는 해당 이벤트 조건이 유지되는 동안 아주 짧은 시간 간격으로 지속해서 콜백 함수를 실행한다. 예를 들어 검색창에 키보드로 글자를 입력할 때 누를 때마다 서버로 요청을 전송하거나, 마우스 스크롤을 내릴 때마다 DOM 좌표 계산 함수가 연속으로 호출되는 방식이다.
이로 인해 CPU 사용량이 급증하고 화면 프레임이 떨어지는 렌더링 병목 현상이 일어난다. 디바운스와 쓰로틀링은 이러한 연속적인 이벤트 실행 횟수를 제어하여 웹 페이지의 반응 속도와 서버 부하를 획기적으로 개선하는 기술이다.
디바운스는 연이어 호출되는 함수들 중 마지막 함수(또는 가장 첫 번째 함수)만 실행되도록 제어하는 기법이다. 지정한 시간 동안 추가 이벤트가 발생하지 않을 때 비로소 실행된다. 대표적으로 검색어 자동완성 기능에서 사용자가 타자 치기를 완전히 멈추었을 때 최종 입력값만 서버로 전송할 때 매우 유용하다.
쓰로틀링은 이벤트를 연속해서 발생시키더라도 지정한 시간 간격(Interval) 내에서는 최대 한 번만 실행되도록 제한하는 기법이다. 무한 스크롤의 스크롤 위치 계산이나 화면 Resize 시 레이아웃을 다시 계산하는 작업처럼 일정 주기마다 꾸준히 실행되어야 하는 이벤트에 적합하다.
두 기술은 모두 과도한 함수 호출을 줄여주지만 실행 타이밍과 동작 목적에서 확실한 차이가 존재한다.
| 구분 | 디바운스 (Debounce) | 쓰로틀링 (Throttle) |
|---|---|---|
| 동작 방식 | 이벤트 발생이 멈춘 후 일정 시간이 지나야 1회 실행 | 이벤트가 계속 발생해도 설정한 시간 간격으로 주기적 실행 |
| 주요 목적 | 연속된 마지막 입력/요청 한 번만 처리 | 이벤트 발생 빈도를 일정한 주기로 제한 |
| 주요 활용처 | 검색어 자동완성, 폼 유효성 검사, 결제 버튼 중복 클릭 방지 | 스크롤 위치 추적(무한 스크롤), 창 크기 변경(Resize), 연속 클릭 제어 |
다만 대부분의 개발자들은 디바운스와 쓰로틀링의 내부 동작 원리를 모른 채 외부 라이브러리(Lodash 등)에만 의존하는 경우가 많다. 타이머 함수인 setTimeout과 clearTimeout을 활용하면 순수 자바스크립트만으로도 쉽게 작성할 수 있다.
✗ 잘못된 코드 (글자를 누를 때마다 매번 API 요청 전송)
const inputElement = document.querySelector('#searchInput');
// 키 입력마다 매번 API 호출 발생
inputElement.addEventListener('input', (e) => {
console.log('API 요청 전송:', e.target.value);
});✓ 올바른 코드 (입력이 멈추고 300ms 후 한 번만 전송)
function debounce(func, delay) {
let timerId = null;
return function (...args) {
if (timerId) clearTimeout(timerId);
timerId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
const inputElement = document.querySelector('#searchInput');
const handleSearch = debounce((e) => {
console.log('API 요청 전송:', e.target.value);
}, 300);
inputElement.addEventListener('input', handleSearch);결과 및 동작 설명:
'자바스크립트'라는 단어를 빠르게 입력할 때 기존에는 15회 이상의 API 요청이 전송되었으나, 디바운스 적용 후에는 타자 입력을 끝내고 300ms가 지난 시점에 최종 단어로 1회만 요청이 실행된다.
✗ 잘못된 코드 (스크롤 시 초당 수십~수백 번의 함수 실행)
// 스크롤 동작마다 함수가 폭주하여 화면 끊김 현상 발생
window.addEventListener('scroll', () => {
console.log('현재 스크롤 Y 위치:', window.scrollY);
});✓ 올바른 코드 (200ms 주기로 일정하게 제한하여 실행)
function throttle(func, limit) {
let inThrottle = false;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
const handleScroll = throttle(() => {
console.log('현재 스크롤 Y 위치:', window.scrollY);
}, 200);
window.addEventListener('scroll', handleScroll);결과 및 동작 설명:
사용자가 2초 동안 빠르게 페이지를 스크롤하더라도 콜백 함수가 수백 번 호출되는 대신 200ms마다 1번씩 총 10번만 균일하게 실행되어 부드러운 스크롤 경험을 제공한다.
디바운스와 쓰로틀링을 사용할 때 자주 발생하는 실수를 점검해보자.
✗ 잘못된 예시: 이벤트 리스너 콜백 내부에서 최적화 함수를 매번 재생성하는 경우
// 이벤트가 실행될 때마다 새 클로저가 생성되어 타이머 리셋이 동작하지 않음
window.addEventListener('resize', () => {
debounce(() => {
console.log('창 크기 변경 완료');
}, 300)();
});✓ 올바른 예시: 최적화 함수(클로저)를 외부에 생성해 핸들러로 등록하는 경우
// 타이머 변수 상태가 인스턴스에 유기적으로 남아 정상 동작함
const handleResize = debounce(() => {
console.log('창 크기 변경 완료');
}, 300);
window.addEventListener('resize', handleResize);
JavaScript Debounce와 Throttle은 과도한 이벤트 호출을 막아 웹 서비스의 성능과 사용자 경험을 향상시키는 핵심 최적화 기법이다. 작은 최적화 습관이 모여서 웹 애플리케이션의 전체적인 완성도를 높이는 큰 효과를 만든다는 점을 잊지 말자. 이 글의 실전 구현 코드를 참고해 스크롤 및 검색 이벤트에 적절히 적용해 보면, 서버 부하 감소와 부드러운 UI 응답성이라는 최종 결과를 얻을 수 있을 것이다.