이번 포스팅에는 리액트 렌더링 최적화를 위한 배칭(Batching)과 자동 배칭(Auto Batching)에 대한 내용을 정리하고자 합니다.
배칭은 state 업데이트를 하나의 렌더링으로 묶는 것을 의미합니다.
아래의 코드를 보면 handleClick을 한 번 클릭하면 count가 2씩 증가하는 것 처럼 보일 수 있으나 배칭으로 인하여 setCount 함수가 한 번만 실행됩니다.
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
// 배칭되어 setCount 함수가 한 번만 실행됩니다.
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1>{count}</h1>
</div>
);
}
handleClick 함수 블록 내부에서 setCount 함수를 여러번 실행시키기 위해서는 함수형 업데이트를 사용하면 됩니다.
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
function handleClick() {
setCount((prev) => count + 1); // 이전 count 값을 불러와 count에 1을 더 해줍니다.
setCount((prev) => count + 1);
}
// 클릭 할때 마다 count가 2씩 증가합니다.
return (
<div>
<button onClick={handleClick}>Next</button>
<h1>{count}</h1>
</div>
);
}
또 다른 예시로, 아래의 코드에서 handleClick 내부에서 count, isBoolean 이라는 state가 변경되고 있습니다.
리액트에서는 state, props가 변경되면 렌더링이 일어나기 때문에 2번의 렌더링이 발생해야하지만 콘솔창을 확인하면 렌더링이 한번만 발생하는 것을 확인할 수 있습니다.
배칭을 레스토랑 웨이터에 비유를 하면 더 쉽게 와닿을 수 있는데, 주문을 할 때 하나 고를 때마다 주방으로 달려가지 않고, 오더를 완성시킬 때까지 대기하는 것 입니다.
import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [isBoolean, setBoolean] = useState(false);
function handleClick() {
setCount((c) => c + 1); // 아직 리렌더링 하지 않습니다.
console.log(count);
setBoolean((f) => !f); // 아직 리렌더링 하지 않습니다.
console.log(flag);
}
// React는 이 함수가 끝나면 리렌더링을 합니다.
return (
<div>
<button onClick={handleClick}>Next</button>
<h1>{count}</h1>
</div>
);
}
리액트는 자동적으로 배칭을 통해 불필요한 렌더링을 줄이면서 성능 최적화와 동시에 예측할 수 없는 버그 발생을 방지합니다.
React 18 이전까지, React 이벤트 핸들러 내부에서 발생하는 업데이트만 배칭을 하였으며, Promise, setTimeout, native 이벤트 핸들러, 그리고 여타 모든 이벤트 내부에서 발생하는 업데이트들은 React에서 배칭되지 않았습니다.
리액트 18 부터는 createRoot를 통해 모든 업데이트들이 자동으로 배칭이 되게 됩니다.
대부분의 경우 배칭은 안전한 코드를 만들기 위해 사용되는 방법이지만, 몇몇 코드는 state 변경 후 즉시 DOM으로부터 값을 가져오는 경우가 있을 수 있습니다. 이런 경우, ReactDOM.flushSync()를 사용함으로써 배칭을 하지 않을 수 있습니다.
아래의 코드 처럼 flushSync 함수의 콜백함수 내부에서 setCount, setFlag 함수를 실행할 경우 렌더링이 두번 발생하는 것을 확인할 수 있습니다. 하지만 리액트 개발팀에서는 최대한 사용하지 않는 것을 추천하고 있습니다.
import { useState } from "react";
import { flushSync } from "react-dom"; // Note: react가 아닌 react-dom이다
export default function App() {
const [count, setCount] = useState(0);
function handleClick() {
flushSync(() => {
setCount((c) => c + 1); // 렌더링이 발생합니다.
console.log(count);
});
flushSync(() => {
setCount((c) => c + 1); // 렌더링이 발생합니다.
console.log(count);
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1>{count}</h1>
</div>
);
}
reference : 리액트 공식홈페이지