React는 Batching을 어떻게 구현했을까요? - (1) 상태 업데이트 Batching

date
Feb 1, 2025
slug
react-batching-mechanism-state-updates
author
status
Public
tags
React
summary
type
Post
thumbnail
react-4.jpg
category
updatedAt
Feb 1, 2025 12:00 PM

🤔 React는 Batching을 어떻게 구현했을까요?

지난 글을 마무리 하면서 Batching에 대한 내용을 정리하다보니 내부 구현이 궁금해져서 딥다이브 하겠다!는 예고를 남겼습니다.
 
이번 글에서는 React의 소스 코드를 분석하며 Batching이 구현된 원리를 살펴보겠습니다.
특히, Batching이 동작하는 대표적인 세 가지 경우를 중심으로 살펴보겠습니다.
 

✅ Batching이 적용되는 주요 시점

 
1️⃣ 상태 업데이트 BatchinguseEffect 또는 생명주기 메서드 내부에서 여러 개의 setState가 호출될 때, React는 이를 어떻게 하나의 렌더링으로 묶을까?
 
2️⃣ 이벤트 핸들러 내부 Batching – 클릭, 입력 등의 이벤트 핸들러 내부에서 발생하는 여러 개의 상태 업데이트는 어떻게 Batching되는가?
 
3️⃣ Automatic Batching (React 18 이후) – 기존에는 Batching되지 않던 setTimeout, fetch 같은 비동기 코드 내부에서도 Batching이 동작하는 원리는?
 
추가적으로, React는 단순히 여러 개의 setState를 묶어서 처리하는 것뿐만 아니라, 업데이트의 중요도(우선순위)에 따라 처리 순서를 정하는 Render Lane 시스템도 사용합니다. 하지만 이 글에서는 Batching의 동작 원리에 집중하고, Render Lane의 개념은 이후 별도의 글에서 자세히 다룰 예정입니다.
 

 

1️⃣ 상태 업데이트 Batching

React에서는 setState가 호출될 때마다 즉시 렌더링을 수행하는 것이 아니라, 여러 개의 상태 업데이트를 모아 한 번의 렌더링으로 처리(Batching) 합니다.
이를 통해 불필요한 렌더링을 방지하고 성능을 최적화할 수 있습니다.
이 과정에서 React Fiber(Reconciler)와 Scheduler가 함께 동작하며, 내부적으로 executionContextupdateQueue를 활용하여 여러 상태 업데이트를 한 번에 처리할지 여부를 결정합니다.
 

🔹 Batching을 이해하기 위해 주목해야 할 두 가지 개념

React의 Batching이 어떻게 동작하는지 살펴보려면, 두 가지 핵심 개념을 이해해야 합니다.
 
  1. executionContext현재 실행 중인 컨텍스트를 관리하여 Batching 여부를 결정
    1. React가 특정 실행 모드(예: 배칭 모드, 렌더링 모드, 커밋 모드)에 있는지를 추적하며, 이 값에 따라 상태 업데이트를 즉시 반영할지, Batching하여 한 번에 처리할지를 결정합니다.
 
  1. updateQueue여러 개의 상태 업데이트를 임시로 저장하여 한 번에 처리
    1. setState가 호출될 때마다 업데이트를 임시로 저장하는 역할을 하며, 최종적으로 렌더링 시점에서 한 번에 반영되도록 도와줍니다.
 
 
React가 상태 업데이트를 어떤 과정을 거쳐 Batching 하는지 파악하기 위해 아래 함수들이 실행되는 과정을 차근차근 따라가보겠습니다.
 
 
notion image
 

 

🔹 상태 업데이트 발생 (Enqueue Update)

React에서 setState()가 호출되면, 즉시 렌더링을 수행하지 않고 먼저 업데이트 객체(Update Object)
생성한 후, updateQueue(업데이트 대기열)에 저장합니다. 이를 통해 여러 개의 상태 변경 요청을 한꺼번에 처리하여 불필요한 렌더링을 방지하고 성능을 최적화할 수 있습니다.
 
순차적으로 살펴보자면, setState()가 호출되면 내부적으로 다음과 같은 순서로 내부 함수들이 실행됩니다.
 

1. dispatchSetState() 실행 (상위 레벨 상태 업데이트 요청)

  • setState()가 호출되면 dispatchSetState() 실행
  • 이 함수는 내부적으로 dispatchSetStateInternal()을 호출하여 실제 업데이트 처리를 위임
 

2. dispatchSetStateInternal() 실행 (업데이트 객체 생성 및 큐에 추가)

 
📂 파일 위치: ReactFiberHooks.js
function dispatchSetStateInternal(fiber, queue, action, lane) { const update = { lane, revertLane: NoLane, action, hasEagerState: false, eagerState: null, next: null, }; ... const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane); if (root !== null) { scheduleUpdateOnFiber(root, fiber, lane); entangleTransitionUpdate(root, queue, lane); return true; } ... }
  • 업데이트 객체(update) 생성
    • lane: 업데이트의 우선순위 (Sync, Transition, Default 등) *Lane에 대해서는 이후에 다루겠습니다.
    • action: 새로운 상태 값 또는 업데이트 함수
    • next: 다음 업데이트 객체 (링크드 리스트 형태로 updateQueue에 추가됨)
 

3. enqueueConcurrentHookUpdate() 실행 (업데이트 대기열에 추가)

export function enqueueConcurrentHookUpdate<S, A>( fiber: Fiber, queue: HookQueue<S, A>, update: HookUpdate<S, A>, lane: Lane, ): FiberRoot | null { const concurrentQueue: ConcurrentQueue = (queue: any); const concurrentUpdate: ConcurrentUpdate = (update: any); enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane); // 내부에서 enqueueUpdate 호출 return getRootForUpdatedFiber(fiber); // 업데이트된 Fiber의 최상위 Root 찾기 }
  • 업데이트를 enqueueUpdate()에 전달
  • 업데이트된 Fiber가 속한 Root를 찾아 반환 (getRootForUpdatedFiber())
 

4. 내부 함수 enqueueUpdate에 위임

 
📂 파일 위치: ReactFiberConcurrentUpdates.js
function enqueueUpdate( fiber: Fiber, queue: ConcurrentQueue | null, update: ConcurrentUpdate | null, lane: Lane, ) { // 현재 업데이트 정보를 global queue (concurrentQueues)에 저장 concurrentQueues[concurrentQueuesIndex++] = fiber; concurrentQueues[concurrentQueuesIndex++] = queue; concurrentQueues[concurrentQueuesIndex++] = update; concurrentQueues[concurrentQueuesIndex++] = lane; // 병렬 업데이트를 위한 Lane 병합 concurrentlyUpdatedLanes = mergeLanes(concurrentlyUpdatedLanes, lane); // Fiber 트리에서 업데이트를 추적할 수 있도록 Lane 정보 갱신 fiber.lanes = mergeLanes(fiber.lanes, lane); const alternate = fiber.alternate; if (alternate !== null) { alternate.lanes = mergeLanes(alternate.lanes, lane); } }
  • 업데이트를 concurrentQueues[]라는 글로벌 큐에 저장
  • 여러 개의 setState() 호출을 병렬적으로 관리할 수 있도록 Lane을 병합
  • Fiber 트리에 Lane 정보를 동기화하여 업데이트가 올바르게 반영되도록 설정
 

 
[중간 정리]
  1. React에서 setState()가 호출되면, React는 즉시 렌더링하지 않고 먼저 업데이트를 저장합니다.
  1. enqueueConcurrentHookUpdate()는 내부적으로 enqueueUpdate()를 호출하여 업데이트를 전역 큐concurrentQueues[]에 추가합니다.
  1. enqueueUpdate()는 여러 개의 setState()를 효율적으로 배치하기 위해 Lane을 병합하고, Fiber의 업데이트 상태를 동기화합니다.
  1. 이어서 enqueueConcurrentHookUpdate 내부에서 finishQueueingConcurrentUpdates()를 호출하면서 여러 setState, 전역 큐(concurrentQueues[])에 저장된 업데이트들을 한 번에 처리하고 각 업데이트를 udpateQueue에 추가합니다. 이를 통해 여러 상태 업데이트를 한꺼번에 처리하여 성능을 최적화할 수 있습니다. 그 다음 scheduleUpdateOnFiber()가 실행되어 최적의 타이밍에 업데이트를 실행하도록 예약됩니다.
 
이제 scheduleUpdateOnFiber()가 업데이트를 실행하도록 예약을 걸기 위해 최적의 타이밍을 찾아가는 과정을 살펴보겠습니다.
 

 

🔹 업데이트 스케줄링 (Schedule Update on Fiber)

초반에 살펴본 함수인 dispatchSetStateInternal()이 실행되면, React는 업데이트된 Fiber의 최상위 Root를 찾고 (getRootForUpdatedFiber()), 이를 기반으로 scheduleUpdateOnFiber()를 호출합니다.
 

1. dispatchSetStateInternal() 내부에서 scheduleUpdateOnFiber() 실행

 
📂 파일 위치: ReactFiberHooks.js
if (root !== null) { scheduleUpdateOnFiber(root, fiber, lane); entangleTransitionUpdate(root, queue, lane); return true; }
  • dispatchSetStateInternal()이 실행되면, React는 업데이트된 Fiber의 Root를 찾고 (getRootForUpdatedFiber()), 이를 기반으로 scheduleUpdateOnFiber()를 호출
 

2. scheduleUpdateOnFiber() 내부 동작

 
📂 파일 위치: ReactFiberWorkLoop.js
export function scheduleUpdateOnFiber(root: FiberRoot, fiber: Fiber, lane: Lane) { if (isRendering) { return; // 이미 렌더링 중이면 Batching하여 처리 } // 1️⃣ 현재 실행 중인 Context를 확인하여 Batching할지 결정 if (executionContext & (RenderContext | CommitContext) !== NoContext) { ensureRootIsScheduled(root); return; } // 2️⃣ Batching을 위해 실행 컨텍스트 설정 const prevExecutionContext = executionContext; executionContext |= BatchedContext; try { // 3️⃣ 최적의 스케줄링을 설정하여 업데이트 예약 ensureRootIsScheduled(root); } finally { executionContext = prevExecutionContext; } }
  • 현재 실행 중인 Context(executionContext)를 확인하여 Batching할지 결정
  • Batching 모드(BatchedContext) 활성화 → 여러 개의 setState()를 한 번에 실행 가능
  • ensureRootIsScheduled()를 호출하여 업데이트를 최적의 시점에 실행하도록 예약
 
 

 

🔹 Batching 처리 (Ensure Root is Scheduled & Scheduler 실행)

 

ensureRootIsScheduled() 업데이트를 예약하는 함수

 
📂 파일 위치: ReactFiberRootScheduler.js
function ensureRootIsScheduled(root) { const existingCallbackNode = root.callbackNode; // 현재 작업을 Scheduler에 예약 const newCallbackNode = scheduleCallback(performConcurrentWorkOnRoot); root.callbackNode = newCallbackNode; }
  • 현재 작업이 스케줄링되어 있는지 확인
  • 없다면 scheduleCallback(performConcurrentWorkOnRoot)을 호출하여 다음 렌더링을 예약
  • 여러 개의 상태 변경이 하나의 작업으로 병합(batch)되어 최적의 타이밍에 처리
 

 

🔹 렌더링 및 커밋 (Reconciliation & Commit Phase)

 

processUpdateQueue()commitRoot() 실행

 
📂 파일 위치: ReactFiberWorkLoop.js
function processUpdateQueue(workInProgress, props, instance) { let queue = workInProgress.updateQueue; let newState = workInProgress.memoizedState; while (queue.length > 0) { const update = queue.shift(); // 큐에서 업데이트 하나씩 가져옴 newState = processUpdate(newState, update); // 새로운 상태 계산 } workInProgress.memoizedState = newState; } function commitRoot(root) { commitBeforeMutationEffects(root); commitMutationEffects(root); commitLayoutEffects(root); }
  • Fiber 트리는 Reconciliation(재조정)하여 변경된 부분만 렌더링
  • processUpdateQueue()가 실행되어 Batching된 업데이트를 한 번에 적용 (updateQueue 에 저장된 상태 변경 사항을 가져와서 memoizedState에 반영)
    • 즉, 여러 개의 setState() 호출이 Batching되었다면, 이 단계에서 한꺼번에 적용
  • commitRoot() 실행 → 최종적으로 DOM에 반영
  • commitRoot()가 실행되면서, executionContext |= CommitContext;가 설정되어 현재 커밋 중임을 표시함
  • 커밋이 완료되면(상태 업데이트가 끝난 후) executionContext는 다시 원래 상태로 복구됨
 
 

 

🔍 상태 업데이트 시 Batching 처리가 작동하는 과정 (최종 정리)

 
단계
실행되는 함수
역할
1️⃣ setState() 호출
dispatchSetState()
상태 업데이트 요청
2️⃣ 업데이트 큐에 추가
enqueueConcurrentHookUpdate()
concurrentQueues[](전역 큐)에 추가
3️⃣ 전역 큐 → updateQueue로 이동
finishQueueingConcurrentUpdates()
전역 큐의 업데이트를 updateQueue로 이동
4️⃣ 업데이트 예약
scheduleUpdateOnFiber()
배칭 여부 결정 후 업데이트 예약
5️⃣ 스케줄링 실행
ensureRootIsScheduled()
업데이트를 최적의 타이밍에 실행
6️⃣ 최적의 타이밍에 렌더링 실행
performConcurrentWorkOnRoot()
배칭된 업데이트를 한 번에 렌더링