debounce는 특정 이벤트가 연속해서 발생할 때, 마지막 이벤트가 실행된 후 일정 시간이 지나야 함수를 실행하도록 하는 기법이다.
예를 들어, 검색창에 "디바운스"를 입력할 때 ㄷ ㅣ ㅂ ㅏ ㅇ ㅜ ㄴ ㅅ ㅡ 마다 이벤트 핸들러가 호출되지 않도록 조절할 수 있다. 사용자가 입력을 멈추고 일정 시간(예: 500ms)이 지나면 이벤트 핸들러가 실행되도록 설정하여 불필요한 호출을 방지할 수 있다.
이번 프로젝트에서는 독서 모임을 검색할 때, 사용자가 입력할 때마다 API를 호출하도록 구현했다. 하지만 과도한 API 호출은 비용과 성능에 부정적인 영향을 줄 수 있기 때문에 debounce를 적용했다.
debounce는 직접 구현할 수도 있지만, 이번에는 lodash의 debounce 라이브러리를 활용하여 간편하게 적용했다. 이를 통해 사용자가 입력을 멈춘 후 일정 시간이 지나야 API 요청이 발생하도록 최적화했다.
import { ChangeEvent, useCallback } from 'react';
import Input from '../Input';
import SearchIcon from '../../../../public/icons/SearchIcon';
import { debounce } from 'lodash';
interface SearchInputProps {
value: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}
function SearchInput({ value, onChange }: SearchInputProps) {
const debouncedOnChange = useCallback(debounce(onChange, 300), [onChange]);
return (
<div className="bg-white">
<Input
value={value}
onChange={debouncedOnChange}
placeholder={'검색어를 입력해주세요'}
icon={<SearchIcon />}
/>
</div>
);
}
export default SearchInput;
근데 여기서 eslint 에러가 발생했다.
debounce 함수는 실행될 때마다 새로운 타이머를 설정하게 되는데, 이때 이전 타이머가 초기화 된다. 결국 입력할 때마다 검색 요청이 계속 실행돼서, 컴포넌트가 리렌더링될 때마다 새로 생성되지 않도록 useCallback, useMemo 등 메모이제이션 해야한다. 최초 코드에는 useCallback으로 적용했으나 다음과 같은 에러가 발생했다.
lint 에러 메세지
React Hook useCallback received a function whose dependencies are unknown. Pass an inline function instead.
해결 참고 블로그: https://kyleshevlin.com/debounce-and-throttle-callbacks-with-react-hooks/
이 블로그에 따르면
"useCallback은 인라인 함수를 기대합니다. debounce나 throttle이 반환하는 함수를 전달하는 것은 적절하지 않습니다. 왜냐하면 ESLint 규칙이 어떤 의존성을 포함해야 하는지 정확히 파악할 수 없기 때문입니다. useMemo를 사용해서 해결해라"
import { ChangeEvent, useMemo } from 'react';
import Input from '../Input';
import SearchIcon from '../../../../public/icons/SearchIcon';
import { debounce } from 'lodash';
interface SearchInputProps {
value: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
}
function SearchInput({ value, onChange }: SearchInputProps) {
const debouncedOnChange = useMemo(
() =>
debounce((e: ChangeEvent<HTMLInputElement>) => {
onChange(e);
}, 300),
[onChange]
);
return (
<div className="bg-white">
<Input
value={value}
onChange={debouncedOnChange}
placeholder={'검색어를 입력해주세요'}
icon={<SearchIcon />}
/>
</div>
);
}
export default SearchInput;