예를 들어 하나의 함수에 setState가 여러 개 있다고 생각해보자.
그럼 보통 처음에 드는 생각은 리렌더링이 setState 개수만큼 일어나지 않을까? 그럼 한 번에 리렌더링이 여러 번 일어날텐데 성능에 괜찮을까? 라는 의문을 가질 수 있다.
...
여기서 리액트의 기능인 배칭 관점에서 본다면 일단 '아니다' 이다. 리액트에서 setState를 많이 실행시켜도 리렌더링은 한번만 일어나게 되고 이것은 리액트의 배칭(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 이전까지는 이벤트 핸들러 내부의 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은 기본적으로 더 많은 배칭을 수행하여 애플리케이션이나 라이브러리 코드에서 수동으로 일괄 업데이트 할 필요성을 제거함으로써 즉시 사용 가능한 성능 향상을 추가했다.
_
createRoot
를 사용하는 React 18부터 시작하여 모든 업데이트가 어디에서 시작되었는지와 관계없이 자동으로 배칭된다. 이 기능을 바로 Automatic Batching이라고 한다.
Automatic Batching은
Promise
,setTimeout
,Native Event Handler 내부의 업데이트
가 React 이벤트 내부의 업데이트와 동일한 방식으로 배칭된다는 것을 의미한다. 이로 인해 렌더링 작업이 줄어들어 애플리케이션 성능이 향상될 것으로 예상된다.
ReactDOMClient
의 createRoot
메서드를 사용해야 한다.import ReactDOM from "react-dom/client";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
createRoot
로 업그레이드 하는 것이 권장된다.일반적으로 배칭은 안전하지만 일부 코드는 상태 변경 직후에 DOM에서 무언가를 읽어야 할 수 있다. 이와 같은 경우에서는
ReactDOM.flushSync()
를 사용하여 배칭을 무시할 수 있다.
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