React 컴포넌트 리렌더링 최적화 삽질기

문제 상황

사용자 목록 페이지에서 검색 필터를 입력할 때마다 500개 이상의 아이템이 모두 리렌더링되고 있었다. Chrome DevTools Profiler로 확인해보니 한 글자 입력할 때마다 200ms 이상 소요되고 있었다.

function UserList({ users }) {
  const [filter, setFilter] = useState('');
  
  const filteredUsers = users.filter(user => 
    user.name.includes(filter)
  );
  
  return (
    <>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {filteredUsers.map(user => <UserItem key={user.id} user={user} />)}
    </>
  );
}

해결 과정

먼저 UserItem 컴포넌트를 React.memo로 감쌌다. 하지만 여전히 모든 아이템이 리렌더링되고 있었다. props 비교 로직을 추가로 확인해보니 부모에서 전달하는 콜백 함수가 매번 새로 생성되고 있었다.

const UserItem = React.memo(({ user, onDelete }) => {
  return (
    <div>
      {user.name}
      <button onClick={() => onDelete(user.id)}>삭제</button>
    </div>
  );
});

useCallback으로 콜백 함수를 메모이제이션하고, filter 로직도 useMemo로 감쌌다.

function UserList({ users }) {
  const [filter, setFilter] = useState('');
  
  const filteredUsers = useMemo(
    () => users.filter(user => user.name.includes(filter)),
    [users, filter]
  );
  
  const handleDelete = useCallback((userId) => {
    // 삭제 로직
  }, []);
  
  return (
    <>
      <input value={filter} onChange={e => setFilter(e.target.value)} />
      {filteredUsers.map(user => (
        <UserItem key={user.id} user={user} onDelete={handleDelete} />
      ))}
    </>
  );
}

결과

입력 지연이 20ms 이하로 줄어들었다. Profiler로 확인해보니 변경된 아이템만 리렌더링되고 있었다.

다만 모든 컴포넌트에 무작정 memo를 적용하는 것은 오버엔지니어링이라는 것도 알게 되었다. props 비교 비용도 있기 때문에 실제 성능 문제가 있는 부분에만 선택적으로 적용하는 것이 맞다.

React 컴포넌트 리렌더링 최적화 삽질기