함수 디바운스는 이벤트가 트리거된 후 n초 뒤에 콜백 함수를 실행하는 기법으로, 이 n초 내에 다시 트리거되면 타이머를 리셋합니다. 이는 다음과 같은 상황에서 성능 최적화를 위해 자주 사용됩니다:
- 폼 컴포넌트 입력 내용 검증
- 다중 클릭으로 인한 폼 중복 제출 방지
등의 경우에 함수가 너무 빈번하게 불필요하게 호출되는 것을 방지합니다.
기본 개념
setTimeout을 사용하여 타이머를 구현하고, clearTimeout을 통해 "타이머 리셋" 기능을 구현합니다.
즉, 이벤트가 트리거될 때마다 이전 타이머를 취소하고 새로운 타이머를 등록합니다. 트리거가 멈춘 후 wait 시간이 지나야만 콜백 함수가 실행됩니다.
이벤트가 계속 트리거되면 이 과정이 반복되어, 대상 함수가 너무 자주 호출되는 것을 방지하는 목적을 달성합니다.
기본 구현
function createDebounceHandler(targetFunction, delayTime) {
let timerId
return function () {
clearTimeout(timerId)
timerId = setTimeout(targetFunction, delayTime)
}
}
사용 예시
container.onmousemove = createDebounceHandler(processMovement, 1000);
클로저 관련 주석
이벤트가 트리거될 때마다 실행되는 것은 반환된 클로저 함수입니다.
클로저가 제공하는 스코프 체인에서 상위 함수 변수의 생명 주기를 연장시키는 효과 때문에, createDebounceHandler 함수의 setTimeout 타이머 ID timerId 변수는 createDebounceHandler 함수 실행이 끝난 후에도 메모리에 남아 클로저에서 계속 사용될 수 있습니다.
최적화: 문제 수정
디바운스 적용 전의
container.onmousemove = processMovement
와 비교했을 때, processMovement 함수의 this가 HTMLDivElement를 가리키던 것이 클로저 익명 함수의 this를 가리키게 되어 전역 변수를 가리키게 됩니다.
마찬가지로 processMovement 함수는 MouseEvent 이벤트 객체를 받지 못하게 됩니다.
수정된 코드
function createDebounceHandler(targetFunction, delayTime) {
let timerId
return function () {
let executionContext = this
clearTimeout(timerId)
timerId = setTimeout(
()=>{targetFunction.apply(executionContext, arguments)}
, delayTime)
}
}
최적화: 즉시 실행 기능
주기 내에서 마지막 트리거 후 일정 시간을 기다렸다가 대상 함수를 실행하는 대신,
주기 내에서 첫 트리거 시 즉시 실행하고, 일정 시간 동안 추가 실행을 방지하는 기능을 구현할 수 있습니다.
이렇게 하면 함수 호출 빈도를 제한하면서 사용자 대기 시간을 줄여 사용자 경험을 향상시킬 수 있습니다.
코드 구현
기존 코드에 즉시 실행 여부를 결정하는 매개변수를 추가합니다
function createDebounceHandler(targetFunction, delayTime, shouldRunImmediately) {
let timer
let debouncedFunction = function() {
let executionContext = this
if(timer) clearTimeout(timer)
if(shouldRunImmediately) {
let canRunNow = !timer
if(canRunNow) targetFunction.apply(executionContext, arguments)
timer = setTimeout(
()=>{timer = null}
, delayTime)
} else {
timer = setTimeout(
()=>{targetFunction.apply(executionContext, arguments)}
, delayTime)
}
}
return debouncedFunction
}
주석
타이머 ID를 저장하는 timer 값을 null로 설정하는 이유는 두 가지입니다:
- 주기 종료를 나타내는 스위치 변수 역할로,
canRunNow가 true가 되어 새로운 주기에서 대상 함수가 트리거될 때 실행될 수 있도록 합니다 timer는 클로저가 참조하는 상위 함수 변수로 자동으로 회수되지 않습니다. 수동으로 null로 설정하여 실행 환경에서 분리시켜 가비지 컬렉터가 다음 실행 시 회수하도록 합니다.
최적화: 실행 취소 기능
"실행 취소" 기능을 추가합니다.
함수도 객체이므로 속성을 추가할 수 있습니다.
실행 취소 기능을 추가하기 위해 debounced 함수에 cancel 속성을 추가하고, 이 속성 값으로 함수를 할당합니다
debouncedFunction.cancel = function() {
clearTimeout(timer)
timer = null
}
사용 예시
var configuredFunction = createDebounceHandler(processAction, 1000, true)
container.onmousemove = configuredFunction
document.getElementById("cancelButton").addEventListener('click', function(){
configuredFunction.cancel()
})
최종 코드
function createDebounceHandler(targetFunction, delayTime, shouldRunImmediately) {
let timer
let debouncedFunction = function() {
let executionContext = this
if(timer) clearTimeout(timer)
if(shouldRunImmediately) {
let canRunNow = !timer
if(canRunNow) targetFunction.apply(executionContext, arguments)
timer = setTimeout(
()=>{timer = null}
, delayTime)
} else {
timer = setTimeout(
()=>{targetFunction.apply(executionContext, arguments)}
, delayTime)
}
}
debouncedFunction.cancel = function() {
clearTimeout(timer)
timer = null
}
return debouncedFunction
}
Vue.js에서의 해결 방안
먼저 공통 함수 파일에서 debounce를 등록합니다
export function createDebounceHandler(targetFunction, delay) {
let timer
return function (...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
targetFunction.apply(this, args)
}, delay)
}
}
그런 다음 사용할 컴포넌트에서 debounce를 가져와 created 라이프사이클에서 호출합니다:
created() {
this.$watch('searchText', createDebounceHandler((newSearchText) => {
this.fetchData(newSearchText)
}, 200))
}
쓰로틀링:
함수가 한 번 실행된 후 설정된 실행 주기보다 큰 간격이 지나야만 두 번째 실행이 가능합니다
//쓰로틀링 함수
function createThrottleHandler(fn, delay) {
//이전 함수 실행 시간 기록
var lastExecution = 0;
return function () {
//현재 함수 실행 시간 기록
var currentTime = Date.now();
if (currentTime - lastExecution > delay) {
//this 컨텍스트 수정
fn.call(this);
lastExecution = currentTime;
}
}
}
document.onscroll = createThrottleHandler(function () {
console.log('스크롤 이벤트 발생: ' + Date.now())
}, 200)
디바운스:
빈번하게 함수를 호출해야 할 때, 성능 최적화를 위해 지정된 시간 내에서는 첫 번째 호출만 유효하게 하고 이후 호출은 무시합니다.
function createDebounceHandler(fn,delay) {
//이전 타이머 저장
var timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this);
}, delay);
}
}