React 19 Compiler와 함께 사용하는 useMemo 최적화 전략

배경

프로젝트에 React 19 Compiler를 적용하면서 기존에 작성했던 useMemo 훅들을 어떻게 관리할지 고민이 생겼다. 컴파일러가 자동으로 최적화를 해준다는 점은 알고 있었지만, 실제로 어느 정도까지 믿고 코드를 수정해야 할지 판단이 필요했다.

테스트 결과

몇 가지 패턴으로 테스트를 진행했다.

1. 단순 계산

// Before
const filteredList = useMemo(() => 
  items.filter(item => item.active),
  [items]
);

// After - Compiler가 알아서 처리
const filteredList = items.filter(item => item.active);

단순 필터링이나 맵핑의 경우 컴파일러가 충분히 최적화했다. 불필요한 재계산이 발생하지 않았다.

2. 무거운 연산

// 여전히 useMemo 유지
const complexData = useMemo(() => {
  return items.reduce((acc, item) => {
    // 복잡한 변환 로직
    return processHeavyCalculation(acc, item);
  }, initialValue);
}, [items]);

수십만 건의 데이터를 처리하는 무거운 연산은 명시적으로 useMemo를 유지했다. 컴파일러 최적화에도 한계가 있었고, 성능 프로파일링 결과 명시적 메모이제이션이 더 나았다.

3. 객체 참조 동일성

// useEffect 의존성 때문에 필요
const config = useMemo(() => ({
  api: apiEndpoint,
  timeout: 3000,
  retries: 3
}), [apiEndpoint]);

useEffect(() => {
  fetchData(config);
}, [config]);

객체 참조를 의존성으로 사용하는 경우는 여전히 useMemo가 필요했다. 컴파일러가 이 부분까지는 완벽하게 처리하지 못했다.

결론

React 19 Compiler를 믿되, 프로파일링을 통해 검증하는 방식으로 접근했다. 단순 연산은 컴파일러에 맡기고, 무거운 연산이나 참조 동일성이 중요한 경우는 명시적으로 최적화를 유지하는 것이 현실적이었다.

팀 내부적으로는 "일단 제거하고 성능 이슈가 발견되면 다시 추가"하는 방향으로 가이드를 정했다.