debounce는 성능을 위해 이벤트를 제어하는 프로그래밍 기법으로 throttle과 묶여 설명이 되곤 합니다.
scroll, resize 이벤트 등과 같이 짧은 시간 간격으로 연속해서 이벤트가 발생하면 성능상 문제가 발생할 수 있습니다.
debounce는 짧은 시간 간격으로 이벤트가 연속해서 발생할 때 일정 시간이 경과한 후 이벤트 핸들러가 한 번만 호출되도록 하는 방법입니다.
throttle
- 짧은 시간 간격으로 이벤트가 연속해서 발생하더라도 일정 시간 간격으로 이벤트 핸들러가 최대 한 번만 호출하여 성능 최적화를 꾀함
- throttle은 특정 시간 주기로 이벤트 실행을 보장하나 debounce는 아무리 많은 이벤트가 발생해도 모두 무시하고 특정 시간동안 이벤트가 발생하지 않았을 때 딱 한 번만 마지막 이벤트를 실행
delay
시간 내 새로운 이벤트가 발생하지 않는지 대기(setTimeout
)callback()
)clearTimeout
)/**
* debounce
* @param callback - 지연 시간 후 실행할 함수
* @param delay - 지연 시간
* @returns typeof callback
*
*/
export const debounce = <T = any>(
callback: (args: T) => void,
delay = 500,
): typeof callback => {
let timer: NodeJS.Timeout | null;
return (args) => {
// 이 전 timer를 clear
if (timer !== null) {
clearTimeout(timer);
}
// delay 시간이 지나면 callback 실행
timer = setTimeout(() => {
// timer가 종료되면 null로 초기화
timer = null;
callback(args);
}, delay);
};
};
검색 기능에 debounce를 적용해봅니다.
const Search = () => {
const { register, handleSubmit } = useForm<SearchForm>();
const onSubmit: SubmitHandler<SearchForm> = (data) => {
const { searchKeyword } = data;
// 검색 결과 페이지로 이동
void router.push(`/search/${searchKeyword}`);
};
// onChange 이벤트 발생 시 debounce 적용
const handleChangeSearchKeyword: ChangeEventHandler<SearchForm> = debounce(handleSubmit(onSubmit));
return (
<Form onSubmit={handleSubmit(onSubmit)}>
<Label htmlFor="keyword">
<button type="submit">검색</button>
</Label>
<Input
{...register('keyword', {
required: true,
setValueAs: (value: string) => value.trim(),
onChange: handleChangeSearchKeyword,
})}
type="search"
id="keyword"
placeholder="검색어를 입력하세요."
/>
</Form>
)
}
안타깝게도 위 코드는 제가 의도한대로 실행되지 않습니다.
input에 검색어를 입력 중에 handleSubmit(onSubmit)
가 실행되어 delay
를 지정한 것이 무색하게 입력이 끝나는 것을 기다리지 않습니다.
그 이유는 함수형 컴포넌트의 경우 컴포넌트가 리렌더링 될 때 함수가 재정의 되기 때문입니다.
검색어 입력에 따라 컴포넌트가 리렌더링 되면서 매번 새로운 debounce
(handleChangeSearchKeyword
)가 정의되어 호출되므로 이 전에 대한 debounce
에 대한 참조를 잃게 되기 때문입니다.
따라서, handleChangeSearchKeyword
가 재정의 되지 않도록 useCallback
을 이용하여 적용합니다.
// useCallback 적용
const handleChangeSearchKeyword: ChangeEventHandler<SearchForm> = useCallback(
debounce(handleSubmit(onSubmit)),
[],
);
debounce
사용 시 항상 useCallback
으로 감싸서 사용하는 것은 불편하므로 커스텀 훅을 생성하여 조금 더 편하게 적용해봅니다.
import { useCallback, useRef } from 'react';
export const useDebounce = <T = any>(
callback: (args: T) => void,
delay = 500,
) => {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
// useCallback으로 감싸서 반환하여 사용 시 바로 debounce를 적용
return useCallback(
(args: T) => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
timeoutRef.current = null;
callback(args);
}, delay);
},
[callback, delay],
);
};
const handleChangeSearchKeyword: ChangeEventHandler<SearchForm> = useDebounce(
handleSubmit(onSubmit),
);
요렇게도 할 수 있다.
import { useCallback, useRef } from 'react';
export const useDebounce = <T extends (args: any) => any>(
callback: T,
delay = 500,
) => {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
return useCallback(
(args: Parameters<T>) => {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
timeoutRef.current = null;
callback(args);
}, delay);
},
[callback, delay],
);
};
const handleChangeSearchKeyword = useDebounce<ChangeEventHandler<SearchForm>>(
handleSubmit(onSubmit),
);
참고