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 함수 유지