디바운서/스로틀링은 사용자의 연속되는 입력값에 대해 (예: 검색창의 텍스트 입력, 스크롤, 리사이즈, 마우스 동작 등) 일정시간씩 기다렸다가 입력값을 모아서 처리하는 것을 말한다.
디바운서와 스로틀링은 입력값을 처리하는 타이밍에서 차이가 있는데, 디바운서 같은 경우는 일정시간을 threshold로 두어 그 시간이내 입력이 없을때 모아서 처리하는 것이고, 스로틀링은 사용자의 입력 여부에 상관없이 정의해 놓은 시간간격마다 입력값을 받아 처리하는 것이다.
두가지 방식이 적용대상에 따라 장단점이 있겠지만, 일단 이번에 사용하게 된 검색창의 텍스트 입력을 받는 경우에는 사용자의 입력값이 없을때도 계속 체크하는 스로틀링 보다는 입력을 시작하면 기다리는 디바운서를 적용하는 것이 더 맞다고 생각 되었다.
(참고 자료 링크: https://medium.com/nerd-for-tech/debouncing-throttling-in-javascript-d36ace200cea)
다음은 내가 구현해 본 디바운서 모듈의 코드이다:
import React from "react";
export default class Debouncer {
actionCallback: (event: React.ChangeEvent<HTMLInputElement>) => void;
delayTime: number;
timeoutID: ReturnType<typeof window.setTimeout>;
constructor(actionCallback: (event: React.ChangeEvent<HTMLInputElement>) => void, delayTime: number) {
this.actionCallback = actionCallback;
this.delayTime = delayTime;
this.timeoutID = 0;
}
build() {
return (event: React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(this.timeoutID);
this.timeoutID = setTimeout(() => this.actionCallback(event), this.delayTime);
};
}
}
사용자의 입력이 시작되면 디바운서는 setTimeoutID를 비우고 setTimeout을 setTimeoutID 에 아이디를 배정하고 시작한다. 사용자의 그 다음 입력이 지정해 놓은 delayTime 보다 일찍 들어오면 위의 패턴이 반복되기에 다시 ID를 비우고 setTimeout을 다시 시작한다. 그러다 사용자 입력이 delayTime 이상 없을경우 setTimeout에 받아놨던 콜백함수를 실행한다. 이러한 디바운서 행위를 클래스화 함으로써 여러 컴포넌트에서 각기 원하는 액션을 할당하여 인스턴스를 생성하여 재사용 할 수 있게 하였다.
위 링크 글의 저자는 디바운서를 closure로 적은 줄의 코드로 잘 구현하였는데, 나는 내용을 소화할 겸 해서 클래스로 구현해 보았다. 클래스로 구현할 경우 장점은 내부의 build 메소드가 prototype으로 지정되기에 클로저로 생성할때보다 메모리를 아낄 수 있다는 점이다. 아쉬운 점은 타입스크립트를 적용한 클래스 작성이다 보니 constructor 의 인자로 받는 디바운서를 적용할 액션함수가 클래스 상단부에도 타입을 기술해야 하고 build 함수에도 기술해야 해서 좀 반복적인 느낌이 없지 않다는 점이다.
본 디바운서를 사용하는 예시는 다음과 같다:
function InputComponent (props) {
const [ searchString, setSearchString ] = useState('');
const handleInputOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchString(event.target.value);
};
const handleInputChangeWithDebouncer = new Debouncer(handleInputOnChange, DEBOUNCER_DELAY_TIME).build();
return <>
<input onChange={handleInputChangeWithDebouncer} type="text" id="searchString" />
<label htmlFor="searchString">{props.children}</label>
</>
}