들어가기 전 - Fiber의 등장
fiber는 일반적인 자바스크립트 객체지만, 본질적으로는 UI 상태를 값으로 표현한 구조체다.
React는 이 Fiber 노드들을 연결해 렌더 트리(가상 DOM 트리)를 구성하고, 이를 실제 DOM과 비교(diff)하여 변경사항만을 반영하는 방식으로 렌더링 효율을 높인다.
React 공식 문서에서는 렌더링을 “상태 업데이트가 발생한 컴포넌트를 재귀적으로 호출하며 변경 사항을 계산하는 과정”이라고 설명한다.
하지만 나는 이 설명이 너무 추상적어서 이해하기 어려웠다..
『모던 React 딥 다이브』도 참고해봤지만, 내부 구조를 모른 채 개념만 다루다 보니 오히려 더 혼란스러웠다.
그래서, 직접 React 코드를 뜯어보기로 결심했다. 🫠
FiberNode 구조
function FiberNode(
this: $FlowFixMe,
tag: WorkTag, // 어떤 타입의 컴포넌트인지 (예: FunctionComponent 등)
pendingProps: mixed, // 새로 렌더링에 사용될 props
key: null | string, // 리스트 렌더링 등에서 사용하는 key
mode: TypeOfMode, // concurrent 모드인지 등의 설정
) {
// 실제 인스턴스 정보 (예: DOM 노드나 클래스 인스턴스)
this.tag = tag;
this.key = key;
this.elementType = null; // JSX 타입 그대로 유지
this.type = null; // 실제 타입 (함수, 클래스, 문자열 등)
this.stateNode = null; // 실제 DOM 노드 또는 클래스 인스턴스
// 트리 구조 정보
this.return = null; // 부모 Fiber
this.child = null; // 첫 번째 자식 Fiber
this.sibling = null; // 다음 형제 Fiber
this.index = 0; // 위치 인덱스 (배열 렌더링 시)
// ref 관련 정보
this.ref = null;
this.refCleanup = null;
// 상태 및 props 관련
this.pendingProps = pendingProps; // 새로 적용될 props
this.memoizedProps = null; // 이전에 렌더에 사용된 props
this.updateQueue = null; // 상태 업데이트 큐
this.memoizedState = null; // 이전 렌더링 시의 state
this.dependencies = null; // context 등의 의존성
this.mode = mode; // Fiber 모드 (StrictMode 등)
// 커밋 단계에서 필요한 변경 정보
this.flags = NoFlags; // 현재 노드의 변경 사항 플래그
this.subtreeFlags = NoFlags; // 자식 노드들의 변경 사항 플래그
this.deletions = null; // 삭제될 자식들
// 스케줄링 관련
this.lanes = NoLanes; // 이 노드의 작업 우선순위
this.childLanes = NoLanes; // 자식 트리 내 작업 우선순위
// 이전 렌더 트리의 Fiber
this.alternate = null;
// 개발/성능 측정용 필드
if (enableProfilerTimer) {
// ...
}
// 디버깅용 정보 (개발 모드에서만 사용)
if (__DEV__) {
// ...
}
}
(코드로 바로바로 보는 게 보기 편할 것 같아서 React 코드를 가져와 한글로 주석을 달아 봤다.)
React 렌더링 프로세스에서 가장 중요한 Fiber 노드의 구성이다.
React v15까지는 렌더링이 동기적으로 이루어졌는데, Fiber 아키텍처가 도입되면서 우선순위와 같은 세밀한 제어도 가능하게 됐다. 비동기적으로 처리할 수 있게 된 것이다.
오늘은 이러한 Fiber와 동기 렌더링 프로세스를 이해해볼 예정이므로, 나머지 dev 모드나 profiler 등등의 로직이나 속성들은 조금 흐린 눈 해볼.. 생각이다.
렌더의 의미와 fiber 구조의 관계
앞서 React 공식문서에서 "렌더링은 상태가 바뀐 컴포넌트를 차례로 호출하고, 그에 따른 변경사항을 계산하는 과정"이라고 설명한다고 하였다.
그런데 어떻게 컴포넌트를 "차례로 호출"할 수 있을까?
this.return = null; // 부모 Fiber
this.child = null; // 첫 번째 자식 Fiber
this.sibling = null; // 다음 형제 Fiber
this.index = 0; // 위치 인덱스 (배열 렌더링 시)
각 Fiber는 return, child, sibling 포인터를 통해 트리 탐색이 가능하게 되어 있다.
추후에 더 자세히 설명하겠지만, child를 따라 탐색하며 컴포넌트를 호출하고, child가 없으면 sibling, 더 이상 없으면 다시 부모 노드로 return 된다.
이때 차례로 호출하는 과정 중에 변경 사항이 있는 경우에만 업데이트 과정을 수행하고, 없는 경우에는 바로 return 되어 최적화한다.
추상화 1) Fiber 트리 : current와 WorkInProgess
먼저 바로 코드에 들어가보기 전, React에서는 두 개의 Fiber 트리를 두고 렌더 작업을 진행한다는 것(= 더블 버퍼링)을 미리 알면 더 좋을 것 같다.
위의 Fiber 노드들로 트리가 구성되는데,
Fiber 트리에는 두 종류가 있다. current , WorkInProgress
- current 는 현재 화면에 반영된 렌더 트리
- WorkInProgress 는 다음 렌더링을 위해 현재 작업 중인 트리다.
렌더링이 트리거 되면 current 트리를 기반으로 WorkInProgress 트리를 생성한다.
그런데 재밌는 점은 이 두 가지의 fiber 트리를 계속해서 재사용 된다는 것이다. (최적화)
어떻게 재사용이 가능한가 하면,
- 렌더링이 발생하면 current를 바탕으로 workInProgress를 생성한다.
- 렌더링과 커밋이 끝나면, workInProgress는 새로운 current 가 된다.
- 다음 렌더링 때는 이 current가 다시 workInProgress를 만드는 데 사용된다.
⇒ 한 번 만들고 끝이 아니라, 커밋 단계까지 끝내고 나면 WorkInProgress 트리가 current 트리가 된다.
이전의 current 트리는 다음 렌더링 시에 재사용 한다.
이를 가능하도록 연결해두는 것이 alternate 다.
// 이전 렌더 트리의 Fiber
this.alternate = null;
앞선 fiber 노드의 요 alternate 는 바로 fiber 노드를 재사용하기 위해 연결해 놓는 속성인 것이다.
추상화 2) Fiber 처리 흐름
[추상화 1] 의 내용을 요약하면,
- 위의 파이버 객체가 모여 트리를 구성한다.
- 파이버에는 두 개의 트리 구조가 있는데, 트리를 사용해 현재 구조(current)와 렌더링 되고 있는 구조(WorkInProgess)를 관리해, 파이버 트리를 갱신한다. (파이버는 재사용된다!)
- WorkInProgress의 변경사항이 반영되고 커밋되면, WorkInProgess가 current 가 된다.
이제 이 트리를 사용해서 어떻게 렌더가 수행되는지 큰 그림을 그려 보면 아래와 같다.
파이버 작업 순서
<A1>
<B1>안녕하세요</B1>
<B2>
<C1>
<D1 />
<D2 />
</C1>
</B2>
<B3 />
</A1>
<모던 리액트 딥 다이브> 141쪽의 예제이다.
- 흐름
beginWork(A1)
beginWork(B1) → 자식 없음 → completeWork(B1)
beginWork(B2)
beginWork(C1)
beginWork(D1) → 자식 없음 → completeWork(D1)
beginWork(D2) → 자식 없음 → completeWork(D2)
completeWork(C1)
completeWork(B2)
beginWork(B3) → 자식 없음 → completeWork(B3)
completeWork(A1)
commitWork() 수행
- React는 beginWork() 함수를 실행해 fiber를 수행
- 자식이 없는 fiber를 만나면 completeWork() 실행해 fiber 작업 완료
- 이후 형제가 있다면 형제로 넘어가 beginWork() 진행
- 형제까지 모두 completeWork() 로 끝났다면 부모 노드로 돌아가 completeWork()
- 최종적으로는 commitWork()가 수행
- 이때 변경 사항을 비교해 변경된 부분만 DOM 에 반영
먼저 바로 코드로 들어가기 보다는 이해하기 수월하도록 어려운 개념들을 추상화해보았다.
이제 본격적으로 코드를 뜯어보려고 한다.
state 등이 변경되어 리렌더링을 트리거 하는 경우, 어떻게 동기적 렌더링이 발생하고 실제 DOM에 반영하게 되는지 그 프로세스를 뜯어보자!
renderRootSync, renderRootConcurrent
렌더링 작업 전체 사이클을 관리하며, 각각 동기/동시 모드 렌더링을 수행한다.
이 부분 눈빠질 것 같았지만😂 멘탈을 잡고 최대한 필요한 부분만 보고자 했다..
- renderRootSync : 동기 방식으로 한 번에 끝까지 렌더를 수행하는 함수 (중단 x)
- renderRootConcurrent : 작업을 쪼개 렌더 수행 (중단 가능)
두 가지가 모두 사용되며 각 상황에 따라 다르게 사용한다고 한다.
오늘은 renderRootSync 동기적 렌더 & 커밋 과정에 대해 정리해보았다.
renderRootSync
renderRootSync : 전체 렌더 흐름 관리
function renderRootSync(
root: FiberRoot,
lanes: Lanes,
shouldYieldForPrerendering: boolean,
): RootExitStatus {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
// 디스패처 세팅
// ...
// fresh stack 준비
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// ...
prepareFreshStack(root, lanes);
}
// profiler 시작
// ...
let didSuspendInShell = false;
let exitStatus = workInProgressRootExitStatus;
outer: do {
try {
if (
workInProgressSuspendedReason !== NotSuspended &&
workInProgress !== null
) {
// Suspense로 인해 중단된 경우 처리
// ...
throwAndUnwindWorkLoop(...);
if (shouldYieldForPrerendering && workInProgressRootIsPrerendering) {
exitStatus = RootInProgress;
break outer;
}
break;
}
// Fiber 순회하며 렌더링 수행
workLoopSync();
exitStatus = workInProgressRootExitStatus;
break;
} catch (thrownValue) {
// 예외 처리
handleThrow(root, thrownValue);
}
} while (true);
// shell에서 suspend되었는지 체크
if (didSuspendInShell) {
root.shellSuspendCounter++;
}
// 컨텍스트 및 디스패처 정리
// ...
// profiler 종료
// ...
if (workInProgress !== null) {
// 트리 완성 못함 (중단된 상태)
// ...
} else {
// 트리 완성됨 → 작업 정리
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
finishQueueingConcurrentUpdates();
}
return exitStatus;
}
먼저 prepareFreshStack 통해 WorkInProgress 트리를 생성한다.
또, workLoopSync()를 통해 이 생성된 트리를 순회하며 동기적으로 렌더링을 수행한다는 것을 확인하면 좋을 것 같다.
prepareFreshStack
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
// profiler 타이머 및 성능 트래킹 관련 처리 (생략)
if (enableProfilerTimer && enableComponentPerformanceTrack) {
// ...
}
// 이전에 설정된 timeout, 커밋 취소 함수 제거
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
root.timeoutHandle = noTimeout;
cancelTimeout(timeoutHandle);
}
const cancelPendingCommit = root.cancelPendingCommit;
if (cancelPendingCommit !== null) {
root.cancelPendingCommit = null;
cancelPendingCommit();
}
// WIP stack 초기화 및 설정
resetWorkInProgressStack();
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = lanes;
workInProgressSuspendedReason = NotSuspended;
workInProgressThrownValue = null;
workInProgressRootDidSkipSuspendedSiblings = false;
workInProgressRootIsPrerendering = checkIfRootIsPrerendering(root, lanes);
workInProgressRootDidAttachPingListener = false;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressDeferredLane = NoLane;
workInProgressSuspendedRetryLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
workInProgressRootDidIncludeRecursiveRenderUpdate = false;
// 얽힌 lanes (entangled lanes) 설정
entangledRenderLanes = getEntangledLanes(root, lanes);
// 병렬 업데이트 큐 처리
finishQueueingConcurrentUpdates();
if (__DEV__) {
resetOwnerStackLimit();
ReactStrictModeWarnings.discardPendingWarnings();
}
return rootWorkInProgress;
}
이 함수에서는 workInProgess 초기화 및 이전 렌더 상태 정리 등의 로직을 담당한다.
createWorkInProgress라는 함수를 호출하는데, 이에 대해선 밑에서 더 자세히 살펴보려고 한다.
createWorkInProgress
이제 current 트리를 기반으로 렌더 작업을 수행하기 위해 workInProgress트리를 어떻게 생성하는지 살펴보자.
current 트리의 fiber 노드를 기반으로 WorkInProgess 트리의 fiber 노드를 생성하는 함수다.
이제 중요한 부분을 하나씩 살펴보자.
1.current는 건들지 않고 이전 작업 트리에서 노드를 가져온다.
let workInProgress = current.alternate;
앞서 alternate가 두 트리를 연결하는 속성이라고 했었다.
current 트리는 건들지 않고 workInProgress 트리를 생성하기 위해, 놀고 있는 다른 fiber 노드를 재사용한다.
2. (alternate가 없다면) 새로 생성한다.
if (workInProgress === null) {
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
if (__DEV__) {
// DEV-only fields
// ...
}
workInProgress.alternate = current;
current.alternate = workInProgress;
}
createFiber 함수를 통해 fiber 노드를 새로 생성한다.
그리고 추후 재사용을 위해 서로 연결해주는 것도 잊지 않는다. (alternate)
3. (재사용 했다면)
else {
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
if (enableProfilerTimer) {
workInProgress.actualDuration = -0;
workInProgress.actualStartTime = -1.1;
}
}
fiber 노드를 재사용했다면, 이전 렌더에서 남아 있는 값들이 다음 렌더에 영향을 주지 않도록 일부 필드를 명시적으로 초기화한다.
4. current fiber 노드의 일부 속성 그대로 복사
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
- childLanes, lanes: 업데이트 우선순위 관련 정보
- child, memoizedProps, memoizedState, updateQueue: 메모이제이션된 정보들. 다음 렌더에서 비교(diff) 시 필요하므로 그대로 복사.
5. dependency 얕은 복사
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: __DEV__
? {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
_debugThenableState: currentDependencies._debugThenableState,
}
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
렌더링 중 dependencies는 변경되므로, current와 공유 되면 안된다고 한다. (버그 발생 가능성)
따라서 상수 currentDependencies 로 얕은 복사해 WorkInProgress의 dependencies에 넣어준다.
그리고 개발환경일 경우는 _debugThenableState 라는 속성이 또 따로 있는데, 디버깅에 도움이 되는 것이라고 한다 ..
6. 트리 구조 복사
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
workInProgress.refCleanup = current.refCleanup;
그리고 current의 트리 구조를 그대로 복사한다.
- sibling : 다음 형제 노드 참조
- index : 현재 노드가 부모의 자식 노드들 중 몇 번째인지
- ref : react ref 객체
- refCleanup : ref 해제 시점에 호출할 정리 함수 복사
workLoopSync()
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
해당 함수에서는 작업을 수행하기 위해 루프를 돈다. (루프인 것으로 보아 동기적 렌더임을 다시 확인할 수 있다.)
performUnitOfWork
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
// profiler 관련
// ...
} else {
if (__DEV__) {
// dev 모드 관련
// ...
} else {
next = beginWork(current, unitOfWork, entangledRenderLanes);
}
}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
}
한 개의 fiber 노드 단위로 작업을 수행하는 함수다.
beginWork를 통해 다음 작업을 수행한다.
Fiber에서 사용된 props를 memoziedProps에 저장한다.
이후 다음 자식이 없다면 completeUnitOfWork 를 수행하며 마친다.
자식이 있다면 다음 작업 대상으로 이동한다. (루프이므로)
이제 beginWork와 CompleteUnitOfWork에 대해서 살펴보자.
beginWork
이 함수도 렌더링 프로세스를 이해하기 위한 부분만 보면,
if (current !== null) {
// 이미 렌더된 적 있음 → 업데이트 여부 판단으로 이어짐
...
} else {
// current === null → 초기 렌더
didReceiveUpdate = false;
// ...
}
초기 렌더 여부를 위의 코드에서 확인해 분기한다. 우리는 리렌더링 과정을 살펴보고 있으므로 업데이트 여부 판단 부분만 더 자세히 살펴보겠다.
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 업데이트 필요
didReceiveUpdate = true;
} else {
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,
);
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// 강제적 업데이트 필요
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
}
current fiber(이전 렌더 결과의 fiber)가 존재할 때, 이번 렌더링에서 업데이트가 필요한지 판단한다.
업데이트가 필요하지 않은 경우는 bail out 될 수 있다.
bail out 되면 그 자식 노드들은 모두 건너 뛰게 되어, 최적화 된다.
switch (workInProgress.tag) {
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
renderLanes,
);
}
case FunctionComponent: {
const Component = workInProgress.type;
return updateFunctionComponent(
current,
workInProgress,
Component,
workInProgress.pendingProps,
renderLanes,
);
}
// 그 외 다양한 case
// ...
case Throw: {
// This represents a Component that threw in the reconciliation phase.
// So we'll rethrow here. This might be a Thenable.
throw workInProgress.pendingProps;
}
}
throw new Error(
`Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
'React. Please file an issue.',
);
}
export {beginWork};
그리고 변경사항이 있는 fiber 노드들은 위의 switch 문을 타고 fiber를 업데이트 하는 로직을 따른다.
updateFunctionComponent
//...
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
다양한 업데이트 로직이 있지만 함수형 컴포넌트 업데이트 로직을 일부 보면
reconcileChildren 내부에서 아래와 같은 동작을 한다. (내부 함수 호출 통해서 ..)
- 이전 children(Fiber)과 새로운 children(ReactElement)을 비교
- key + type 비교 → 재사용 여부 결정
- 재사용이 불가능한 경우 새로운 Fiber 생성
이렇게 생성된 child 노드를 return하면 이전 코드에서 등장했던, 아래와 같이 다음 순회 대상이 된다.
next = beginWork(current, unitOfWork, entangledRenderLanes);
이 부분에 child 노드가 들어가게 된다.
자식 노드가 있을 때까지는 계속 탐색한다는 것이다.
참고로 bail out된 노드는 여기서 next가 null일 것이다.
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
그리고 next가 null이라면 더 이상 탐색할 child가 없다는 뜻이므로 completeUnitOfWork를 수행한다.
completeUnitOfWork
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork: Fiber = unitOfWork;
do {
// ...
// ...
if (__DEV__) {
// ... (개발 모드)
} else {
next = completeWork(current, completedWork, entangledRenderLanes);
}
if (next !== null) {
workInProgress = next; // 새로운 일 있으면 다시 beginWork
return;
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
// ...
}
completeUnitOfWork는 DFS 탐색에서 더 이상 자식 노드가 없어, 자식부터 부모 방향으로 올라오면서 남은 일을 처리하는 함수다.
- siblingFiber (형제 노드)가 존재 하면 해당 노드로 이동하고,
- 형제 노드가 없다면 부모 노드(returnFiber)로 이동한다.
이제 위의 코드에서 자식 노드가 더 이상 없어 수행하는 completeWork에 대해서 살펴본다.
completeWork
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
// 기본적으로 특별한 작업 없이 그대로 리턴
return null;
case HostComponent: {
if (current !== null && workInProgress.stateNode != null) {
// 기존 DOM이 있으면 props만 업데이트
// ... 생략
} else {
// 최초 마운트일 경우 DOM 노드 생성
const type = workInProgress.type;
const instance = createInstance(type, newProps, ...);
appendAllChildren(instance, workInProgress);
finalizeInitialChildren(instance, type, newProps, ...);
workInProgress.stateNode = instance;
}
return null;
}
case HostText: {
if (current === null) {
const text = newProps;
workInProgress.stateNode = createTextInstance(text, ...);
}
return null;
}
case HostRoot: {
// 루트 처리 (예: context, 업데이트 처리 등)
// ... 생략
return null;
}
case SuspenseComponent: {
// Suspense 처리
// ... 생략
return null;
}
// ... 기타 케이스 생략
}
}
completeWork 에서는 위와 같이 케이스 별로 변경사항을 Fiber에 반영해둔다.
이걸 실제 DOM에 반영하는 것은 커밋 단계에서 진행한다.
performSyncWorkOnRoot
function performSyncWorkOnRoot(root) {
const exitStatus = renderRootSync(root, SyncLane);
if (exitStatus === RootCompleted) {
// 렌더 성공했으니 커밋 단계로 넘어감
commitRoot(root);
} else {
// Suspense나 에러로 인한 중단 상태 처리
// 재시도하거나 fallback 처리 등
}
}
지금까지 봤던 renderRootSync는 exitStatus를 반환하고, 성공했다면 커밋 단계로 넘어가게 된다.
commitRoot
function commitRoot(root) {
const finishedWork = root.finishedWork; // 작업 완료된 Fiber 트리
const lanes = root.finishedLanes;
// 1. before mutation phase
commitBeforeMutationEffects(finishedWork);
// 2. mutation phase
commitMutationEffects(finishedWork, root, lanes);
// 3. layout phase
commitLayoutEffects(finishedWork, root, lanes);
// 4. 루트 상태 정리 및 현재 작업 중인 Fiber 업데이트
root.current = finishedWork; // 작업 완료 Fiber 트리를 현재 Fiber로 설정
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 5. 이후 스케줄링 등 후처리
}
커밋 단계에서는 실제 DOM 조작이 발생하고, layout effect를 실행 (예: useLayoutEffect 훅 실행)하고,
그리고 커밋 완료 후 current에 WorkInProgress를 할당한다.
이제 렌더와 커밋 과정을 거쳐 실제 DOM 반영까지 완료되었다.
정리
1. current fiber 트리를 기반으로 WorkInProgress 트리 생성하는데, 이때 fiber 노드들은 재사용될 수 있음 (alternate로 연결되어 있어서)
2. 생성 후 동기적 렌더 수행, WorkInProgress 트리를 순회하며 각 fiber의 업데이트 필요 여부 판단한다.
- 순회할 때 beginWork()에서 fiber 업데이트 필요 여부 판단해서 업데이트
- 업데이트 필요하다고 여겨지면? (ex. props가 바뀜, key가 바뀜)
- 컴포넌트 타입에 따라 알맞은 업데이트 함수 실행
- 컴포넌트 함수 실행 및 자식 요소들 반환, 자식요소들 또한 fiber 노드로 변환되어 workInProgress.child로 연결됨
- 기존 current.child와 비교하여 해당 자식 노드에 flags 설정 (Placement, Update, ...)
- 첫 자식 fiber가 반환되며, 다음 beginWork() 대상
- 업데이트 필요없다고 여겨진다면 bail out 되어 자식 노드들도 순회하지 않는다.
- 더 이상 순회할 자식 노드가 없다면 completeWork() 수행하며 fiber 노드 자체에 업데이트 정보를 저장해둠.
- completeWork 수행 후 형제 노드가 있다면 해당 노드로 beginWork() 수행해 처리
- 더 이상 처리할 노드가 또 없다면 부모 노드로 돌아가 completeWork()
- 최종적으로 commitWork 수행
3. 순회가 끝나면 커밋 단계에서는 변경 플래그를 기반으로 실제 DOM을 조작하거나, useLayoutEffect 같은 사이드이펙트를 실행, 최종적으로 current 트리를 WorkInProgress 트리로 스왑하며 렌더링이 완결된다.
처음으로 React 코드를 뜯어 봤는데, (사실 뜯어 봤다고 하기엔 띄엄띄엄 보긴 했지만 ..)
어려운 개념들의 경우 이렇게 직접 동작 원리를 체감해보는 게 이해하기에 빠른 것 같다.
사실 아무리 잘 추상화 해놓은 이론을 공부한다 해도, 숲만 보고 나무까지 모두 이해하기엔 어려우니까 ...
코드를 읽는 능력이 나에겐 조금 부족하지만, 그렇기에 더 필요한 학습이었다 생각한다.
아직 동기적 렌더링만 공부해봤는데, 추후에 fiber로 어떻게 비동기적으로 렌더 할 수 있는지도 공부해보면 좋을 것 같다.
+) 잘못된 내용이나 더 추가되어야 할 내용이 있다면 조언 부탁드려요 댓글 환영합니다ㅎ.ㅎ !!!!
참고자료
https://github.com/facebook/react/tree/main/packages/react-reconciler/src
'웹 > React' 카테고리의 다른 글
| [React] 좋아요 낙관적 업데이트 적용하기 (1) | 2025.07.21 |
|---|---|
| [React] memo, useCallback을 사용한 렌더링 최적화 (3) | 2025.07.08 |
| [코드 개선하기] try, catch vs React Query의 onSuccess, onError : 비동기 처리 로직 통일하기 (0) | 2025.06.25 |
| [React] 폰트 최적화 + preload 적용해 사용자 경험 개선하기 (0) | 2025.06.23 |
| [React, Vite, Vite-bundle-analyzer] CSR에서 초기 로딩 시간 단축하기 2 - 라이브러리 청크 단위로 정적 분리 (0) | 2025.06.03 |