# React18 Auto Batching

React18 에서 더 많은 배칭을 통해 별도의 수동 배칭을 하지 않고도 성능 개선을 바로 누릴 수 있다.

이전에는 배칭이 어떻게 동작했는지, React 18에서 어떻게 개선됐는지 알아봅니다.

# What is Batching? (배칭이란?)

React가 더 나은성능을 위해, 여러개의 state 업데이트를 하나로 묶는다.

아래의 예시처럼, 하나의 클릭 이벤트 안에 두 개의 state 업데이트를 가지고 있다면, 이 작업을 배칭하여 하나의 리렌더링으로 만들었다.

다음과 같은 코드를 실행해보면, 매 번 누를 때마다, state를 두 번 변경하였지만, 한 번의 렌더링만 한 것을 볼 수 있다.

Demo

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

  function handleClick() {
    setCount(c => c + 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }

  console.log('render');

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

이 과정은 불필요한 연산(리렌더링)을 줄이기때문에 성능에 이득이 있다.

그리고 컴포넌트가 "half-finished" state를 렌더링하는 것을 방지할 수 있다.

(위 예시에서, count가 +1 됐지만, flag는 아직 true로 바뀌지않은 상태)

이것은 레스토랑 웨이터에 비유를 하면 더 쉽게 와닿을 수 있는데,

주문을 할 때 하나 고를 때마다 주방으로 달려가지 않고, 오더를 완성시킬 때까지 대기하는 것과 비슷하다.

기존에 React는 클릭과 같은 브라우저 이벤트에서만 업데이트를 배칭했었고,

아래 경우에 fetch 콜백에서 이벤트 핸들링이 완료된 이후에 state를 업데이트하기 때문에 배칭이 적용되지 않는다.

Demo

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

  function handleClick() {
    fetchSomething().then(() => {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c => c + 1); // Causes a re-render
      setFlag(f => !f); // Causes a re-render
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

React 18 이전까지, React 이벤트 핸들러 내부에서 발생하는 업데이트만 배칭을 하였다.

Promise, setTimeout, native event handler 그리고 여타 모든 이벤트 내부에서 발생하는 업데이트들은 React에서 배칭되지 않았다.

# What is automatic batching? (자동 배칭이란?)

React 18의 createRoot를 통해, 모든 업데이트들은 어디서 왔는가와 무관하게 자동으로 배칭되게 된다.

(React 18 + legacy render는 여전히 auto batching을 수행하지 않는다.)

이를 통해 렌더링 횟수를 줄이고, 앱의 성능을 조금 더 끌어올릴 수 있다.

React18 with createRoot Demo

React18 with legacy render Demo

자동 배칭을 하고싶지않다면 ReactDOM.flushSync()을 사용하면 된다.

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

# Hook을 사용할 때 이슈?

hook을 사용한다면, 거의 모든 경우에 있어 자동 배칭은 아무 이슈 없을 것이다. (있으면 버그..?)

# Class를 사용할 때 문제가 생길 수 있을까?

React의 이벤트 핸들러는 항상 배칭하므로 이러한 업데이트에는 변경 사항이 없다.

하지만 문제가 생길 수 있는 예외 케이스가 존재하는데,

Class Component에는 이벤트 내부에서 state 업데이트된 값을 동기적으로 읽을 수 있는 구현 특성이 있었다.

그 뜻은.. setState 호출 사이에 state 변화 값을 읽을 수 있었다.

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

React 18에서 이건 더 이상 동작하지 않는다.

setTimeout 안에 있는 모든 업데이트도 배칭되기 때문에 더 이상 첫 번째 setState의 결과를 동기적으로 렌더링하지 않는다.

렌더링은 다음 브라우저 tick상에서 발생하게되기에 렌더가 아직 수행되지 않은 상태로 남는다.

// { count: 1, flag: false } 
// React18 => { count: 0, flag: false }

이 케이스때문에 React18로 업그레이드를 못한다면 위에서 설명한 ReactDOM.flushSync()를 쓰면 된다.

물론 최대한 사용하지 않는 것을 추천.

useState에서 state 변경은 기존 값을 업데이트하지 않기에 Hooks를 가진 함수형 컴포넌트는 이 이슈에 영향을 받지 않는다.

# unstable_batchedUpdates

Some React libraries는 event handlers 바깥의 setState가 배칭되는 것을 강제하기 위해 도큐먼트에도 없는 API를 사용하고 있다.

import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
});

이 API는 React 18에서도 존재할 것이지만, 배칭이 자동으로 동작하기에 사실 더 이상 필요는 없다.

18 버전에서 없앨 예정은 아니고, 향후에 메이저 라이브러리들이 이 API 사용을 지우고난 뒤에 주요 버전 업데이트에서 없앨 예정이다.


ref: https://github.com/reactwg/react-18/discussions/21

https://immigration9.github.io/react/2021/06/12/automatic-batching-react.html