React 18 Automatic Batching 도입 후기

배경

프로젝트를 React 17에서 18로 마이그레이션했다. 주요 목적은 Concurrent Features 준비였지만, Automatic Batching이 생각보다 큰 영향을 미쳤다.

기존 코드의 문제

17에서는 이벤트 핸들러 외부의 상태 업데이트가 배칭되지 않았다.

const handleSubmit = async () => {
  setLoading(true);
  const data = await fetchData();
  setData(data);
  setLoading(false);
  // React 17: 3번 렌더링
  // React 18: 1번 렌더링
};

비동기 함수 내에서 3번의 setState가 각각 렌더링을 트리거했다. 복잡한 컴포넌트에서는 불필요한 렌더링이 성능 이슈를 만들었다.

변경 사항

React 18의 자동 배칭으로 Promise, setTimeout, 네이티브 이벤트 핸들러 내부의 업데이트도 자동으로 묶인다.

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // 하나의 렌더링으로 처리됨
}, 1000);

주의점

의도적으로 각 업데이트마다 렌더링이 필요한 경우 flushSync를 사용해야 한다.

import { flushSync } from 'react-dom';

flushSync(() => {
  setCount(c => c + 1);
});
// DOM이 업데이트됨
setFlag(f => !f);

애니메이션이나 스크롤 위치 계산처럼 중간 상태의 DOM 측정이 필요한 경우에만 사용했다.

성능 개선

복잡한 대시보드 페이지에서 데이터 페칭 후 렌더링 횟수가 평균 5회에서 2회로 줄었다. React DevTools Profiler로 측정한 결과 초기 로딩 시간이 약 30% 개선됐다.

마이그레이션 자체는 createRoot 변경과 타입 업데이트 정도로 간단했고, 자동 배칭은 코드 수정 없이 적용됐다.