debounce와 throttle은 성능 저하 방지를 위한 방법들이다.
클로저라는 개념을 안다면 이해하기 쉽다.
IIFE(Immediately Invoked Function expression) 즉시 호출 함수를 통해 이벤트 발생 전에 이미 리턴된 함수가 이벤트 리스너에 콜백함수로 할당된다. 이벤트 리스너가 호출되기 전에 이미 리턴된 함수들이 계속해서 이벤트 발생으로 인해서 호출될 때마다 클로저 함수로서 이전의 timeId에 접근하는 컨셉이다.
1️⃣Debounce 동작 예시

- 이벤트를 그룹화하여 하나의 이벤트만 호출되도록 한다.
- 일정 시간(
waiting time) 함수 호출이 없는 경우 가장 마지막 이벤트를 선택한다.(trailing) - 이 대신에 가장 처음 호출된 이벤트를 선택하는 방법도 있다.(
leading)
debounce 구현
function debounce(callbackFn: Function, limit: number = 100) {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (...args: any[]) {
if (timer) {
clearTimeout(timer);
}
// @ts-ignore
console.log(this);
timer = setTimeout(
// @ts-ignore
() => callbackFn.apply(this, args),
limit,
);
};
}
debounce의 return 되는 함수를 익명 함수 표현식으로 하는 이유는 debounce의 콜백함수가 event listener로 사용됐을 때 return 함수내부의 this가 event.currentTarget에 바인딩 될 수 있게 하기 위함이다. 화살표 함수로 한다면 익명 함수가 선언됐을 때 이미 this가window 전역 객체에 고정이 되어 버리므로 이를 방지하기 위함이다.
debounce 실험할 html 코드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>debounce</title>
</head>
<body>
<button id="special">디바운스</button>
<script type="module">
import debounce from './debounce.js';
// const debounceEventHandler = debounce(() => console.log(this), 2000); // window {}
// const debounceEventHandler = debounce(function () {
// console.log(this);
// }, 2000); // <button id="special">디바운스</button>
// special.addEventListener('click', debounceEventHandler);
special.addEventListener(
'click',
debounce(function () {
console.log(this); // <button id="special">디바운스</button>
}, 1000),
);
</script>
</body>
</html>
debounce의 콜백 함수 자리에 오는 this도 화살표 함수냐 함수 표현식이느냐에 따라 달라진다. 하지만 this에 대해서 설명하기 위한 글이 아니므로 설명은 생략한다.
보통 this를 콘솔에 찍는 코드 보다는 무엇인가를 입력하는 동작에 많이 쓰인다.
2️⃣ throttle 동작 예시

- throttle은 일정 기간동안 연속적인 이벤트가 최대 한 번만 실행되게 한다.
- 이벤트가 꾸준히 발생하는 상황에서 일정
time interval간격으로 이벤트를 처리하고 싶다면 debounce 대신 throttle을 사용하면 된다.(예를 들어 화면 스크롤할 때 스크롤 위치를 저장해둘 경우. 근데 debounce로도 가능하긴 함. )
throttle 구현
function throttle(callbackFn: Function, delay: number = 1000) {
let timerId: number | null = null;
return function (...args: any[]) {
if (timerId) return;
timerId = window.setTimeout(() => {
// @ts-ignore
callbackFn.apply(this, args);
timerId = null;
}, delay);
};
}
쓰로틀에서는 이벤트 처리가 성공한 후 기다리는 상황(timerId = true)에서 불필요하게 또 setTimeout을 지정할 필요가 없다. 어차피 이벤트 처리가 불가능한 시간이기 때문이다. 이를 위해 if (timerId) return;코드가 위에 있는 것이다.
단, 이벤트 처리 직후(callbackFn.apply(this, args);) 곧바로 timerId = null;로 만들어서 이벤트 처리가 가능한 상태로 만들어줘야 한다. 그렇게 해야 조건문(if(timerId) return;)에 걸리지 않고 다음 이벤트를 수행할 수 있다.
throtte 실험할 html 코드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>throttle</title>
</head>
<body>
<button id="special">쓰로틀</button>
<script type="module">
import throttle from './throttle.js';
// const throttleEventHandler = throttle(() => console.log(this), 2000); // window {}
// const throttleEventHandler = throttle(function () {
// console.log(this);
// }, 2000); // <button id="special">쓰로틀</button>
special.addEventListener(
'click',
throttle(function () {
console.log(this); // <button id="special">쓰로틀</button>
}, 2000),
);
</script>
</body>
</html>
코드 재사용하기
그런데 코드를 보면 throttle과 debounce코드의 대부분이 겹치는 점을 알 수 있다.
throttle에서 debounce 코드를 재사용해보자.
debounce를 재사용하도록 할 것이고 그렇게 하기 위해서는 debounce로 동작할 것인지 throttle로 동작할 것인지 구분해줘야 한다.
function debounce(callbackFn: Function, limit: number = 100, isThrottle: boolean = false) {
let timer: ReturnType<typeof setTimeout> | null = null;
console.log('event handler가 계속 새로 생기는 지 확인 -> 새로 생김');
return function (...args: any[]) {
if (timer) {
if (isThrottle) return;
clearTimeout(timer);
}
// @ts-ignore
// console.log(this);
timer = setTimeout(() => {
// @ts-ignore
callbackFn.apply(this, args);
if (isThrottle) timer = null;
}, limit);
};
}
throttle에서는 이를 가져와서 isThrottle에 true를 줘서 throttle로 사용하면 된다.
function throttle(callbackFn: Function, delay: number = 1000) {
return debounce(callbackFn, delay, true);
}
이 글은 옵시디언(Obsidian)에서 작성되었습니다. 티스토리에서 상대경로 링크는 작동하지 않습니다. 큰 이미지 파일은 업로드되지 않을 수 있습니다.