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

위 코드 과정을 살펴보면,

  1. Input Search ElementKeyup Event Listener 를 등록한다.
  2. normal 에서는, 키보드입력 발생하면, 그 즉시 value 를 출력한다.
  3. throttle 에서는 키보드입력 발생하면, 500ms 후에, 가장 최신 value 를 출력하고, 초기화 하여, 키보드 입력이 끝날때까지 반복한다.
  4. debounce 에서는 키보드 입력이 발생하면, 500ms 동안 기다리다, 그 안에 키보드 입력이 발생하면, 시간을 초기화 하고 다시 기다리다, 가장 최신 value 를 출력한다.

Throttle

Throttle 는 여러번 발생하는 이벤트를 일정 시간 동안, 한번만 실행 되도록 만드는 개념이다.

위 예제에서 500ms 동안 이벤트 실행을 막고, 한번만 실행 때문에, 잦은 이벤트 발생을 막아 성능상의 유리함을 가져 올 수 있다.

Debounce 와 다른점은 이벤트 발생 시간 이후에 일정 시간 동안 만을 기다리고, 이벤트를 실행 후 재차 기다린 다는 점이다.

Debounce

Debounce 는 여러번 발생하는 이벤트에서, 가장 마지막 이벤트 만을 실행 되도록 만드는 개념이다.

위 예제에서 입력이 끝난 500ms 동안 동일한 이벤트가 계속해서 발생 한다면, 입력이 끝날때, 가장 마지막 이벤트만을 실행하여, 성능성 유리함을 가져올 수 있다.

Throttle 와 다른점은, 마지막 이벤트에서 일정 시간동안 이벤트가 발생한다면, 또 일정 시간을 기다린다는 점이다.

Throttle 와 Debounce 차이점

ThrottleDebounce 의 차이점은 이벤트를 언제 발생 시킬지의 시점 차이이다.

Debounce 는 입력이 끝날때까지 무한적으로 기다리지만, Throttle 는 입력이 시작되면, 일정 주기로 계속 실행한다.

Debounce 의 시간을 짧게 가져간다면, Throttle 와 비슷한 효과가 날 수 있지만, 그럼에도 시점에서 차이가 날 수 있다.

때문에, 작업물의 성격에 따라 사용방법이 달라질 수 있다.

대표적인 예로 자동완성 만들 경우,
일정 주기로 자동으로 완성되는 리스트를 보여주는 것에는
사용자 측면에서 Throttle (검색 되는 경험) 가 유리할 수 있지만,
성능상에서는 Debounce (1번만 호출) 가 훨씬 유리할 수 있다.

throttle-debounce

위 라이브러리를 이용하면, 매우 쉽게 throttle 와 debounce 개념을 사용할 수 있다.

https://github.com/niksy/throttle-debounce/blob/master/throttle.js

document.getElementById(“search”)
.addEventListener(‘keyup’,
throttle(300,(e) => {
console.log(e.target.value);
})

);

throttle 에 delayTimecallback function 을 넘겨주면, keyup 이벤트가 일어날때 마다, throttle 로 반환된 funtion 을 실행하게 되는데, 이때 delayTime 마다, callback function 을 호출하게 된다.

내부 코드를 살펴보면, 내부에 setTimeout 의 ID 을 가질 timeoutID 와 마지막 실행시간을 가질 lastExec 를 둔다.

그리고, 함수가 호출될 때마다 Date.nowlastExec 를 비교하여, delay 시간보다 크다면, callback (exec) 을 실행하고 lastExec 을 현재 시간으로 업데이트 한다.

function exec () {
lastExec = Date.now();
callback.apply(self, args);
}

만약 아니라면 timeoutIDclearTimeout 로 초기화하고, 다시 setTimeout 을 세팅한다.

timeoutID = setTimeout(
debounceMode ? clear : exec,
debounceMode === undefined ?
delay — elapsed :
delay
);

입력이 계속 될때, 일정 시간을 기준으로 한번 실행할 수 있다.

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 개념이 적용되어 있다.

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 functiondelayTime 를 넘겨주면, 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
}

https://github.com/lodash/lodash/blob/master/throttle.js

throttlecallback functiondelayTime 를 넘겨주면, delayTime 마다 callback function 을 호출하게 된다.

import _ from ‘lodash’;document
.getElementById(“search”)
.addEventListener(‘keyup’,
_.throttle((e) => {
console.log(e.target.value);
}, 300)

);

내부 코드를 살펴보면, debounce 를 재사용하고 있는 것을 볼 수 있다. 이때 위와 다른 점은 leadingmaxWait 를 전달하는 것이다.

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 개념이 적용되어 있다.

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 의 차이를 설명하는 글이다.

Javascript is great We may not be great

Javascript is great We may not be great