웹 사이트를 사용하다보면 사용자와의 상호작용하는 부분들이 많이 존재한다.
스크롤, 창 크기 조절, 키보드 입력 등 수많은 이벤트가 쉴 새 없이 발생하는데 만약 이벤트가 발생할 때마다 복잡한 연산이나 API 호출과 같은 무거운 작업을 수행한다면 어떻게 될까? 사용자가 검색창에 '강남맛집'을 입력하는 동안 '강', '강남', '강남맛' ... 등 모든 입력마다 API를 호출하게 되어 서버에 엄청난 부하를 주게 될 것이다.

이러한 문제를 해결하기 위해 디바운스(Debounce) 와 쓰로틀(Throttle) 이 등장했다.
두 기법은 과도한 이벤트 핸들러 호출을 제어하여 성능을 최적화하는 것을 목표로 한다.
디바운스는 연속적으로 발생하는 이벤트를 하나의 그룹으로 묶어, 특정 시간 동안 추가적인 입력이 없을 때 마지막 이벤트에 대해서만 핸들러를 실행하는 기법이다.
위의 예시에서 '강남 맛집'을 입력 시 모든 입력마다 API를 호출하는것이 아닌, 일정한 시간동안 사용자의 입력이 없을 경우에만 API를 호출할 수 있게 된다.
/**
* 디바운스 함수
* @param {Function} func - 디바운스를 적용할 함수
* @param {number} delay - 지연 시간 (밀리초)
* @returns {Function} - 디바운스가 적용된 새로운 함수
*/
const debounce = (func, delay) => {
let timeoutId; // setTimeout의 타이머 ID를 저장할 변수
// 디바운스가 적용된 새로운 함수를 반환
return (...args) => {
// 이전에 설정된 타이머가 있다면 취소
clearTimeout(timeoutId);
// delay 이후에 func 함수를 실행하는 새로운 타이머 설정
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
};
// --- 사용 예시 ---
// 디바운스를 적용할 함수
const searchAPI = (keyword) => {
console.log(`'${keyword}' 검색 요청을 보냅니다.`);
// 실제로는 여기에 API 호출 코드가 들어갑니다.
};
// 500ms(0.5초) 지연 시간을 갖는 디바운스 함수 생성
const debouncedSearch = debounce(searchAPI, 500);
const searchInput = document.getElementById('searchInput');
searchInput.addEventListener('input', (event) => {
debouncedSearch(event.target.value);
});
timeoutId라는 변수를 클로저(closure) 를 이용해 내부에서 유지한다.clearTimeout(timeoutId): 호출될 때마다 이전에 설정되어 있던 타이머(만약 있다면)를 취소한다.setTimeout(...): 그리고 나서 새로운 타이머를 설정한다. 이 타이머는 delay 시간 후에 원래의 func 함수를 실행한다.debounce(searchAPI, 500)가 실행되면, debounce 함수는 searchAPI와 500이라는 재료를 가지고 새로운 함수를 조립해서 반환하고, const debouncedSearch = ... 코드를 통해, 이렇게 새롭게 만들어진 함수에 debouncedSearch라는 이름이 붙는 것이다.
debouncedSearch는 아래와 같은 함수라고 생각하면 된다.
// debouncedSearch의 실제 모습 (개념적으로)
(..args) => {
clearTimeout(timeoutId); // timeoutId는 클로저를 통해 기억하고 있음
timeoutId = setTimeout(() => {
// searchAPI를 args와 함께 실행!
searchAPI.apply(this, args);
}, 500); // delay도 클로저를 통해 기억하고 있음
}
따라서 debouncedSearch(event.target.value)를 호출하면, event.target.value가 ...args라는 매개변수로 들어가게 되고, setTimeout 내부에서 최종적으로 searchAPI(event.target.value)가 실행되는 구조이다.
사용자가 '강', '남', '맛', '집'을 빠르게 입력한다고 가정해보자
1. '강' 입력: 500ms 후에 searchAPI('강')을 실행하도록 타이머가 설정됨
2. (500ms가 지나기 전) '남' 입력: 이전 타이머('강'에 대한)는 취소되고, 500ms 후에 searchAPI('강남')를 실행할 새 타이머가 설정됨
3. '맛', '집' 이 입력될 때마다 이 과정 반복
4. 마지막 글자 '집'를 입력하고 500ms 동안 추가 입력이 없으면 마지막으로 설정된 타이머가 실행되어 searchAPI('강남맛집')이 딱 한 번 호출된다.
-> 이렇게 하면 불필요한 API 호출을 크게 줄여 성능을 최적화할 수 있다!
apply()는 자바스크립트 함수를 특정 this 값과 인수 배열을 가지고 호출할 때 사용하는 내장 메서드이다.
쉽게 말해, 함수를 대신 호출해 주면서 함수 내부의 this가 누구를 가리킬지 지정하고, 파라미터를 배열 형태로 전달하는 역할을 한다.
func.apply(this, args)
(func.apply(thisValue, [arg1, arg2, ...]) 형태로 사용된다.)
최신 자바스크립트
요즘에는 화살표 함수와 스프레드 문법(...)을 사용하면 apply나 call 없이도 더 직관적으로 코드를 작성할 수 있는 경우가 많다.
setTimeout(() => func(...args), delay);
this가 화살표 함수에 의해 자동으로 상위 스코프에 바인딩되므로 apply를 쓴 것과 유사한 효과를 낸다. 하지만 debounce와 같은 범용 유틸리티 함수를 만들 때는 어떤 상황에서도 this가 명확하게 유지되도록 apply를 사용하는 것이 더 정석적인 방법이다.
결론적으로 func.apply(this, args)는 debouncedSearch가 호출될 때의 this 문맥과 모든 파라미터(args)들을 온전히 보존했다가, delay 시간이 지난 후에 원래 함수인 searchAPI에게 그대로 전달하여 실행해주는 역할을 한다.
throttle은 이벤트를 일정한 주기마다 한 번씩만 실행하도록 강제하는 기법이다. 디바운스가 "실행을 계속 미루는 것"이라면, 쓰로틀은 "일정 시간마다 꾸준히 실행하지만 그 사이의 이벤트는 무시하는 것"이다.
throttle는 플래그 변수(inThrottle) 를 클로저에 두고, 함수가 호출됐을 때 이 플래그를 확인하여 실행 여부를 결정한다.
/**
* 쓰로틀 함수
* @param {Function} func - 쓰로틀을 적용할 함수
* @param {number} limit - 지연 시간 (밀리초)
* @returns {Function} - 쓰로틀이 적용된 새로운 함수
*/
const throttle = (func, limit) => {
// 쓰로틀 상태를 저장하는 플래그 변수
// true이면, 현재 쿨타임(지연 시간) 중임을 의미함
let inThrottle;
// 쓰로틀이 적용된 새로운 함수를 반환합니다.
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
};
// --- 사용 예시 ---
// 쓰로틀을 적용할 함수
const handleScroll = () => {
console.log('API 요청! - 스크롤 이벤트 처리 중...');
// 실제로는 여기에 무거운 작업(API 호출 등)이 들어갑니다.
};
// 1000ms(1초) 간격으로 실행되는 쓰로틀 함수 생성
const throttledScroll = throttle(handleScroll, 1000);
// window 객체에 스크롤 이벤트 리스너 추가
window.addEventListener('scroll', throttledScroll);
throttledScroll 함수가 계속해서 호출됨inThrottle은 false이므로 if (!inThrottle) 조건을 통과handleScroll() 함수가 즉시 실행inThrottle 변수가 true로 바뀌고, setTimeout으로 1초 뒤에 inThrottle을 false로 바꾸도록 예약을 건다.throttledScroll 함수는 계속 호출됨inThrottle이 true이므로 if 문을 통과하지 못하고 모든 호출이 무시됨setTimeout에 예약된 작업이 실행되어 inThrottle이 다시 false가 됨throttledScroll이 호출되면, inThrottle이 false이므로 다시 2번 과정부터 반복-> 결과적으로 handleScroll 함수는 사용자가 아무리 빠르게 스크롤을 해도 1초에 최대 한 번만 실행되므로 성능을 효과적으로 제어할 수 있다.