React 컴포넌트에서 debounce 적용하기
문제 상황
검색창에 사용자가 입력할 때마다 자동완성 API를 호출하는 기능을 만들었다. 그런데 한 글자 입력할 때마다 요청이 나가면서 서버 부하와 불필요한 렌더링이 발생했다.
class SearchInput extends React.Component {
handleChange = (e) => {
const keyword = e.target.value;
this.props.fetchSuggestions(keyword); // 매번 호출됨
}
render() {
return <input onChange={this.handleChange} />;
}
}
첫 번째 시도와 실패
lodash의 debounce를 바로 적용했다.
import debounce from 'lodash/debounce';
handleChange = debounce((e) => {
this.props.fetchSuggestions(e.target.value);
}, 300);
그런데 입력값이 제대로 전달되지 않았다. React의 SyntheticEvent가 이벤트 풀링으로 재사용되면서 debounce 콜백 실행 시점에는 이미 이벤트 객체가 초기화된 상태였다.
해결 방법
이벤트 값을 먼저 추출한 뒤 debounce 함수에 전달했다.
class SearchInput extends React.Component {
debouncedFetch = debounce((keyword) => {
this.props.fetchSuggestions(keyword);
}, 300);
handleChange = (e) => {
const keyword = e.target.value;
this.debouncedFetch(keyword);
}
componentWillUnmount() {
this.debouncedFetch.cancel(); // 메모리 누수 방지
}
render() {
return <input onChange={this.handleChange} />;
}
}
API 호출이 300ms 간격으로 제한되면서 네트워크 요청이 크게 줄었다. componentWillUnmount에서 debounce를 취소하는 것도 중요했다. 컴포넌트가 unmount된 후 예약된 함수가 실행되면서 setState 경고가 발생하는 것을 방지할 수 있었다.
추가 고려사항
- debounce 시간은 300ms로 설정했지만, UX 테스트를 통해 조정 필요
- throttle과 debounce의 차이를 명확히 이해하고 사용해야 함
- 클래스 필드로 선언해야 인스턴스마다 독립적인 debounce 함수 유지