디바운스(Debounce)는 짧은 간격으로 이어지는 호출을 한 묶음으로 보고, 호출이 멈춘 뒤 정해진 시간이 지나면 한 번만 실행하는 기법이다. 입력처럼 잦게 일어나는 이벤트에 무거운 작업(결과 필터링, 추천어 요청)이 붙을 때 쓴다.
동작
- 묶음의 첫 호출 시점을 leading edge, 마지막 호출 뒤 대기 시간이 지난 시점을 trailing edge라고 한다
- 보통 trailing edge에서 한 번 실행한다. 용도에 따라 leading edge나 양쪽 모두에서 실행하기도 한다
- 대기 시간 안에 새 호출이 오면 같은 묶음이 되고, 대기 시간은 그 호출부터 다시 잰다. 그래서 호출이 끊기지 않으면 실행도 계속 미뤄진다
스로틀(Throttle)과 다른 점
| 디바운스 | 스로틀 | |
|---|---|---|
| 호출이 계속될 때 | 멈출 때까지 기다린다 | 정해진 간격마다 최대 한 번 실행한다 |
| 보통 실행하는 때 | 마지막 호출 뒤(trailing edge) | 묶음의 첫 호출(leading edge) |
| 맞는 일 | 입력이 멈춘 뒤 검색·추천 | 스크롤하는 동안 위치 반영 |
스크롤 위치에 맞춰 다른 요소를 움직이는 일은 스크롤 도중에도 계속 반영돼야 하므로 스로틀이 맞다. 검색어 추천은 입력이 멈춘 뒤 한 번이면 되므로 디바운스가 맞다.
구현 예
debounce(fn, wait)는 fn을 감싼 새 함수를 돌려준다. 호출될 때마다 이전 예약을 취소하고 wait 뒤로 다시 예약해서, 마지막 호출만 실행된다(trailing edge).
function debounce<A extends unknown[]>(fn: (...args: A) => void, wait: number) {
let timer: ReturnType<typeof setTimeout> | undefined
return (...args: A) => {
clearTimeout(timer) // 이전 예약 취소
timer = setTimeout(() => fn(...args), wait) // 마지막 호출 기준으로 다시 예약
}
}
const search = debounce((q: string) => console.log('검색', q), 300)
search('r'); search('re'); search('rea') // 300ms 뒤 '검색 rea'만 한 번 찍힌다관련 개념
- React의 useDeferredValue는 시간이 아니라 우선순위로 렌더를 미룬다. 네트워크 요청 수를 줄이는 일은 여전히 디바운스의 몫이다
- HTMX의
delay수식어도 디바운스다 - 서버가 클라이언트별 요청 수를 제한하는 것은 요청 제한으로, 함수 호출을 줄이는 스로틀과는 다른 장치다
- 디바운스는 타이머를 쓴다. 해제하지 않은 타이머가 무엇을 붙잡아 두는지는 브라우저 JS 메모리 누수 원인