Skip to content

Instantly share code, notes, and snippets.

@easylogic
Created November 19, 2025 13:38
Show Gist options
  • Select an option

  • Save easylogic/b87d1863483e166930a3c0ddb494d6ca to your computer and use it in GitHub Desktop.

Select an option

Save easylogic/b87d1863483e166930a3c0ddb494d6ca to your computer and use it in GitHub Desktop.
react-style-reconciliation

React-style Reconciliation 요약

1. 전체 흐름

단계 Render Phase (renderFiberNode) Commit Phase (commitFiberNode) 결과
DOM 준비 Fiber 트리를 순회하며 Text/Host DOM 생성만 수행 (부모에 삽입하지 않음) effectTag(PLACEMENT/UPDATE/DELETION)에 따라 실제 DOM 삽입/재배치/제거 React completeWork + commitMutationEffects와 동일
순서 결정 Fiber child/sibling 링크만 구성 getHostSibling으로 다음 형제 DOM 찾은 뒤 insertBefore DOM 순서 안정
속성/스타일 설정하지 않음 dom.updateAttributes / dom.updateStyles가 diff를 계산해 Insert/Update/Delete 수행 React commitUpdate와 동일
텍스트 Text node 생성만 handleVNodeTextProperty가 최종 텍스트 커밋 Text-only VNode 처리

1-1. 단계별 다이어그램 (Mermaid)

flowchart LR
  A["VNode 입력<br/>(prev / next)"]
  B["Render Phase<br/>(renderFiberNode)"]
  C["DOM 생성/재사용만<br/>(삽입 X, attrs X)"]
  D["effectTag 결정<br/>(PLACEMENT / UPDATE / DELETION)"]
  E["Commit Phase<br/>(commitFiberNode / commitFiberTree)"]
  F["CL: effectTag === PLACEMENT?<br/>→ getHostSibling & insertBefore"]
  G["속성/스타일 diff & 커밋<br/>(dom.updateAttributes / updateStyles)"]
  H["텍스트 처리<br/>(handleVNodeTextProperty)"]
  I["최종 DOM 반영 완료"]

  A --> B --> C --> D --> E --> F --> G --> H --> I
Loading

1-2. VNode 비교/ID 전파 흐름

flowchart LR
  subgraph Prev_Render
    P1["VNode_prev (sid/key)"] -->|metaDomElement| P2[DOM_prev]
  end
  subgraph Next_Render
    N1[VNode_next] -->|transferVNodeIdFromPrev| N2["VNode_next + sid"]
    N2 -->|generateVNodeIdIfNeeded fallback| N3["VNode_next stable ID"]
  end
  P2 -. reused DOM .-> N3
  N3 -->|fiber.domElement| RenderPhase
  RenderPhase -->|effectTag| CommitPhase
Loading
  • transferVNodeIdFromPrev: 이전 VNode의 sid/key를 새 VNode로 복사해 동일 개체임을 표시.
  • generateVNodeIdIfNeeded: sid/key가 없으면 컴포넌트(stype) → ComponentManager, 일반 Host → tag-index 기반 auto ID 생성 (DOM에는 노출되지 않음).
  • fiber.domElement가 prev DOM을 가리키게 되어 Commit Phase에서도 같은 DOM을 재사용할 수 있음.

2. Render Phase 상세

if (prevVNode?.meta?.domElement) {
  domElement = prevVNode.meta.domElement;
} else if (vnode.tag === '#text') {
  domElement = document.createTextNode(vnode.text);
} else if (vnode.tag) {
  domElement = dom.createSimpleElement(vnode.tag);
  if (vnode.sid && !isAutoGeneratedSid(vnode)) {
    dom.setAttribute(domElement, 'data-bc-sid', vnode.sid);
  }
  if (vnode.attrs) {
    for (const [key, value] of Object.entries(vnode.attrs)) {
      dom.setAttribute(domElement, key, String(value));
    }
  }
}
vnode.meta.domElement = domElement;
fiber.domElement = domElement;
  • DOM 생성 후 vnode.meta.domElement/fiber.domElement에 저장.
  • 부모에 삽입하거나 속성을 diff하지 않음.
  • Portal도 별도 FiberScheduler로 동일 흐름 수행.

3. Commit Phase 상세

if (fiber.effectTag === 'PLACEMENT') {
  const parent = fiber.parentFiber?.domElement ?? fiber.parent;
  let before = getHostSibling(fiber);
  if (before && before.parentNode !== parent) before = null;
  parent.insertBefore(domElement, before);
}

if (domElement instanceof HTMLElement) {
  dom.updateAttributes(domElement, prevVNode?.attrs, vnode.attrs);
  dom.updateStyles(domElement, prevVNode?.style, vnode.style);
}

if (domElement instanceof HTMLElement && vnode.text && !vnode.children) {
  handleVNodeTextProperty(domElement, vnode, prevVNode);
}
  • 삽입: Render Phase에서 준비된 DOM을 commit 시점에만 insertBefore.
  • 속성/스타일: React와 동일하게 diff → 부족분 삭제, 새 값 적용.
  • Deletion: prevVNode.meta.domElement 기반으로 DOM 제거 + component unmount.

4. getHostSibling (React 알고리즘)

let sibling = fiber.sibling;
while (sibling) {
  if (sibling.domElement) return sibling.domElement;
  if (sibling.child) {
    let child = sibling.child;
    while (child) {
      if (child.domElement) return child.domElement;
      child = child.child;
    }
  }
  sibling = sibling.sibling;
}
return null;
  • Render Phase에서 domElement를 미리 세팅했기 때문에 commit에서 바로 reference node 확보 가능.
  • reference node가 부모의 child가 아니면 null로 강등 → React와 동일한 append 동작.

5. 속성/스타일 Insert / Update / Delete

dom.updateAttributes

  • Prev attrs에 있지만 next attrs에 없는 키 → removeAttributeWithNamespace로 삭제.
  • next 값이 undefined/null → 삭제로 간주.
  • 나머지는 namespace-aware setAttributeWithNamespace 호출.

dom.updateStyles

  • Prev 스타일에 있지만 next에 없는 키 → style.removeProperty.
  • next 값이 undefined/nullremoveProperty.
  • 나머지는 style.setProperty로 업데이트.

6. 텍스트 처리

  • Text VNode는 Render Phase에서 Text 노드만 생성.
  • Commit Phase에서 handleTextOnlyVNode / handleVNodeTextProperty가 최종 텍스트 커밋 및 위치 이동을 담당.

7. Deletion 흐름

  • effectTag === 'DELETION'prevVNode.meta.domElement를 parent에서 제거.
  • Component VNode이면 components.unmountComponent 호출 후 DOM 제거.

8. React와의 대조표

React 개념 현재 구현
Fiber Render (beginWork/completeWork) renderFiberNode (DOM 생성만)
Effect flags fiber.effectTag (PLACEMENT/UPDATE/DELETION)
Effect list traversal commitFiberTree (child→sibling DFS)
commitPlacement insertBefore + getHostSibling
commitUpdate (attrs/styles) dom.updateAttributes / dom.updateStyles
getHostSibling 동일 알고리즘
namespace-aware attributes setAttributeWithNamespace / removeAttributeWithNamespace

결론: Render Phase에서 DOM을 준비하고, Commit Phase에서 삽입/속성/스타일/텍스트/삭제를 수행하는 React 흐름을 그대로 재현했습니다.

9. Child Matching Matrix

Fiber 생성 시 prev/next VNode 자식 매칭은 아래 우선순위로 진행된다. 각 단계에서 매칭되면 DOM을 재사용하고 effectTag = 'UPDATE', 끝까지 매칭되지 않으면 새 DOM을 만들고 effectTag = 'PLACEMENT'가 된다. 매칭에 실패한 prev VNode는 Commit Phase에서 removeStaleChildren이 제거한다.

우선순위 조건 매칭 기준 결과
1 getVNodeId(childVNode)가 truthy (sid, key, data-decorator-sid 등) prevVNode.children 중 동일 ID, 아직 매칭되지 않은 항목 검색 DOM/컴포넌트 완전 재사용. 위치가 달라도 현재 인덱스로 이동.
2 명시 ID 없음, transferVNodeIdFromPrev가 stype 기반 ID를 부여 prev 자식의 stype이 같고 ID가 있는 경우 ID를 복사 → 1단계와 동일하게 동작 컴포넌트/Decorator가 명시 ID 없이도 안정적으로 재사용됨.
3 ID 없음, 같은 인덱스에 prev 자식 존재 prev.children[i]tag와 현재 tag가 같거나 둘 다 텍스트 (#text) 동일 타입일 때만 DOM 재사용. 타입이 다르면 매칭 실패.
4 ID/인덱스 매칭 실패, 둘 다 Host VNode prev children 전체를 순회하며 tag + 클래스 조합이 같은 항목 탐색 (이미 매칭된 VNode 제외) mark/decorator wrapper처럼 ID가 없는 Host 노드도 안정적으로 재사용.
5 어느 조건에도 해당하지 않음 매칭 실패로 간주 Render Phase에서 새 DOM 생성, Commit Phase에서 기존 DOM 제거 후 삽입.

추가 규칙:

  • 텍스트 자식은 tag: '#text'로 통일해 3단계에서 타입 검사가 일관적으로 동작한다.
  • generateVNodeIdIfNeeded가 Host/Text VNode에 tag-index 기반 auto ID를 부여해 동일 구조가 반복될 때도 매칭이 안정적이다.
  • Render Phase는 매칭 결과만 계산하고 DOM 이동/삭제는 하지 않는다. Commit Phase에서 commitFiberTree가 effectTag에 따라 DOM을 삽입·업데이트·삭제하고, 마지막에 removeStaleChildren/processPrimitiveTextChildren가 남은 동기화를 수행한다.

10. Matching Flow Diagram

flowchart TD
  A[Next child VNode] --> B{getVNodeId 존재}
  B -->|Yes| C[prev.children 중 동일 ID & 미매칭 항목]
  C --> D[DOM 재사용<br/>effectTag = UPDATE]
  B -->|No| E{transferVNodeIdFromPrev 결과 ID}
  E -->|Yes| C
  E -->|No| F{같은 인덱스 prev child}
  F -->|Yes, tag 동일| D
  F -->|No| G{Host 구조 매칭}
  G -->|Yes| H[prev 전체 순회<br/>tag·class 동일 노드]
  H --> D
  G -->|No| I[새 Fiber/DOM 생성<br/>effectTag = PLACEMENT]
  D --> J[Commit Phase에서 기존 DOM 위치보정]
  I --> K[Commit Phase에서 새 DOM 삽입]
  J --> L[removeStaleChildren로 사용되지 않은 prev DOM 제거]
  K --> L
Loading

위 다이어그램은 Render Phase와 Commit Phase 간의 역할 분리를 시각화한다. Render Phase는 매칭 경로를 결정하고 fiber.effectTag만 설정하며, Commit Phase는 effectTag에 따라 DOM을 삽입하거나 재배치한 뒤 남은 prev DOM을 안전하게 제거한다.

11. Mount / Update / Unmount Timing

Render Phase에서는 오직 diff 계산과 effectTag 지정만 수행하고 어떤 DOM 조작도 하지 않는다. Mount, Update, Unmount는 모두 Commit Phase에서 effectTag별로 처리된다.

effectTag 시점 실행 함수 수행 작업
PLACEMENT Commit Phase의 commitFiberNodecommitPlacement 구간 insertBefore + getHostSibling 새 DOM 삽입, decorator/portal 포함
UPDATE Commit Phase의 commitFiberNode 본문 dom.updateAttributes, dom.updateStyles, 텍스트 갱신, 자식 Fiber parent 갱신 기존 DOM 유지한 채 속성·스타일·텍스트 diff 반영
DELETION Commit Phase의 commitFiberNode (vnode 없음) + 부모 removeStaleChildren components.unmountComponent, removeChild prev DOM 제거 및 컴포넌트 언마운트, primitive text 포함

11-1. Lifecycle Diagram

sequenceDiagram
  participant Scheduler as FiberScheduler
  participant Render as Render Phase<br/>(renderFiberNode)
  participant Commit as Commit Phase<br/>(commitFiberTree)
  participant DOM as 실제 DOM
  participant Components as ComponentManager

  Note over Scheduler: reconcileWithFiber 시작
  Scheduler->>Scheduler: scheduleWork(rootFiber)
  Scheduler->>Render: performUnitOfWork<br/>(Fiber DFS 순회)
  
  loop Render Phase (각 Fiber 노드)
    Render->>Render: transferVNodeIdFromPrev
    Render->>Render: generateVNodeIdIfNeeded
    Render->>Render: 타입 비교 (prevType vs nextType)
    Render->>Render: effectTag 결정<br/>(PLACEMENT/UPDATE)
    alt prevVNode.meta.domElement 존재 & 타입 동일
      Render->>Render: 기존 DOM 재사용
    else 새 DOM 생성 필요
      Render->>Render: createTextNode 또는<br/>createSimpleElement
      Render->>Render: attrs 설정 (DOM에 저장)
    end
    Render->>Render: vnode.meta.domElement 저장
    Render->>Render: fiber.domElement 저장
    Note over Render: DOM 조작 없음<br/>(생성만)
  end
  
  Scheduler->>Scheduler: Render Phase 완료
  Scheduler->>Commit: onCompleteCallback<br/>(commitFiberTree 호출)
  
  loop Commit Phase (각 Fiber 노드)
    Commit->>Commit: commitFiberNode (DFS 순회)
    alt effectTag = PLACEMENT
      Commit->>Commit: getHostSibling (referenceNode 찾기)
      Commit->>DOM: insertBefore (DOM 삽입)
      Note over Commit,DOM: mount (DOM에 추가)
    else effectTag = UPDATE
      Commit->>DOM: updateAttributes (diff 적용)
      Commit->>DOM: updateStyles (diff 적용)
      alt Text 노드
        Commit->>DOM: textContent 업데이트
      end
      Note over Commit,DOM: update (속성/스타일/텍스트 갱신)
    else effectTag = DELETION
      Commit->>Components: unmountComponent
      Commit->>DOM: removeChild
      Note over Commit,DOM: unmount (DOM 제거)
    end
  end
  
  Commit->>Commit: processPrimitiveTextChildren<br/>(primitive text 처리)
  Commit->>DOM: removeStaleChildren<br/>(사용되지 않은 DOM 제거)
  Commit->>Components: unmountComponent<br/>(stale 컴포넌트 언마운트)
Loading

위 시퀀스는 FiberScheduler가 Render Phase를 스케줄링하고, Render Phase가 effectTag만 계산하며, Commit Phase가 mount/update/unmount를 실제 DOM에 적용하는 전체 흐름을 보여준다.

주요 포인트:

  • FiberScheduler: reconcileWithFiber에서 생성되어 Render Phase를 비동기로 스케줄링 (테스트 환경에서는 동기 모드)
  • Render Phase: DOM 생성만 수행하고 삽입/수정은 하지 않음. mountComponent/updateComponent는 호출하지 않음 (향후 추가 예정)
  • Commit Phase: effectTag에 따라 실제 DOM 조작 수행. unmountComponent만 호출 (삭제 시)

11-2. Component Lifecycle (향후 구현 예정)

현재 Fiber reconciler에서는 mountComponent/updateComponent를 호출하지 않지만, 기존 reconciler의 동작을 참고하면 다음과 같이 구현될 예정이다:

mountComponent 호출 시점:

  • createHostElement에서 새 Host element 생성 후 (기존 reconciler)
  • Fiber reconciler에서는 commitFiberNodePLACEMENT 구간에서 호출 예정

updateComponent 호출 시점:

  • updateHostElement에서 기존 Host element 업데이트 시 (기존 reconciler)
  • Fiber reconciler에서는 commitFiberNodeUPDATE 구간에서 호출 예정
  • 호출 시 전달되는 변경 사항:
    components.updateComponent(prevVNode, nextVNode, host, context);
    // 내부에서 다음 항목들이 업데이트됨:
    // - instance.props = nextSanitizedProps (prevVNode.props와 diff)
    // - instance.model = nextModelData (prevVNode.model과 diff)
    // - instance.decorators = nextDecorators (prevVNode.decorators와 diff)
    // - instance.vnode = nextVNode
    // - component.update(host, prevVNode.props, nextVNode.props) (props 변경 시)

unmountComponent 호출 시점:

  • commitFiberNode에서 effectTag === 'DELETION'일 때
  • removeStaleChildren에서 사용되지 않은 컴포넌트 제거 시
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment