디바운싱이란 무엇인가요?
디바운싱은 이벤트가 연속적으로 발생하는 것을 제어하여, 특정 시간 간격이 지난 후에 한 번만 실행되도록 만드는 기법입니다.
예를 들어, 입력 필드에서 사용자가 타이핑을 할 때마다 이벤트가 발생하는 것을 생각해보세요. 매번 API를 호출한다면 성능 문제가 발생할 수 있습니다. 디바운싱은 이런 문제를 해결합니다.
문제가 되는 코드 예시
searchInput.addEventListener('input', (e) => {
console.log(e.target.value)
})

입력할 때마다 console.log가 출력되는 것을 확인할 수 있습니다.
만약 유료 API를 사용하는 경우라면, 이렇게 많은 호출이 비용 낭비로 이어질 수 있습니다.
일반 디바운싱 구현
let timerId;
function debouncing(func, timeout) {
clearTimeout(timerId);
timerId = setTimeout(func, timeout);
}
첫 번째 호출: setTimeout으로 타이머를 시작합니다.
두 번째 호출: clearTimeout으로 기존 타이머를 취소하고 새 타이머를 설정합니다.
timerId를 전역으로 선언한 이유:
지역 변수로 선언하게 되면 timerId는 undefined로 계속 초기화되어 clearTimeout을 실행하지 못함
timeout 이후: func가 실행됩니다
바로 프로젝트에 적용해 봅시다.
const handleSearch = (e) => {
const search = e.target.value.trim();
search ? searchMovieList(search) : updateMovieList();
};
let timerId;
function debouncing(func, timeout) {
clearTimeout(timerId);
timerId = setTimeout(func(), timeout);
}
searchInput.addEventListener('input', debouncing(handleSearch, 300));

debouncing 함수에서 setTimeout에 전달된 func는 매개변수를 받아야 하는데, func를 호출할 때 e (이벤트 객체)가 전달되지 않아서 오류가 발생한 것입니다.
그렇다면 매개변수를 전달해 봅시다.
function debouncing(func, timeout) {
clearTimeout(timerId);
timerId = setTimeout(() => {
func(e);
}, timeout);
}


e은 Input 이벤트 객체로 searchInput 요소가 찍혀야 하는데 handleSearch의 이벤트 객체가 출력되는 것을 확인할 수 있습니다.
그렇다면 searchInput의 이벤트 객체가 찍힐 수 있도록 코드를 변경해봅시다.
function debouncing(func, timeout) {
clearTimeout(timerId);
timerId = setTimeout(func, timeout);
}
searchInput.addEventListener('input', (e) => debouncing(() => handleSearch(e), 300) );
왜 () => handleSearch(e)로 작성했을까?
- 이벤트 객체를 명시적으로 넘겨주기 위해서
input 이벤트 리스너에서 전달되는 e 객체를 handleSearch에 전달하기 위해 화살표 함수가 필요합니다. 만약 화살표 함수를 사용하지 않고 handleSearch만 전달하면, debouncing 내부에서 e 객체를 명시적으로 전달할 수 없게 됩니다. - 콜백 함수의 실행 지연 및 정확한 호출을 보장하기 위해서
setTimeout은 콜백 함수의 참조를 인자로 받습니다. 만약 handleSearch(e)처럼 작성하면 함수가 즉시 실행되고 반환값이 전달됩니다. 이를 방지하고, 원하는 시점에 실행되도록 콜백 형태로 전달하기 위해 화살표 함수가 사용되었습니다.
클로저를 활용한 디바운싱 구현
const debounce = (func, delay) => {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => func(...args), delay);
};
};
searchInput.addEventListener('input', debounce(handleSearch, 300));
이해하기 어렵다면 함수를 직접 옮겨서 해석하면 편합니다.
searchInput.addEventListener('input', (...args) => {
clearTimeout(timer);
timer = setTimeout(() => func(...args), 300);
});
...arg를 통해 searchInput의 이벤트 객체를 전달합니다.
첫 실행: timer가 null이므로 setTimeout이 실행되고 새 타이머가 설정됩니다.
이벤트 재발생 (타이머 실행 전): clearTimeout으로 기존 타이머를 취소한 후, 새로운 setTimeout을 실행하여 타이머를 다시 등록합니다.
타임아웃 후 실행: 지정된 시간 동안 추가 이벤트가 발생하지 않으면, setTimeout에 등록된 func가 실행됩니다.
이렇게 구현하면 timer를 은닉할 수 있기 때문에 클로저를 이용한 debouncing을 사용하는 것이 좋을 것 같습니다.