본문 바로가기
react

React 18 자동 배칭, Automatic batching

by 새우하이 2022. 3. 3.

Automatic batching for fewer renders in React 18 #21

React는 여러 개의 state update를 모아서 한 번에 re-rendering을 진행합니다.

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

  function handleClick() {
    setCount(c => c + 1); // 아직 리렌더링 안함
    setFlag(f => !f); // 아직 리렌더링 안함
    // 리액트는 이 함수가 다 끝나면 리렌더링 함 
        // 이를 batching이라 부름.
  }

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

이런 처리 방식은 웹페이지의 렌더링 횟수를 줄일 수 있습니다.

Demo

하지만 현재(리액트 17) 까지의 batch update는 일관적이지 못했습니다. 현재까지는 React event handler 내부의 업데이트 까지만 batch update를 했기 때문에 Promise, setTimeout, native event handler와 그 외 모든 이벤트 내부에서의 업데이트 들은 React에서 batching되지 않았습니다.

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

  function handleClick() {
    fetchSomething().then(() => {
      // 리액트 17까지는 이 업데이트 들이 
            // 진행중인 이벤트 상태가 아닌 완료된 후의 콜백에서
            // 실행되기 때문에 batching 되지 않았습니다.
      setCount(c => c + 1); // 리렌더링 발생 시킴
      setFlag(f => !f); // // 리렌더링 발생 시킴
    });
  }

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

이 경우에도 fetchSomething 에서 이벤트가 핸들링 완료된 후에 state를 업데이트 하기 때문에 batching 이 일어나지 않습니다.

Demo

이제 18버전 부터는 [ReactDOM.createRoot](https://ko.reactjs.org/docs/concurrent-mode-reference.html#createroot) 를 통해 브라우저 이벤트 뿐만 아니라 어디에서 왔는가와 무관하게 자동으로 batching이 적용되게 할 수 있습니다. 이를 통해 리액트 팀은 렌더링을 최소화 하여 performance 개선을 기대한다고 합니다. 이 기능을 automatic batching 이라고 합니다.

Demo

Demo2 : React18 + legacy render 는 이전 방식을 유지합니다.

적용되게 할 수 있다는 말은. Automatic Batching을 하지 않게 할 수도 있다는 말 입니다.

대부분에는 batching이 안전한 절차이지만, 몇몇 코드는 DOM으로부터 값을 읽어오는 것에 의존합니다. 이런경우엔 ReactDOM.flushSync 를 이용해 해당 상태 업데이트 호출을 배치 대상에서 제외시킬 수 있습니다.

import { flushSync } from 'react-dom';

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // 렌더링 함
  flushSync(() => {
    setFlag(f => !f);
  });
  // 또 렌더링 함
}

이런 상황이 일반적인 상황은 아님.

React의 이벤트 핸들러는 늘 batch update를 수행해왔기 때문에 별 변화는 없을 것입니다. 하지만 Class Components를 사용할 때 문제가 생길 수 있는 예외가 케이스가 있습니다.

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

Class Component에는 이벤트 내부에서 state 업데이트된 값을 동기적으로 읽을 수 있습니다.

Hooks를 가진 Function Component는 useState 에서 state 변경은 기존 값을 업데이트하지 않기 때문에 이 이슈에 영향을 받진 않습니다.

하지만 React18 부터는 이는 더 이상 동작하지 않습니다 : Demo

왜냐면 앞서 말했듯 이젠 setTimeout에 있는 update도 batching 되기 때문에 더 이상 첫 번째 setState의 결과를 동기적으로 렌더링하지 않습니다.

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

      console.log(this.state.count);

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

ReactDOM.flushSync 를 사용해서 Automatic Batching을 사용하지 않아, 해당 상태 업데이트 호출을 배치 대상에서 제외시킬 수 있지만 권장하지 않습니다..

댓글