React 18 beta의 Automatic Batching 동작 확인

배경

React 18 beta가 공개되면서 주요 변경사항 중 하나인 automatic batching을 실제로 테스트해볼 필요가 있었다. 기존 프로젝트에 영향을 줄 수 있는 부분이라 미리 파악하고자 했다.

기존 동작 (React 17)

React 17까지는 이벤트 핸들러 내부에서만 배칭이 동작했다. setTimeout이나 Promise 내부에서는 각 setState마다 리렌더링이 발생했다.

function Counter() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    // 배칭됨 - 1번 렌더
    setCount(c => c + 1);
    setFlag(f => !f);

    setTimeout(() => {
      // 배칭 안됨 - 2번 렌더 (React 17)
      setCount(c => c + 1);
      setFlag(f => !f);
    }, 1000);
  }
}

React 18의 변경점

React 18에서는 모든 상황에서 자동으로 배칭이 적용된다. createRoot를 사용하면 활성화된다.

import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'));
root.render(<App />);

이제 setTimeout, Promise, native event handler 등 어디서든 배칭이 동작한다.

테스트 결과

실제로 렌더링 횟수를 측정해봤다.

function TestComponent() {
  const [count, setCount] = useState(0);
  const renderCount = useRef(0);

  renderCount.current++;
  console.log('Render count:', renderCount.current);

  const handleAsyncUpdate = async () => {
    const data = await fetchData();
    setCount(data.count);     // React 17: 렌더
    setFlag(data.flag);        // React 17: 렌더
    // React 18: 1번만 렌더
  };
}

Promise 내부에서도 배칭이 적용되어 렌더링 횟수가 줄어드는 것을 확인했다.

배칭을 원하지 않는 경우

의도적으로 배칭을 막아야 한다면 flushSync를 사용할 수 있다.

import { flushSync } from 'react-dom';

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

결론

React 18의 automatic batching은 성능 개선에 도움이 되지만, 기존 코드에서 렌더링 타이밍에 의존하는 부분이 있다면 점검이 필요하다. 대부분의 경우 문제없이 동작했지만, flushSync의 존재를 알아두면 유용할 것 같다.

React 18 beta의 Automatic Batching 동작 확인