대표적으로 검색어 자동완성 기능에 쓰인다.
검색창에 타이핑을 할때마다 API와 통신해 검색어를 불러온다면 너무 많은 트래픽이 발생할 수 있다.
사용자가 타이핑을 잠깐 중단할때, 그러니까 300ms 정도 타이핑이 중단 되었을때 API에 요청을 보내 자동완성검색어는 받아오도록 하는 것이다.
const debounce = <T extends (...args: any[]) => any>(
func: T,
delay: number,
leading = false,
) => {
let timer: ReturnType<typeof setTimeout> g | null;
return (...args: Parameters<T>) => {
if (leading && !timer) {
func(...args);
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!leading) func(...args);
}, delay);
};
};
클로저 함수로 timer 참조해 계속해서 업데이트하는 방식으로 동작한다.
타이핑을 할 때마다<input> 태그에 onChange 이벤트가 발생한다고 가정해본다면,
첫 글자를 입력하고 300ms 뒤에 func를 실행시키는 것이다. 그런데 중요한 점은 300ms라는 간격에 다시 타이핑(onChange)이벤트가 발생했다면 마지막 타이핑부터 다시 300ms 뒤에 실행되도록 미뤄진다.
이런식으로 계속 func의 실행시점을 지연시키는 원리이다.
SNS에서 자주 볼 수 있는 무한스크롤은 보통 HTML의 특정 엘리먼트를 타겟을 두고 그 엘리먼트가 화면에 잡히는지를 조건으로 이벤트를 발생시킨다. 이 엘리먼트는 보통 스크롤 가장 끝에 위치시켜 스크롤이 다 내려갔다면 이벤트를 발생시켜 API로부터 다음 페이지를 불러온다.
우리는 다음 페이지를 딱 한 번만 불러오면 된다.
하지만 여기서 발생하는 문제가 스크롤이 끝에 다다른 그 짧은 시간에 수천 수만번씩 요청이 발생할 수 있다는 것이다. 그래서 쓰로틀이라는 개념이 필요하다.(사실 Intersection Observer API를 쓰면 이런 오작동을 고려하지 않아도 된다. 심지어 비동기로 실행되어 메인스레드에도 영향이 없다고 한다.)
쓰로틀은 설정한 시간동안 딱 한번씩만 함수를 실행하게 하는 장치이다.
const throttle = <T extends (...args: any[]) => any>(
func: T,
delay: number,
) => {
let waiting = false;
return (...args: Parameters<T>) => {
if (!waiting) {
waiting = true;
func(...args);
setTimeout(() => {
waiting = false;
}, delay);
}
};
};
func를 실행하게 되면 wait를 300ms 동안 true로 만들고 func를 실행할 수 없게 만든다.
300ms가 지나면 wait를 다시 false로 바꿔 func가 실행될 수 있도록 풀어준다.
React에서 컴포넌트 안에 사용할 때는 주의할 점이 있다.
const App = () => {
const [value, setValue] = useState("");
// useCallback으로 캐싱해야 한다.
const throttleConsoleLog = useCallback(
throttle((text: string) => console.log(text), 1000),
[],
);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
throttleConsoleLog("throttle");
};
return (
<div className="flex h-full w-full flex-col">
<input
className="border"
placeholder="throttle test"
value={value}
onChange={onChange}
/>
</div>
);
};
리렌더링이 일어나 함수 객체가 새로 생성되면 waiting의 참조를 잃어버리는 문제가 있는 것 같다.
debounce와 throttle 함수를 useCallback 등으로 캐싱하여 해결하였다.
리액트에서 custom hooks의 형태로 바꾸어 사용하는 것도 세련된 방법 같다.