렌더링 최적화를 위한 React의 기능, Part 1. Automatic Batching.

kim yeseul·2023년 11월 13일
0
post-thumbnail
post-custom-banner

1. Automatic Batching

예를 들어 하나의 함수에 setState가 여러 개 있다고 생각해보자.
그럼 보통 처음에 드는 생각은 리렌더링이 setState 개수만큼 일어나지 않을까? 그럼 한 번에 리렌더링이 여러 번 일어날텐데 성능에 괜찮을까? 라는 의문을 가질 수 있다.
...
여기서 리액트의 기능인 배칭 관점에서 본다면 일단 '아니다' 이다. 리액트에서 setState를 많이 실행시켜도 리렌더링은 한번만 일어나게 되고 이것은 리액트의 배칭(Batching)을 통해 일괄 처리하는 것이다. 그렇다면 대체 배칭이란 어떤 기능이고 어떤 방식으로 동작을 하는 걸까?

🧐 Batching이란?

React가 더 나은 성능을 위해 여러 state 업데이트를 단일 리렌더링으로 그룹화(일괄 처리)하는 것!
_
배칭은 불필요한 리렌더링 방지를 하므로 성능에 좋다. 또한 하나의 상태 변수만 업데이트되어 버그가 발생할 수 있는 상태(반완료)를 컴포넌트가 렌더링하는 것을 방지한다.

예를 들어, 동일한 클릭 이벤트 내에 두 개의 상태 업데이트가 있는 경우 React는 항상 이를 하나의 리렌더링으로 배칭했다. 아래의 예시 코드를 보면 클릭 시마다 상태를 두 번 설정했으나 React는 단일 렌더링만 수행하는 것을 볼 수 있다.

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

  function handleClick() {
    console.log("=== click ===");
    setCount(c => c + 1); // 아직 리렌더링 하지 않는다
    setFlag(f => !f); // 아직 리렌더링 하지 않는다
    // React는 이 함수가 끝나면 리렌더링을 한다 (이것이 배칭이다!)
  }

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

function LogEvents(props) {
  useLayoutEffect(() => {
    console.log("Commit");
  });
  console.log("Render");
  return null;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

React 18 이전에는 Batching 기능이 있었나요?

⭕ Yes. 그렇다!

React 18 이전까지는 이벤트 핸들러 내부의 state 업데이트를 배칭하였다. 하지만 Promise, setTimeout, Native Event Handler 내부의 업데이트는 기본적으로 배칭되지 않았다.

🧐 왜 그런가요?

리액트는 브라우저 이벤트(ex. 클릭) 중에만 배칭 작업을 수행하기 때문이다. 이벤트가 종료된 후에 실행되는 경우는 배칭이 불가능 하다.

예시로 아래 코드를 보자.
setState가 비동기로 처리되어 fetchSomething()이 종료된 후에 상태를 업데이트 하기 때문에 배칭이 불가능하여 두 번씩 콘솔에 찍히는 것을 볼 수 있다.

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

  function handleClick() {
    console.log("=== click ===");
    fetchSomething().then(() => {
      // React 17과 이전 버전에서는 이 업데이트들이 배칭되지 않음
      // 이벤트 진행 중이 아닌, 완료 후의 콜백에서 실행되기 때문에
      setCount((c) => c + 1); // 리렌더링을 발생시킨다.
      setFlag((f) => !f); // 리렌더링을 발생시킨다.
    });
  }

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

function LogEvents(props) {
  useLayoutEffect(() => {
    console.log("Commit");
  });
  console.log("Render");
  return null;
}

function fetchSomething() {
  return new Promise((resolve) => setTimeout(resolve, 100));
}

React는 일괄 업데이트 시기에 대한 일관성이 없다. 예를 들어, 데이터를 가져온 다음 위의 상태를 업데이트해야 하는 경우 handleClick은 업데이트를 배칭하지 않고 두 개의 독립적인 업데이트를 수행한다.

🙊 React 18 버전부터는 위와 같은 문제점을 보완한 Automatic Batching 기능이 추가되었다!

Automatic Batching

React 18은 기본적으로 더 많은 배칭을 수행하여 애플리케이션이나 라이브러리 코드에서 수동으로 일괄 업데이트 할 필요성을 제거함으로써 즉시 사용 가능한 성능 향상을 추가했다.
_
createRoot를 사용하는 React 18부터 시작하여 모든 업데이트가 어디에서 시작되었는지와 관계없이 자동으로 배칭된다. 이 기능을 바로 Automatic Batching이라고 한다.

기대되는 효과 - 성능!

Automatic BatchingPromise, setTimeout, Native Event Handler 내부의 업데이트가 React 이벤트 내부의 업데이트와 동일한 방식으로 배칭된다는 것을 의미한다. 이로 인해 렌더링 작업이 줄어들어 애플리케이션 성능이 향상될 것으로 예상된다.

실제로 사용하기 위해서는 React 18부터 제공하는 ReactDOMClientcreateRoot 메서드를 사용해야 한다.

  • index.js 예시
import ReactDOM from "react-dom/client";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
	<React.StrictMode>
		<App />
	</React.StrictMode>
);

➡️ 프로덕션 환경에서 테스팅하기 용이하므로 React 18을 도입할 때 createRoot로 업그레이드 하는 것이 권장된다.

🙅‍♀️ 배칭을 원하지 않으면 어떻게 하나요?

일반적으로 배칭은 안전하지만 일부 코드는 상태 변경 직후에 DOM에서 무언가를 읽어야 할 수 있다. 이와 같은 경우에서는 ReactDOM.flushSync()를 사용하여 배칭을 무시할 수 있다.

단, React에서 공식적으로 해당 메서드 사용을 추천하지 않는다. 클래스 컴포넌트에서 문제가 될 수 있는 경우 등이 있기 때문이다. 필요한 경우에만 사용할 것!

  • 사용 예시
import { flushSync } from "react-dom"; // Note: react가 아닌 react-dom이다

function handleClick() {
  flushSync(() => {
    setCounter((c) => c + 1);
  });
  // 이 과정이 끝났을 때 React는 DOM을 업데이트한 상태이다
  flushSync(() => {
    setFlag((f) => !f);
  });
  // 이 과정이 끝났을 때 React는 DOM을 업데이트한 상태이다
}

reference

profile
출발선 앞의 준비된 마음가짐, 떨림, 설렘을 가진 주니어 개발자
post-custom-banner

0개의 댓글