React Fiber Architecture

TL;DR

Fiber는 React가 컴포넌트 트리를 표현하는 데이터 구조이자, 그 트리에 대한 work을 쪼개고 우선순위를 매기고 재개·폐기 가능한 단위로 만든 실행 모델 입니다. 핵심 6가지:

  1. Fiber 노드 = "한 컴포넌트 인스턴스에 대한 work 한 단위". React 16 이전의 stack-based reconciler를 대체하기 위해 만들어짐.
  2. 단방향 linked list tree (return / child / sibling / index). 트리지만 메모리상으로는 linked list라 양방향 traverse가 자유로움.
  3. Double buffer: 항상 두 트리(currentwork-in-progress)가 alternate 포인터로 짝지어 있다. 새 트리를 만들면서도 현재 트리는 멀쩡.
  4. Two-phase work: Render phase (interrupt 가능, beginWork 내려가며 + completeWork 올라오며) + Commit phase (atomic, mutation → layout → passive).
  5. Effects는 bitmask(flags/subtreeFlags)로 fiber 자신에 기록되고, completeWorkbubbleProperties가 자식 → 부모로 OR-합산 → commit phase가 한 번에 traverse.
  6. Hooks는 fiber 안에 singly linked list로 저장(fiber.memoizedState). 그래서 hook 순서가 바뀌면 안 됨.

이 데이터 구조의 모양 때문에 — 그 위에서만 — Concurrent Rendering, Suspense, Error Boundary, Selective Hydration이 가능하다.


1. 왜 Fiber인가 — 16 이전의 한계

React 15까지의 reconciler는 "stack reconciler"라 불렸다. 그 모양은 단순했다:

function reconcile(node) {
  // 1. 새 element와 비교
  // 2. update 결정
  for (const child of node.children) {
    reconcile(child)   // ← 재귀 호출
  }
}

문제 3개:

  1. JS call stack을 사용 → 재귀가 끝나기 전엔 다른 작업이 메인 스레드 점유 불가
  2. 중단 불가 → 한 번 시작하면 끝까지 가야 함
  3. 우선순위 없음 → 모든 update가 동등하게 즉시 처리

이를 풀려면 reconciliation의 실행 상태를 JS call stack이 아닌 명시적 자료구조 에 담아야 한다. 그래야:

  • 중단했다가 다음 frame에 이어서 진행
  • 어떤 fiber까지 처리했는지 명시 (= workInProgress 포인터)
  • 더 급한 일이 들어오면 그 자료구조를 버리고 새로 시작

그 자료구조가 Fiber다. 이름의 유래는 "JS call stack 위의 가벼운 스레드(fiber)"라는 비유.


2. Fiber 노드의 데이터 구조

타입 정의는 ReactInternalTypes.js:89-210:

react-main/packages/react-reconciler/src/ReactInternalTypes.js:89-210

// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
export type Fiber = {
  ...
  tag: WorkTag,
  ...
  key: ReactKey,
  ...
  elementType: any,
  ...
  type: any,
  ...
  stateNode: any,
  ...
  return: Fiber | null,

  // Singly Linked List Tree Structure.
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  ...
  pendingProps: any,
  memoizedProps: any,
  updateQueue: mixed,
  memoizedState: any,
  dependencies: Dependencies | null,
  ...
  mode: TypeOfMode,

  // Effect
  flags: Flags,
  subtreeFlags: Flags,
  deletions: Array<Fiber> | null,

  lanes: Lanes,
  childLanes: Lanes,

  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null,
  ...
};

필드 5개 그룹

그룹필드역할
정체성tag (FunctionComponent / HostComponent / Fragment 등 26종 enum), key, elementType, type, stateNode이 fiber가 무엇인가, 실제 DOM/instance는 어디 있나
트리 구조return, child, sibling, index부모/자식/형제 — singly linked tree
work 데이터pendingProps (들어오는 props), memoizedProps (직전 렌더된 props), memoizedState (직전 state — 함수형은 hook chain), updateQueue, dependencies (context 구독)render 중·후 데이터
이펙트/우선순위flags, subtreeFlags, deletions, lanes, childLanes, modecommit해야 할 일과 그 우선순위
double bufferalternate짝꿍 fiber 포인터

tag 종류 (ReactWorkTags.js)

가장 흔한 것:

  • 0 HostRoot — root container
  • 1 FunctionComponent — 함수형 컴포넌트
  • 1 ClassComponent
  • 5 HostComponent — DOM 노드 (div, span 등)
  • 6 HostText — text 노드
  • 7 Fragment
  • 9 ContextProvider / 10 ContextConsumer
  • 13 SuspenseComponent
  • 15 MemoComponent / 16 SimpleMemoComponent
  • ... 등 26종

각 tag마다 beginWork/completeWork/commit*switch 분기가 따로 있다.


3. 단방향 Linked List Tree

이게 Fiber의 첫 번째 묘미다. 트리지만 메모리상에선 child 1개 + sibling 포인터만 있다.

   App
    │ child
   Header ──sibling──► Main ──sibling──► Footer
    │ child            │ child           │ child
    ▼                  ▼                 ▼
   ...                ...                ...

각 자식 fiber에는 return이 부모를 가리킨다 (이름이 parent가 아니라 return인 이유는 "JS call stack에서 이 함수가 끝나면 어디로 return할지"의 비유다).

왜 이렇게 만들었나

  • 다음에 처리할 fiber가 명확 — child가 있으면 거기로, 없으면 sibling, sibling도 없으면 return의 sibling
  • completeUnitOfWork이 정확히 이 알고리즘:

react-main/packages/react-reconciler/src/ReactFiberWorkLoop.js:3347-3404

function completeUnitOfWork(unitOfWork: Fiber): void {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.
  let completedWork: Fiber = unitOfWork;
  do {
    ...
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    ...
    next = completeWork(current, completedWork, entangledRenderLanes);
    ...
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  ...
}

정확히 "현재 fiber complete → sibling이 있으면 거기로 → 없으면 return으로 올라가서 다시 그 sibling 보기". DFS post-order의 정통 구현.

  • 언제든 work loop을 멈춰도 workInProgress 포인터 하나로 어디까지 했는지 복구 가능 — 이게 concurrent 렌더링의 전제

4. Double Buffer — current ↔ work-in-progress

게임 그래픽의 double buffering에서 차용한 아이디어. 항상 두 트리가 메모리에 있고, 짝꿍 fiber끼리 alternate 포인터로 연결:

current tree (화면에 보이는 것)
   App ◄──alternate──► App (work-in-progress)
   ...                  ...

새 render가 시작되면 React는 현재 트리를 건드리지 않고, alternate(없으면 새로 만들고 짝지음)를 work-in-progress 트리로 사용해 거기에 변경사항을 쌓는다. Commit이 끝나면 root.current = workInProgress포인터 한 줄 바꿔치기 — 이것이 트리 전환의 전부.

createWorkInProgress 실제 코드

react-main/packages/react-reconciler/src/ReactFiber.js:327-414

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    // We use a double buffering pooling technique because we know that we'll
    // only ever need at most two versions of a tree. We pool the "other" unused
    // node that we're free to reuse. This is lazily created to avoid allocating
    // extra objects for things that are never updated. It also allow us to
    // reclaim the extra memory if needed.
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;
    ...
    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    // Needed because Blocks store data on type.
    workInProgress.type = current.type;

    // We already have an alternate.
    // Reset the effect tag.
    workInProgress.flags = NoFlags;

    // The effects are no longer valid.
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
    ...
  }
  ...
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;
  ...
}

핵심 관찰

  • 두 fiber만 메모리에 둔다 — 트리의 모든 노드에 대해 (메모리 효율). 주석: "we'll only ever need at most two versions of a tree"
  • 재활용 풀(pooling)alternate가 이미 있으면 새로 만들지 않고 필드만 reset
  • stateNode는 공유 — 실제 DOM 노드는 둘 다 같은 것을 가리킴. Fiber만 두 벌, host instance는 한 벌.
  • flags, subtreeFlags, deletions는 reset — 이번 렌더 동안 다시 채워질 것
  • child/memoizedProps/memoizedState/updateQueue는 일단 current에서 복제 — 그 후 reconciliation이 필요하면 수정

이게 React가 "렌더 중에도 화면은 멀쩡"한 비결이다. work-in-progress가 어떻게 되든 current는 계속 사용자가 보는 진실의 원천.


5. Two-Phase Work: Render + Commit

Phase책임중단 가능?트리
Render새 work-in-progress 트리를 만들고 effects를 표시✓ (Concurrent에서)work-in-progress 작성
Commit트리를 화면에 반영, effects를 실제로 실행✗ (atomic)current ↔ work-in-progress 스왑

Render는 다시 두 phase로 나뉜다:

  • Begin phase — 트리를 내려가며 beginWork 호출
  • Complete phase — 자식이 끝나면 올라오며 completeWork 호출

performUnitOfWork이 한 fiber 단위를 처리하는데, 그 안에서 beginWork 호출 → 자식이 있으면 workInProgress = next로 내려가고, 자식이 없으면 completeUnitOfWork로 올라오는 phase로 전환:

react-main/packages/react-reconciler/src/ReactFiberWorkLoop.js:3060-3102

function performUnitOfWork(unitOfWork: Fiber): void {
  ...
  next = beginWork(current, unitOfWork, entangledRenderLanes);
  ...
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

이 한 함수가 fiber tree DFS의 정확한 구현이다. work loop은 이 함수를 반복 호출할 뿐.


6. Begin Phase (beginWork) 상세

react-main/packages/react-reconciler/src/ReactFiberBeginWork.js:4168-4222

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  ...
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      ...
    ) {
      // If props or context changed, mark the fiber as having performed work.
      didReceiveUpdate = true;
    } else {
      // Neither props nor legacy context changes. Check if there's a pending
      // update or context change.
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes,
      );
      if (
        !hasScheduledUpdateOrContext &&
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        // No pending updates or context. Bail out now.
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes,
        );
      }
      ...
    }
  }
  ...
}

Begin phase가 하는 일 3가지

  1. Bailout 결정: props가 그대로이고 이 fiber/childLanes에 예정된 업데이트가 없으면 → 이 subtree 전체 skip. 이게 React의 "변경되지 않은 부분은 그냥 건너뛴다" 매커니즘. oldProps !== newProps 비교가 reference equality라는 게 중요 — 부모가 매번 새 객체 prop을 만들면 bailout 무효화.

  2. Tag별 처리: switch (workInProgress.tag)로 갈라져서:

    • FunctionComponent → 함수 본체 실행 → JSX 반환받음 → 자식 element들을 받음
    • ClassComponent → 인스턴스의 render() 호출
    • HostComponent (DOM 노드) → children prop 그대로 사용
    • ContextProvider → context value 변경되었는지 비교 → 변경시 구독자 fiber에 lane 표시
  3. Reconciliation: 받은 자식 elements와 기존 current.child linked list를 비교해 work-in-progress의 child linked list를 만듦. 이 과정에서 각 child fiber에 Placement, Update, Deletion 같은 flag가 박힘.

beginWorkworkInProgress.child를 반환하고 (혹은 null), work loop이 그걸로 내려간다.


7. Complete Phase (completeWork) 상세

react-main/packages/react-reconciler/src/ReactFiberCompleteWork.js:1068-1117

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  ...
  popTreeContext(workInProgress);
  switch (workInProgress.tag) {
    ...
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    ...
      bubbleProperties(workInProgress);
      return null;
    ...
    case HostRoot: {
      const fiberRoot = (workInProgress.stateNode: FiberRoot);
      ...
    }
    ...
  }
}

Complete phase가 하는 일

  • Host effects 생성: HostComponent(DOM 노드)면 실제 DOM 인스턴스를 만들거나 (mount), 변경된 props를 updatePayload로 계산해 flags |= Update (update). 이때 host instance가 만들어지므로 render phase 마지막에 DOM이 메모리에 준비됨 (commit phase에서 attach만 하면 됨).
  • bubbleProperties 호출 — 이 부분이 핵심:
    • 자식들의 subtreeFlags를 OR로 합산
    • 자식들의 lanes/childLanes를 OR로 합산해 자기 childLanes에 기록
    • 결과: root에 도달하면 root의 subtreeFlags가 "트리 어딘가에 어떤 effect가 있나"를 한 비트마스크로 압축해서 들고 있음. Commit phase가 빠르게 traverse할 수 있는 이유.

bubbleProperties가 정확히 fiber 아키텍처의 bottom-up 정보 집계 메커니즘이다.


8. Effects는 Bitmask로 산다

react-main/packages/react-reconciler/src/ReactFiberFlags.js:18-39

export const NoFlags = /*                      */ 0b0000000000000000000000000000000;
export const PerformedWork = /*                */ 0b0000000000000000000000000000001;
export const Placement = /*                    */ 0b0000000000000000000000000000010;
...
export const Update = /*                       */ 0b0000000000000000000000000000100;
...
export const ChildDeletion = /*                */ 0b0000000000000000000000000010000;
export const ContentReset = /*                 */ 0b0000000000000000000000000100000;
export const Callback = /*                     */ 0b0000000000000000000000001000000;
...
export const Ref = /*                          */ 0b0000000000000000000001000000000;
export const Snapshot = /*                     */ 0b0000000000000000000010000000000;
export const Passive = /*                      */ 0b0000000000000000000100000000000;
...
export const Visibility = /*                   */ 0b0000000000000000010000000000000;

각 fiber에 두 비트마스크:

  • flags: 이 fiber 자체에 어떤 effect가 필요한가
  • subtreeFlags: 이 fiber의 subtree에 (자기 자신 제외) 어떤 effect가 있나 — completeWorkbubbleProperties가 채움

세 가지 commit phase에 대응하는 마스크:

react-main/packages/react-reconciler/src/ReactFiberFlags.js:116-132

export const MutationMask =
  ...
export const LayoutMask = Update | Callback | Ref | Visibility;
...
export const PassiveMask = Passive | Visibility | ChildDeletion;
Mask의미Commit phase
MutationMaskDOM 변경 (Placement, Update content, deletion 등)1. Mutation phase
LayoutMaskuseLayoutEffect, ref, callback 등 — DOM mutation 직후 동기로2. Layout phase
PassiveMaskuseEffect — DOM mutation 후 비동기로3. Passive phase

이 비트마스크 덕에 commit phase가 fiber 전체를 traverse하면서 "이 subtree에 mutation이 있나?"를 1번의 비트 AND로 결정. 없으면 subtree 통째로 skip.


9. Commit Phase 3단계

react-main/packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitMutationEffects(
react-main/packages/react-reconciler/src/ReactFiberCommitWork.js
export function commitLayoutEffects(
react-main/packages/react-reconciler/src/ReactFiberWorkLoop.js
function flushPassiveEffects(): boolean {

순서

  1. commitMutationEffects — DOM 변경 적용. 이 phase 안에서:

    • Snapshot flag가 있는 클래스 컴포넌트의 getSnapshotBeforeUpdate 호출
    • ChildDeletion이 있으면 해당 fiber 트리를 unmount → componentWillUnmount / useLayoutEffect cleanup 호출 → DOM에서 제거
    • Placement: 새 노드 attach
    • Update: prop 변경 적용
    • 이 phase 종료 시점이 정확히 "currentworkInProgress 스왑이 일어나는 순간"
  2. commitLayoutEffects — DOM이 update된 직후, 브라우저가 paint하기 전:

    • useLayoutEffect body 실행
    • componentDidMount/componentDidUpdate
    • ref attach/detach (useRef는 mutation phase에서 처리, callback ref는 layout)
    • 동기. 여기서 setState하면 추가 render가 같은 commit batch 안에서 일어남.
  3. flushPassiveEffects다음 microtask 또는 idle 시점:

    • useEffect cleanup → body 순서로 실행
    • useInsertionEffect도 여기 (정확히는 더 일찍이지만 같은 phase 계열)

왜 이 순서인가

useLayoutEffect가 paint 전에 실행되는 이유 = layout측정 후 추가 setState로 paint를 보정할 수 있어야 하기 때문. useEffect가 늦게 실행되는 이유 = layout/paint를 blocking하지 않기 위함. 18에서 추가된 useInsertionEffect는 layout effect보다도 더 일찍 — <style> 주입이 layout 측정을 깨지 않게.


10. Hooks는 Fiber에 Linked List로 저장

react-main/packages/react-reconciler/src/ReactFiberHooks.js:194-200

export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};

함수형 컴포넌트의 fiber.memoizedState첫 번째 hook을 가리킨다. 그 hook의 next가 다음 hook, ... 이렇게 singly linked list.

fiber.memoizedState
   useState ─next─► useEffect ─next─► useState ─next─► null
       │              │                  │
   {state, queue}  {effect, deps}    {state, queue}

함의

  • Hook 순서는 절대 바뀌면 안 된다 — index가 아니라 호출 순서에 의존. 첫 번째 hook은 list의 첫 노드, ... 만약 조건문 안에서 hook을 호출해 순서가 달라지면 어떤 fiber에선 useState 자리에 useEffect의 state가 매핑되어 버린다. React가 lint로 막는 이유.
  • Hook은 fiber 안에 산다 — 컴포넌트 인스턴스가 곧 fiber. 그래서 같은 컴포넌트도 두 번 마운트되면 별개의 hook chain을 가진다.
  • Effect도 hook: Hook.memoizedStateEffect 객체가 들어 있고, 효과 chain은 별도로 fiber.updateQueue에도 연결. Commit phase가 그걸 순서대로 실행.

react-main/packages/react-reconciler/src/ReactFiberHooks.js:220-226

export type Effect = {
  tag: HookFlags,
  inst: EffectInstance,
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null,
  next: Effect,
};

Effect.tag 비트로 Layout/Passive/Insertion 구분. 같은 commit phase 코드가 비트마스크 하나로 어떤 effect 종류를 실행할지 결정.


11. Lanes와 Fiber

react-main/packages/react-reconciler/src/ReactFiberLane.js:47-107

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010;
...
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000;
...
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000100000;
...
export const IdleLane: Lane = /*                        */ 0b0010000000000000000000000000000;

각 fiber는 두 lane 마스크:

  • lanes: 이 fiber 자체에 pending 업데이트들의 lane 합
  • childLanes: subtree 안 어딘가에 pending 업데이트의 lane 합 — completeWorkbubbleProperties가 채움

Work loop이 lane을 골랐을 때, 그 lane이 fiber.lanes에도 childLanes에도 없으면 그 subtree 전체 skipattemptEarlyBailoutIfNoScheduledUpdate. 이게 React가 "내가 지금 처리하는 priority와 무관한 fiber는 안 봐도 됨"을 결정하는 메커니즘.

Concurrent 렌더링이 fiber 위에서 동작하는 그림

1. 이벤트 발생 → setState 호출
2. fiber.lanes에 lane 추가 + 조상들의 childLanes에 OR
3. ensureRootIsScheduled → microtask
4. processRootScheduleInMicrotask → getNextLanes로 가장 높은 lane 선택
5. renderRootConcurrent:
   - root에서 시작해 workInProgress = createWorkInProgress(root.current, ...)
   - work loop이 fiber 단위로 beginWork → 자식 → completeWork
   - workLoopConcurrent는 매 25ms/shouldYield 마다 양보 가능
6. yield 동안 더 급한 update가 들어오면:
   - workInProgressRoot, workInProgressRootRenderLanes 비교
   - stale → prepareFreshStack으로 새로 시작 (abandon)
7. complete → commit (mutation → layout → passive)
8. root.current = workInProgress

위 모든 단계가 Fiber의 트리 구조와 alternate 포인터, lane 비트마스크 위에서만 가능. Fiber 없이는 어느 한 줄도 동작 안 함.


12. Suspense / Error Boundary가 Fiber 단위로 작동하는 모습

Suspense

컴포넌트가 promise를 throw하면, React는 work loop에서 catch한다. 그 다음:

  1. 가장 가까운 Suspense boundary fiberreturn 체인 따라 위로 찾아 올라감
  2. 그 fiber에 DidCapture flag 설정
  3. 이번 렌더에선 그 boundary의 fallback을 child로 reconcile
  4. promise resolve 시 retry — 그 boundary부터 다시 render (트리 전체 재시작 X)

이게 가능한 이유는 fiber tree에 부모 포인터(return)가 있어서 boundary를 빠르게 찾을 수 있고, boundary가 자기 fallback과 primary children을 모두 알고 있기 때문.

Error Boundary

같은 메커니즘. componentDidCatch / getDerivedStateFromError가 정의된 fiber를 return 체인으로 찾아 그 boundary에 에러를 위임. 자식 트리는 unmount.

둘 다 fiber tree의 위상적(topological) 구조 위에서 동작 — 트리 walking + flag set이 핵심 메커니즘.


13. 큰 그림 — 한 setState가 commit까지 가는 모습

[1] setState 호출
     │  scheduleUpdateOnFiber(fiber, lane)
[2] fiber.lanes |= lane
    return 체인으로 올라가며 조상의 childLanes |= lane
[3] ensureRootIsScheduled(root)
     │  scheduleMicrotask(processRootScheduleInMicrotask)
[4] microtask: getNextLanes로 우선순위 결정
[5] renderRootConcurrent(root, lanes):
     │  workInProgress = createWorkInProgress(root.current)
[6] WORK LOOP (Render Phase):
     │  while (workInProgress !== null && !shouldYield())
     │     performUnitOfWork(workInProgress):
     │       beginWork(current, workInProgress):    [내려가는 phase]
     │         - props 비교, bailout 결정
     │         - tag별 처리 (FC면 함수 실행, HC면 children 채택)
     │         - reconcileChildren → 자식 fiber linked list 생성
     │       if (next === null):
     │         completeUnitOfWork(workInProgress):  [올라오는 phase]
     │           completeWork:
     │             - HostComponent면 DOM instance 생성/diff
     │             - bubbleProperties로 subtreeFlags·childLanes 합산
     │           sibling 있으면 거기로, 없으면 return으로 올라감
[7] root에 도달 → workInProgress 트리 완성
[8] COMMIT PHASE (atomic):
     │  commitMutationEffects:    DOM 변경 적용
     │   ├ deletion: lifecycle cleanup + DOM remove
     │   ├ placement: DOM attach
     │   └ update: prop diff 적용
     │  *** root.current = workInProgress ***  ← 트리 스왑 한 줄
     │  commitLayoutEffects:      paint 전, 동기
     │   ├ useLayoutEffect body
     │   └ componentDidMount/Update, ref attach
     │  flushPassiveEffects:      microtask/idle, 비동기
     │   ├ useEffect cleanup
     │   └ useEffect body
[9] 끝. 다음 update를 기다림.

매 단계가 fiber 노드 구조의 어떤 필드를 읽고 쓰는지가 명확하다. Fiber는 단순한 자료구조가 아니라 React의 실행 의미론(operational semantics) 그 자체다.


14. 결론 — Fiber는 모든 신기능의 전제

기능Fiber의 어떤 측면에 의존
Concurrent Rendering단방향 linked list tree + workInProgress 포인터 1개로 중단점 표현
Lane 우선순위fiber.lanes/childLanes bitmask + work loop의 lane 선택
Double Bufferingalternate 포인터, "렌더 중에도 화면은 멀쩡"
Bailout 최적화props reference 비교 + childLanes skip
Suspensereturn 체인 walking + boundary fiber에 DidCapture flag
Error Boundary같은 메커니즘 — 트리 위상 walking
Selective HydrationSuspense boundary 단위 fiber 트리 부분 hydration
Hooksfiber.memoizedState의 singly linked Hook chain
Commit 3-phaseflags/subtreeFlags bitmask + MutationMask/LayoutMask/PassiveMask
RSC서버에서 만든 element 트리가 클라이언트에서 fiber 트리로 reconcile

한 줄 요약: Fiber는 "reconciliation 상태를 JS call stack에서 빼내어 명시적·중단가능·우선순위가 있는 자료구조로 옮긴 것" 이며, 이 결정 이후의 React 진화(16~19)는 거의 전부 Fiber의 어떤 측면을 더 활용하는 방향이었다.