들어가며
이전에 배웠던 클로저(하단 링크 첨부 참조)가 디바운스에는 어떻게 활용되는지 알아보자.
closure디바운스란?
- Debounce는 계속해서 어떤 요청이 들어올 때, 모든 요청을 처리하지 않고, 마지막 요청에 대해서만 응답을 하는 기법을 말한다.
- 모든 요청에 대응하지 않아도 된다는 점에서, 성능 향상을 노릴 수 있다.
- 예를 들어, input 창에 사용자가 글자를 입력할 때마다 함수를 호출하는 것이 아니라, 일정 시간이 지난 후에 함수를 호출 한다.
- 참고) throttle은 게임에서 스킬쓰면 쿨타임 돌듯이 한번 함수가 호출되면 일정 시간동안은 다시 호출하지 않습니다.
구현
export const debounce = (fn: Function, ms = 300) => { let timeout: ReturnType<typeof setTimeout>; return function (this: any, ...args: any[]) { clearTimeout(timeout); timeout = setTimeout(() => fn.apply(this, args), ms); }; };
function debounce(func, wait) { let timeout; return function() { const context = this; const args = arguments; clearTimeout(timeout); timeout = setTimeout(() => func.apply(context, arguments), wait); }; }
- 일정 시간이 지나면 함수를 반환하는 게 핵심!
- timeout은 callback, 즉, 사용자가 지정한 특정 행동이 계속 들어오는지 지켜보는
관찰자
역할을 하는 변수이다.
- debounce는 함수를 반환한다. debounce가 반환한 함수를 호출할 때 있던 매개변수들은,
…args
를 통하여 받을 수 있다.
- setTimeout의 반환하는
timeoutID
는 양수로서setTimeout()
이 생성한 타이머를 식별할 때 사용합니다. 이 값을clearTimeout()
에 전달하면 타이머를 취소할 수 있습니다.
사용 예시
this.$target.addEventListener( "keydown", debounce(async (e: InputEvent) => { const { value } = e.target as HTMLInputElement; if (value.trim()) { const suggestionList = await this.getSuggestionList(value); this.suggestionListComp?.setState({ suggestionList, }); } else { this.suggestionListComp?.hide(); } }) );
어디서 클로저가 사용될까?
- 클로저는 상위 스코프를 기억한다!
- 상위 스코프에 정의한 timeout을 하위 스코프가 기억을 하고 있기 때문에 timeout을 clear할 수 있다.
- 만약 clearTimeout을 사용하지 못할 경우 디바운스가 원활히 일어나지 않는다.


클로저가 만능은 아니다!
편리한 기능이지만 단점이 있기때문에 매번 사용하기 보단 구현에 있어 필요할 때를 구분해 사용하면 좋다. 단점은 크게 2가지로 볼 수 있다.
- 메모리를 소모한다.
- 리턴하거나 timer, 콜백 등으로 등록했던 함수들이 메모리에 계속 남아있게 되면 해당하는 closure도 같이 메모리에 계속 남아있게 되는 것이기 때문에, 지속적으로 루프를 돌면서 closure 생성하는 것은 지양해야할 설계가 될지도 모른다.
- 최신 버전에서는 해결되었지만, 구 버전의 IE 같은 경우에는 DOM의 콜백함수로 등록을 하고 콜백함수의 해제 없이 바로 DOM을 삭제해버리면 메모리 누수가 생기는 단점도 있었던 점만 봐도 closure의 메모리 누수와 누적에 대한 고민을 해야한다는 것을 깨달을 수 있다
- Scope 생성에 따른 퍼포먼스 손해가 있다.
- 클로저는 그 특성상 폐쇄된 스코프 안의 변수가 스코프 종료와 동시에 회수되지 못한다.
- 스코프 밖에서 언제든지 그 변수를 호출할 가능성이 있기 때문에 자바스크립트는 그 변수를 메모리에 계속해서 저장하게 된다.
- 즉, 메모리 효율적인 개발 방식은 아니다. 수거되지 않는 특성 때문이다.
- 따라서 꼭 필요한 경우에만 사용하는 것이 바람직하다.
Closure가 필요없음을 전달하는 방법은 간단하다. Closure를 담고있는 객체를 다른 값으로 초기화 시키면 더 이상 root에서 참조되지 않는 Closure를 가비지 컬렉터가 메모리 해제하게 된다.
function makeClosureFunc(hi) { const name = "peter"; return function () { console.log(`${hi} ${name}~!`); }; } const sayHiToPeter = makeClosureFunc("Good morning!"); sayHiToPeter(); //Good morning! peter~! // ~ 함수의 필요성이 완료됨 ~ sayHiToPeter = null; // 가비지 컬렉터가 이전 할당 값인 Closure의 reachable이 false 된 것을 확인하고 메모리에서 해제 함
추가로 재사용성이 없는 객체라면 즉시 실행함수를 통해서도 private data를 구현 할 수 있다.
참고자료: