React 18의 Automatic Batching: 렌더링 최적화의 숨은 공신
React 애플리케이션을 개발하다 보면 성능 최적화, 특히
불필요한 리렌더링 방지는 언제나 중요한 숙제입니다.
혹시 React 17을 사용하면서 비동기 함수(setTimeout, Promise 등) 내부에서
상태를 여러 번 변경했을 때, 변경 횟수만큼 컴포넌트가 리렌더링 되는 현상을
겪어보셨나요?
이를 막기 위해 `unstable_batchedUpdates` 같은 API를 사용하거나 코드를
복잡하게 수정했던 기억이 있을 것입니다.
하지만 React 18로 넘어오면서 이 문제는 아주 우아하게 해결되었습니다.
바로 Automatic Batching(자동 배칭)이라는 기능
덕분입니다.
별도의 설정 없이도 렌더링 횟수를 획기적으로 줄여주는 이 마법 같은 기능이
내부적으로 어떻게 동작하는지, 그리고 우리가 주의해야 할 점은 무엇인지
자세히 알아보겠습니다.
🚀 이 글의 핵심 포인트:
- 배칭(Batching)의 기본 개념과 필요성
- React 17 vs React 18 배칭 동작 비교
- 예외 케이스: 배칭을 강제로 막아야 할 때 (`flushSync`)
🍔 배칭(Batching) 쉽게 이해하기
기술적인 설명에 앞서 간단한 비유를 들어보겠습니다.
여러분이 식당에서 웨이터에게 주문을 한다고 상상해 보세요.
"물 좀 주세요"라고 말하고 웨이터가 주방에 다녀오고, 다시 "수저 주세요"라고
말해서 웨이터가 또 다녀오고, 마지막으로 "메뉴판 주세요"라고 한다면
어떨까요?
아마 매우 비효율적일 것입니다.
현명한 웨이터라면 손님의 요구사항을 다 들은 뒤,
한 번에 주방으로 가서 모든 것을 가져올 것입니다.
React의 배칭(Batching)이 바로 이와 같습니다.
여러 개의 상태 업데이트(setState)가 발생하더라도, React는 이를 하나로
묶어서 단 한 번만 렌더링을 수행합니다.
⚖️ React 17과 18의 결정적 차이
사실 배칭은 React 18에서 처음 생긴 개념은 아닙니다.
React 17 이전에도 배칭은 존재했으나,
'React 이벤트 핸들러 내부'에서만 작동한다는 명확한 한계가
있었습니다.
🚫 React 17의 한계: 비동기 처리
React 17에서는 `setTimeout`, `fetch`, `Promise` 등의 비동기 함수 내부나
네이티브 이벤트 핸들러에서는 배칭이 작동하지 않았습니다.
따라서 아래 코드와 같은 상황에서 각각 별도로 렌더링이 발생했습니다.
setTimeout(() => {
setCount(c => c + 1); // 리렌더링 발생!
setFlag(f => !f); // 또 리렌더링 발생!
}, 1000);
// 총 2번의 렌더링 실행
✅ React 18의 개선: 어디서나 배칭 (Automatic Batching)
React 18은 `createRoot`를 통해 생성된 루트에서
모든 업데이트를 자동으로 배칭 처리합니다.
이제 업데이트가 어디서 발생하든(타임아웃, 프로미스, 네이티브 이벤트 등)
상관없이, React는 렌더링을 최대한 늦추고 모아서 처리합니다.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React가 기다렸다가...
}, 1000);
// 함수 실행이 끝난 후, 단 1번만 렌더링 실행!
| 상황 | React 17 (Legacy) | React 18 (Concurrent) |
|---|---|---|
| 클릭 이벤트 (React) | 배칭 O | 배칭 O |
| Promise / fetch | 배칭 X | 배칭 O |
| setTimeout | 배칭 X | 배칭 O |
🚨 배칭을 막아야 할 때는? (flushSync)
대부분의 경우 자동 배칭은 유용하지만, 아주 드물게 상태 변경 직후에 DOM을
즉시 업데이트하고 그 값을 읽어와야 하는 경우가 있을 수 있습니다.
이럴 때는 배칭을 강제로 끄고 즉시 렌더링을 수행해야 합니다.
React는 이를 위해 flushSync라는 메서드를 제공합니다.
⚠️ 주의:
flushSync는 성능을 저하시킬 수 있으므로 꼭 필요한 상황이
아니라면 사용을 자제해야 합니다.
function handleClick() {
flushSync(() => {
setCount(c => c + 1);
});
// 이 시점에 DOM이 이미 업데이트 되었습니다.
// 즉, 여기서 즉시 리렌더링이 발생했습니다.
setFlag(f => !f);
// 두 번째 리렌더링 발생
}
위 코드에서는 flushSync로 감싼 부분이 완료되는 즉시 React가
DOM을 업데이트하므로, 배칭이 깨지고 총 2번의 렌더링이 발생하게 됩니다.
🏁 더 나은 성능을 위하여
React 18의 Automatic Batching은 개발자가 별다른 노력을 들이지 않아도
애플리케이션의 성능을 높여주는 훌륭한 기능입니다.
단, 이 기능이 제대로 작동하려면 반드시
ReactDOM.createRoot를 사용하여 앱을 렌더링해야 한다는
점을 잊지 마세요.
기존의 `ReactDOM.render` 방식을 사용하면 React 18을 설치했더라도 React
17처럼 동작(Legacy Mode)하게 되어 자동 배칭의 혜택을 받을 수 없습니다.
✅ 실천 가이드
-
index.js파일을 확인하여createRootAPI를 사용 중인지 점검하세요. -
불필요하게
unstable_batchedUpdates를 사용하던 레거시 코드를 제거하세요. - 상태 업데이트 로직을 단순화하여 코드 가독성을 높이세요.

댓글 쓰기