| 단계 | 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 처리 |
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
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
transferVNodeIdFromPrev: 이전 VNode의 sid/key를 새 VNode로 복사해 동일 개체임을 표시.generateVNodeIdIfNeeded: sid/key가 없으면 컴포넌트(stype) → ComponentManager, 일반 Host →tag-index기반 auto ID 생성 (DOM에는 노출되지 않음).fiber.domElement가 prev DOM을 가리키게 되어 Commit Phase에서도 같은 DOM을 재사용할 수 있음.
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로 동일 흐름 수행.
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.
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 동작.
- Prev attrs에 있지만 next attrs에 없는 키 →
removeAttributeWithNamespace로 삭제. - next 값이
undefined/null→ 삭제로 간주. - 나머지는 namespace-aware
setAttributeWithNamespace호출.
- Prev 스타일에 있지만 next에 없는 키 →
style.removeProperty. - next 값이
undefined/null→removeProperty. - 나머지는
style.setProperty로 업데이트.
- Text VNode는 Render Phase에서
Text노드만 생성. - Commit Phase에서
handleTextOnlyVNode/handleVNodeTextProperty가 최종 텍스트 커밋 및 위치 이동을 담당.
effectTag === 'DELETION'→prevVNode.meta.domElement를 parent에서 제거.- Component VNode이면
components.unmountComponent호출 후 DOM 제거.
| 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 흐름을 그대로 재현했습니다.
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가 남은 동기화를 수행한다.
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
위 다이어그램은 Render Phase와 Commit Phase 간의 역할 분리를 시각화한다. Render Phase는 매칭 경로를 결정하고 fiber.effectTag만 설정하며, Commit Phase는 effectTag에 따라 DOM을 삽입하거나 재배치한 뒤 남은 prev DOM을 안전하게 제거한다.
Render Phase에서는 오직 diff 계산과 effectTag 지정만 수행하고 어떤 DOM 조작도 하지 않는다. Mount, Update, Unmount는 모두 Commit Phase에서 effectTag별로 처리된다.
| effectTag | 시점 | 실행 함수 | 수행 작업 |
|---|---|---|---|
PLACEMENT |
Commit Phase의 commitFiberNode → commitPlacement 구간 |
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 포함 |
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 컴포넌트 언마운트)
위 시퀀스는 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만 호출 (삭제 시)
현재 Fiber reconciler에서는 mountComponent/updateComponent를 호출하지 않지만, 기존 reconciler의 동작을 참고하면 다음과 같이 구현될 예정이다:
mountComponent 호출 시점:
createHostElement에서 새 Host element 생성 후 (기존 reconciler)- Fiber reconciler에서는
commitFiberNode의PLACEMENT구간에서 호출 예정
updateComponent 호출 시점:
updateHostElement에서 기존 Host element 업데이트 시 (기존 reconciler)- Fiber reconciler에서는
commitFiberNode의UPDATE구간에서 호출 예정 - 호출 시 전달되는 변경 사항:
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에서 사용되지 않은 컴포넌트 제거 시