Throttle 와 Debounce 개념 정리하기
Throttle 와 Debounce 라는 개념 을 알게 되어 이를 정리해보고자 한다.
Throttle 와 Debounce 는 자주 사용 되는 이벤트나 함수 들의 실행되는 빈도를 줄여서, 성능 상의 유리함을 가져오기 위한 개념이다.
자주 사용되는 간단한 예로는 자동 완성이 있다.
keyboard
가 한자씩 입력될 때마다, api 로 데이터를 가져오게 되면, 사용자의 의도와 무관한 요청 이 자주 발생되는데, 이를 줄이기 위해, 입력이 끝난후나, 입력되는 중간 중간 200ms 마다 api 값을 가져온다면, 성능에서 매우 유리해 진다.
Throttle 는 입력 주기를 방해하지 않고, 일정 시간 동안의 입력을 모와서, 한번씩 출력을 제한한다.
Debounce 는 입력 주기가 끝나면, 출력한다.
위 예제에서 Throttle 는 200ms 마다 api 값을 가져오는 것이고, Debounce 는 입력이 끝난후 api 값을 가져오는 것이다.
간단 예제
// html
<input id="search" type="search" name="search" value="" />
input 에 keyup event listener 를 등록한다.
var debounce = null;
var throttle = null;function keyUpFn(el) {
// normal
console.log('normal', el.target.value, new Date().getTime()); // debounce
clearTimeout(debounce);
debounce = setTimeout(() => {
console.log(
'debounce',
el.target.value, new Date().getTime());
}, 500); // throttle
if(!throttle) {
setTimeout(() => {
console.log('throttle', throttle, new Date().getTime());
throttle = null;
}, 500);
}
throttle = el.target.value;
}document.getElementById("search").addEventListener('keyup', keyUpFn);// normal h 1583658583270
// normal he 1583658583582
// normal hel 1583658583878
// normal hell 1583658584182
// throttle hell 1583658584271
// 입력이 끝남
// normal hello 1583658584534
// debounce hello 1583658585036
// throttle hello 1583658585536
위 코드의 과정을 살펴보면,
Input Search Element
에Keyup Event Listener
를 등록한다.- normal 에서는, 키보드입력 발생하면, 그 즉시 value 를 출력한다.
- throttle 에서는 키보드입력 발생하면, 500ms 후에, 가장 최신 value 를 출력하고, 초기화 하여, 키보드 입력이 끝날때까지 반복한다.
- debounce 에서는 키보드 입력이 발생하면, 500ms 동안 기다리다, 그 안에 키보드 입력이 발생하면, 시간을 초기화 하고 다시 기다리다, 가장 최신 value 를 출력한다.
Throttle
Throttle 는 여러번 발생하는 이벤트를 일정 시간 동안, 한번만 실행 되도록 만드는 개념이다.
위 예제에서 500ms 동안 이벤트 실행을 막고, 한번만 실행 때문에, 잦은 이벤트 발생을 막아 성능상의 유리함을 가져 올 수 있다.
Debounce 와 다른점은 이벤트 발생 시간 이후에 일정 시간 동안 만을 기다리고, 이벤트를 실행 후 재차 기다린 다는 점이다.
Debounce
Debounce 는 여러번 발생하는 이벤트에서, 가장 마지막 이벤트 만을 실행 되도록 만드는 개념이다.
위 예제에서 입력이 끝난 500ms 동안 동일한 이벤트가 계속해서 발생 한다면, 입력이 끝날때, 가장 마지막 이벤트만을 실행하여, 성능성 유리함을 가져올 수 있다.
Throttle 와 다른점은, 마지막 이벤트에서 일정 시간동안 이벤트가 발생한다면, 또 일정 시간을 기다린다는 점이다.
Throttle 와 Debounce 차이점
Throttle 와 Debounce 의 차이점은 이벤트를 언제 발생 시킬지의 시점 차이이다.
Debounce 는 입력이 끝날때까지 무한적으로 기다리지만, Throttle 는 입력이 시작되면, 일정 주기로 계속 실행한다.
Debounce 의 시간을 짧게 가져간다면, Throttle 와 비슷한 효과가 날 수 있지만, 그럼에도 시점에서 차이가 날 수 있다.
때문에, 작업물의 성격에 따라 사용방법이 달라질 수 있다.
대표적인 예로 자동완성 만들 경우,
일정 주기로 자동으로 완성되는 리스트를 보여주는 것에는
사용자 측면에서 Throttle (검색 되는 경험) 가 유리할 수 있지만,
성능상에서는 Debounce (1번만 호출) 가 훨씬 유리할 수 있다.
throttle-debounce
위 라이브러리를 이용하면, 매우 쉽게 throttle 와 debounce 개념을 사용할 수 있다.
throttle-debounce 에서 throttle
https://github.com/niksy/throttle-debounce/blob/master/throttle.js
document.getElementById(“search”)
.addEventListener(‘keyup’,
throttle(300,(e) => {
console.log(e.target.value);
})
);
throttle 에 delayTime
과 callback function
을 넘겨주면, keyup 이벤트가 일어날때 마다, throttle 로 반환된 funtion 을 실행하게 되는데, 이때 delayTime
마다, callback function
을 호출하게 된다.
내부 코드를 살펴보면, 내부에 setTimeout
의 ID 을 가질 timeoutID
와 마지막 실행시간을 가질 lastExec
를 둔다.
그리고, 함수가 호출될 때마다 Date.now
와 lastExec
를 비교하여, delay 시간보다 크다면, callback (exec
) 을 실행하고 lastExec
을 현재 시간으로 업데이트 한다.
function exec () {
lastExec = Date.now();
callback.apply(self, args);
}
만약 아니라면 timeoutID
를 clearTimeout
로 초기화하고, 다시 setTimeout
을 세팅한다.
timeoutID = setTimeout(
debounceMode ? clear : exec,
debounceMode === undefined ?
delay — elapsed :
delay
);
입력이 계속 될때, 일정 시간을 기준으로 한번 실행할 수 있다.
throttle-debounce 에서 decounce
https://github.com/niksy/throttle-debounce/blob/master/debounce.js
document.getElementById("search")
.addEventListener('keyup',
debounce(300, (e) => {
console.log(e.target.value);
})
);`
debounce 에도 같은 형태로 쓸 수 있다.
내부 코드를 살펴보면, throttle 를 재사용한 것을 볼 수 있다.
function ( delay, atBegin, callback ) {
return callback === undefined ?
throttle(delay, atBegin, false) :
throttle(delay, callback, atBegin !== false);
}
throttle 와 차이점은 Date.now 와 lastExec 을 비교하지 않고, 계속 timeoutID 를 업데이트 하여, 입력이 끝나면, 일정 시간후 실행한다.
lodash
lodash 에서도 throttle 와 debounce 개념이 적용되어 있다.
lodash 에서 decounce
https://github.com/lodash/lodash/blob/master/debounce.js
import _ from ‘lodash’;document
.getElementById(“search”)
.addEventListener(‘keyup’,
_.debounce((e) => {
console.log(e.target.value);
}, 300)
);
decounce 에 callback function
과 delayTime
를 넘겨주면, keyup 이벤트
가 끝난후 delayTime
이후에 callback function
을 호출하게 된다.
내부 코드를 살펴보면,
입력이 들어오면, 현재 시간과 마지막 호출 그리고, delayTime (wait)
을 비교하고, lastArgs, lastThis, lastCallTime
를 저장한다.
function shouldInvoke(time) {
const timeSinceLastCall = time — lastCallTime
const timeSinceLastInvoke = time — lastInvokeTime return (
lastCallTime === undefined ||
(timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) ||
(maxing && timeSinceLastInvoke >= maxWait)
)
}
timerId
가 세팅되지 않았다면, leadingEdge
를 호출하여, startTimer(timerExpired, wait)
로 timerId
를 세팅한다.
function leadingEdge(time) {
lastInvokeTime = time
timerId = startTimer(timerExpired, wait)
return leading ? invokeFunc(time) : result
}
wait
시간이 지나면 timerExpired
로 마지막 입력 시간 (lastCallTime)
을 비교하여, 만약 입력이 계속 들어오고 있다고 판단되면, timerId = startTimer(timerExpired, remainingWait(time))
반복한다.
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
timerId = startTimer(timerExpired, remainingWait(time))
}
만약 더이상 입력이 없고, 시간이 지났다면, trailingEdge(time)
를 호출하여, invokeFunc()
에서, func.apply(thisArg, args)
으로 callback 을 전달하게 한다.
function trailingEdge(time) {
timerId = undefined
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
lodash 에서 throttle
https://github.com/lodash/lodash/blob/master/throttle.js
throttle 에 callback function
과 delayTime
를 넘겨주면, delayTime
마다 callback function
을 호출하게 된다.
import _ from ‘lodash’;document
.getElementById(“search”)
.addEventListener(‘keyup’,
_.throttle((e) => {
console.log(e.target.value);
}, 300)
);
내부 코드를 살펴보면, debounce
를 재사용하고 있는 것을 볼 수 있다. 이때 위와 다른 점은 leading
와 maxWait
를 전달하는 것이다.
function throttle(func, wait, options) {
let leading = true
let trailing = true
return debounce(func, wait, {
leading,
trailing,
'maxWait': wait
})
}
시간을 체크할 때 shouldInvoke
에서 maxWait
을 확인하게 되고, startTimer
에서 remainingWait
로 일정 시간만큼 계속 이벤트 실행을 반복할 수 있게 남은 시간을 계산한다.
function remainingWait(time) {
const timeSinceLastCall = time — lastCallTime
const timeSinceLastInvoke = time — lastInvokeTime
const timeWaiting = wait — timeSinceLastCall return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
RxJS
rxjs 에서도 throttle 와 debounce 개념이 적용되어 있다.
RxJS 에서 decounce
https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/debounceTime.ts
import { fromEvent, interval } from ‘rxjs’;
import { debounce, debounceTime } from ‘rxjs/operators’;fromEvent(
document.getElementById("search"),
'keyup'
).pipe(
debounceTime(300)
// debounce(ev => interval(300))
).subscribe(e => console.log(e.target.value));
Observable 의 이벤트 가 발생되었을때, 이전 scheduler
를 취소하고, 다시 scheduler
를 적재하고, 일정 시간이 지나면 scheduler
를 통해, 다음 스트림으로 넘어간다.
RxJS 에서 throttle
https://github.com/ReactiveX/rxjs/blob/master/src/internal/operators/throttleTime.ts
import { fromEvent, interval } from ‘rxjs’;
import { throttle, throttleTime } from ‘rxjs/operators’;fromEvent(
document.getElementById("search"),
'keyup'
).pipe(
throttleTime(300)
// throttle(ev => interval(300))
).subscribe(e => console.log(e.target.value));
Observable 의 이벤트 가 발생되었을때, 진행중인 throttled 가 있으면, _trailingValue 를 업데이트 하고, 일정 시간이 지나면 scheduler 를 통해, 다음 스트림으로 넘어간다.
참고자료
아래링크는 lodash 문서 참조중 소개받은 글로 debounce 와 throttled 의 차이를 설명하는 글이다.