TOC
- 들어가며
- 1. Fiber 아키텍처에 대한 간략한 소개
- 2. Trigger 단계의 최초 마운트
- 3. Render 단계의 최초 마운트
- 3.1. performConcurrentWorkOnRoot
- 3.2. renderRootSync
- 3.3. performUnitOfWork
- 3.4. prepareFreshStack
- 3.5 updateHostRoot
- 3.6. reconcileChildren
- 3.7. reconcileChildFibers vs mountChildFibers
- 3.8. reconcileSingleElement
- 3.9. placeSingleChild
- 3.10. mountIndeterminateComponent
- 3.11. updateHostComponent
- 3.12. updateHostTest
- 3.13. DOM 노드는
completeWork()
인 화면 외부에서 생성
- 4. Commit 단계에서의 최초 마운트
- 5. 요약
- 총평
- References
들어가며
아래부터는 내용을 요약한 부분입니다!
반말은 내용의 요약, 존댓말은 제가 작성한 내용입니다.
복습
- 이전 장 : overview
- React는 내부적으로 tree스러운 구조를 사용(Fiber Tree)
- WHY? Commit 단계에서 최소한의 DOM 업데이트와 commit을 계산하기 위해
이번 장에서는
- React가 최초 마운트를 어떻게 수행하는지 알아본다.
- 아래의 코드를 통해서 Deep Dive를 시작해보자.
import {useState} from 'react'
function Link() {
return <a href="https://jser.dev">jser.dev</a>
}
export default function App() {
const [count, setCount] = useState(0)
return (
<div>
<p>
<Link/>
<br/>
<button onClick={() => setCount(count => count + 1)}>click me - {count}</button>
</p>
</div>
);
}
1. Fiber 아키텍처에 대한 간략한 소개

Fiber란?
- React가 앱 상태를 내부적으로 표현하는 방식
FiberRootNode
와FiberNodes
으로 구성된 Tree 같은 구조- Fiber에는 모든 종류의 FiberNode가 있고, 이들 중 일부는 백업 DOM 노드인
HostComponent
를 가짐 - React 런타임은 Fiber Tree를 유지 및 업데이트, 그리고 최소한의 업데이트로 Host DOM과 동기화
1.1. FiberRootNode
- 전체 앱의 필수적인 메타정보를 가지는,
React root
처럼 동작하는 특별한 노드 - 이 노드의
current
는 실제 Fiber Tree를 가리킴 - 새로운 FiberTree가 생성될 때마다,
current
는 새로운HostRoot
를 가리킴
1.2. FiberNode
: FiberRootNode
를 제외한 모든 노드
몇 가지 중요한 속성
tag
tag
별로 구분되는 많은 하위 유형이 있음- FunctionComponent, HostRoot, ContextConsumer,MemoComponent,SuspenseComponent 등
stateNode
- 다른 백업 데이터를 가리킴
HostComponent
의 경우,stateNode
는 실제 백업 DOM 노드를 가리킴
child
,sibling
,return
: 함께 Tree 구조를 형성elementType
: 우리가 제공하는 컴포넌트 함수 or 고유 HTML 태그flags
- Commit 단계에서 적용할 업데이트를 나타냄
subtreeFlags
는flags
의 하위 트리
lanes
- 보류중인 업데이트들의 우선순위를 나타냄
childLanes
는lanes
의 하위 트리
memoizedState
- 중요한 데이터를 가리킴
- FunctionComponent의 경우 hook을 의미
2. Trigger 단계의 최초 마운트
createRoot()
는current
로 React root를 생성- React root : 더미 HostRoot FiberNode를 가짐
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
// 🗣️ 이 root가 FiberRootNode를 반환
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);
Dispatcher.current = ReactDOMClientDispatcher;
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root); // 🗣️ 이렇게
}
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: (error: mixed) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
containerInfo,
tag,
hydrate,
initialChildren,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
}
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onRecoverableError: null | ((error: mixed) => void),
transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
): any);
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(
tag,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
root.current = uninitializedFiber;
// 🗣️ HostRoot의 FiberNode가 생성됨 / React root의 current로 할당
uninitializedFiber.stateNode = root;
...
initializeUpdateQueue(uninitializedFiber);
return root;
}
root.render()
는 HostRoot의 업데이트를 예약- element의 argument는 update payload에 저장
function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
updateContainer(children, root, null, null);
};
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
const update = createUpdate(lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};
// render()의 argument는 update paylaod에 저장됨
const root = enqueueUpdate(current, update, lane);
// 그러면update가 queue에 삽입됨(enqueue)
// update가 처리되기 위해 대기한다는 것만 기억
if (root !== null) {
scheduleUpdateOnFiber(root, current, lane);
entangleTransitions(root, current, lane);
}
return lane;
}
3. Render 단계의 최초 마운트
3.1. performConcurrentWorkOnRoot
- 초기 렌더링과 리렌더링의 렌더링 시작 진입점(entry point)
concurrent
(동시성)이라고 이름 붙었지만, 내부적으로는 필요한 경우sync
모드로 돌아감- 최초 마운트도 이 중 하나이다. DefaultLane이 blocking lane이기 때문에!
function performConcurrentWorkOnRoot(root, didTimeout) {
...
// root에 저장된 필드를 사용하여 작업할 다음 lane을 결정한다.
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
...
// 몇몇 경우에는 time-slicing을 제한한다.
// 작업이 CPU를 오래 점유하는 경우 (기아 현상을 방지하기 위해)
// 또는 비동기식 업데이트를 기본으로 하는 경우
// TODO: 아직 조사중인 스케줄러 버그를 설명하기 위해 `didTimeout`만을 보수적으로 확인한다.
// 스케줄러의 버그가 수정되면, track의 만료를 직접 추적할 수 있기 때문에 이를 제거할 수 있다.
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
...
}
// 🗣️ blocking은 이 작업이 중요하며, 방해되면 안 된다는 것을 의미
export function includesBlockingLane(root: FiberRoot, lanes: Lanes) {
const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane;
// 🗣️ DefaultLane은 blocking lane
return (lanes & SyncDefaultLanes) !== NoLanes;
}
Lane에 대해 더 알고 싶다면 React에서 Lane은 무엇인가를 참고
- 위의 코드를 보면 최초 마운트는 동시성 모드를 사실상 사용하지 않음을 알 수 있다.
- 최초 마운트의 경우 최대한 빨리 UI에게 고통을 줘야 하며, 지연(defer)시키는 것은 도움이 되지 않는다.
저의 의역
: "최초 마운트는 블로킹을 만드므로 그냥 빨리 시작하고 마치는 게 좋다."
3.2. renderRootSync
renderRootSync()
는 단지 while 루프일 뿐이다.
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// root나 lanes가 변경된다면, 존재하는 stack에서 나와 새로운 stack에 쌓이게 준비한다.
// 그렇지 않으면 중단한 부분부터 계속 진행한다.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
const memoizedUpdaters = root.memoizedUpdaters;
if (memoizedUpdaters.size > 0) {
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// 여기서, 예정된 작업을 예약한 Fiber를 map에서 set으로 이동시킨다.
// 이 작업에서 비상탈출하게 된다면, 위와 같이 되돌리게 된다.
// 작업이 다른 업데이터를 통해 같은 우선순위의 더 많은 작업을 낳는 경우엔 당장 이동시키는 것이 중요하다.
// 이렇게 하면 현재 업데이트와 향후 업데이트를 분리하여 유지할 수 있다.
movePendingFibersToMemoized(root, lanes);
}
}
workInProgressTransitions = getTransitionsForLanes(root, lanes);
prepareFreshStack(root, lanes);
}
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
resetContextDependencies();
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
// 진행중인 렌더링이 없다는 것을 표시하기 위해 이것을 null로 설정
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
// 작업 루프는 극히 인기 있는 경로이다. 클로저에게 inline하지 말라고 말하라.
/** @noinline */
function workLoopSync() {
// 이미 시간이 초과되었으므로, 양보해야 하는지 확인하지 않고 작업을 수행한다.
while (workInProgress !== null) {
// 🗣️ 이 while 루프는 workInProgress가 있다면, performUnitOfWork()를 수행함을 의미한다.
performUnitOfWork(workInProgress);
// 🗣️ performUnitOfWork라는 이름처럼, Fiber Node의 개별 단위로 동작한다.
}
}
workInProgress
의 의미- React 코드 베이스에서
current
와workInProgress
의 접두사는 어디에나 있음 - React는 내부적으로 현재 상태를 표현하기 위해 Fiber Tree를 사용
- 때문에 업데이트가 있을 때마다 새로운 트리를 생성하고 이전 트리와 비교해야 한다.
- 그래서
current
는 UI에 그려진 현재 버전을 의미하고,workInProgress
는 빌드중이며, 다음current
로써 사용될 버전을 의미
3.3. performUnitOfWork
- React가 단일 Fiber Node에서 작동하여 완료되어야 하는 것들이 있는지 확인
- 이 구역을 더 쉽게 이해하고 싶다면 먼저 React는 Fiber Tree를 어떻게 순회하는가를 먼저 확인하는 걸 추천
저의 의견
짧으니까 참고하면 좋습니다. (아래 사진으로 한눈에 이해 가능)

function performUnitOfWork(unitOfWork: Fiber): void {
// 이 fiber의 현재 상태는 alternate이다.
// 이상적으로는 여기에 아무것도 의지하지 않지만, 여기에 의존한다는 것은
// 진행중인 작업에서 추가적인 field가 필요하지 않음을 의미한다.
const current = unitOfWork.alternate;
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 새로운 작업을 만들지 않는다면, 현재 작업을 완료한다.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
// 🗣️ 언급했듯이, workLoopSync()는 workInProgress에서 completeUnitOfWork()의 동작을 유지하기 위한 while 루프일 뿐이다.
// 그래서 여기서 workInProgress의 할당은 다음 Fiber Node를 설정하는 것을 의미한다.
}
ReactCurrentOwner.current = null;
}
beginWork()
가 실제로 렌더링이 일어나는 곳
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
if (current !== null) {
// 🗣️ current가 null이 아니라면, 최초 마운트가 아니라는 뜻
...
} else {
didReceiveUpdate = false
// 🗣️ 반대로 최초 마운트라면, 당연히 update는 없을 것
...
}
switch (workInProgress.tag) {
// 🗣️ 요소의 다른 타입들을 다르게 처리
case IndeterminateComponent: {
// 🗣️ IndeterminateComponent는 인스턴스화되지 않은 클래스형 또는 함수형 컴포넌트
// 렌더링되면 올바른 태그로 결정됨
return mountIndeterminateComponent(
current,
workInProgress,
workInProgress.type,
renderLanes,
);
}
case FunctionComponent: {
// 🗣️ 우리가 작성한 사용자 정의 함수 컴포넌트
const Component = workInProgress.type;
const unresolvedProps = workInProgress.pendingProps;
const resolvedProps =
workInProgress.elementType === Component
? unresolvedProps
: resolveDefaultProps(Component, unresolvedProps);
return updateFunctionComponent(
current,
workInProgress,
Component,
resolvedProps,
renderLanes,
);
}
case HostRoot:
// 🗣️ FiberRootNode 아래의 HostRoot
return updateHostRoot(current, workInProgress, renderLanes);
case HostComponent:
// 🗣️ p, div 등 내재된 HTML 태그
return updateHostComponent(current, workInProgress, renderLanes);
case HostText:
// 🗣️ HTML 텍스트 노드
return updateHostText(current, workInProgress);
case SuspenseComponent:
...
// 🗣️ 여러 타입들이 더 있음
}
}
렌더링 단계를 더 살펴보자.
3.4. prepareFreshStack
renderRootSync()
에서는 prepareFreshStack()
의 중요한 호출이 있다.
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
// 🗣️ root의 current는 FiberNode의 HostRoot
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
finishQueueingConcurrentUpdates();
return rootWorkInProgress;
}
- 새로운 렌더링이 시작될 때마다, 현재 HostRoot에서 새로운
workInProgress
가 생성 - 이는 새로운 Fiber Tree의 root로 작동
beginWork()
내의 가지들로부터HostRoot
로 먼저 이동하고,updateHostRoot()
는 그 다음 단계
3.5 updateHostRoot
function updateHostRoot(
current: null | Fiber,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostRootContext(workInProgress);
const nextProps = workInProgress.pendingProps;
const prevState = workInProgress.memoizedState;
const prevChildren = prevState.element;
cloneUpdateQueue(current, workInProgress);
processUpdateQueue(workInProgress, nextProps, null, renderLanes);
// 🗣️ 이 호출은 이 포스트의 시작에서 언급된 업데이트를 처리
// 예약된 업데이트가 처리된다는 것만 기억
// 페이로드가 추출되면, 요소는 memoizedState로 할당됨
const nextState: RootState = workInProgress.memoizedState;
const root: FiberRoot = workInProgress.stateNode;
pushRootTransition(workInProgress, root, renderLanes);
if (enableTransitionTracing) {
pushRootMarkerInstance(workInProgress);
}
// 주의: React DevTools는 'element'라고 불리는 이 속성에 의존
const nextChildren = nextState.element;
// 🗣️ ReactDOMRoot.render()의 인수를 얻을 수 있음
if (supportsHydration && prevState.isDehydrated) {
...
} else {
// Root는 탈수화되지 않았음. 클라이언트 전용 root이거나 이미 수화된 root
resetHydrationState();
if (nextChildren === prevChildren) {
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
// 🗣️ current와 workInProgress는 자식을 가지지 않으며, nextChildren은 <App />이다.
}
return workInProgress.child;
// 🗣️ reconciling이 실행된 후, workInProgress에 대한 새 하위 항목이 생성됨
// 여기서 return은 다음 처리를 workLoopSync()가 맡게될 것을 의미
}
3.6. reconcileChildren
- React 내부에서 매우 중요한 함수
- 이름의
reconcile
을 대략diff
로 바꿔 생각할 수 있다. - 이전의 children과 새로운 children을 비교,
workInProgress
에 올바른child
를 설정
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
// 🗣️ current가 없다면, 최초 마운트임을 의미
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 🗣️ current가 있다면, 이것은 리렌더이므로 reconcile됨을 의미
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.
// If we had any progressed work already, that is invalid at this point so
// let's throw it out.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
- FiberRootNode는 항상
current
를 가지고 있으므로 두 번째 브랜치인reconcideChildFibers
로 이동 - 하지만 이것은 최초 마운트이므로, 이것의 childdls
current.child
는 null - 또한
workInProgress
는 생성중이며 아직child
가 없으므로 우리가workInProgress
에child
를 설정하고 있음
3.7. reconcileChildFibers vs mountChildFibers
reconcile
의 목표 : 이미 가지고 있는 것을 재사용mount
는 '모든 것을 새로고치는reconcile
의 특별한 원시(primitive) 버전으로 취급- 이 둘은 크게 다르지 않고, 동일한 클로저이지만
shouldTrackSideEffects
플래그가 살짝 다름
여기부터 조금 지쳤어요....
코드레벨 분석은 배제하고 함수별 정리나 전체 플로우에 집중하기로 했습니다.
3.8. reconcileSingleElement
- 최초 마운트를 위한 함수
- 새로 생성된 Fiber Node가
workInProgress
의child
노드가 됨 - 주목할 점 : 사용자 정의 컴포넌트에서 Fiber Node를 생성할 때 해당 태그가 아직
FunctionComponent
가 아닌IndeterminateComponent
3.9. placeSingleChild
reconcideSingleElement()
는 Fiber Node 조정만 수행placeSingleChild()
는 자식 Fiber Node가 DOM에 삽입되도록 표시
function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case.
// We only need to do a placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
// 🗣️ shouldTrackSideEffects flag는 여기에서 사용(다른 곳도 동일)
newFiber.flags |= Placement | PlacementDEV;
// 🗣️ Placement 는 DOM sub-tree를 삽입해야 함을 의미
}
return newFiber;
}
- 이 작업은
child
에서 수행 - 최초 마운트에서
HostRoot
의 자식은Placement
로 표시 - 데모 코드에서는
<App/>
3.10. mountIndeterminateComponent
beginWork()
에서 다음으로 살펴볼 분기는IndeterminateComponent
<App />
이 HostRoot 아래에 있고, (언급했듯이) 사용자 정의 컴포넌트는 처음에IndeterminateComponent
로 표시- 따라서
<App />
이 처음 조정될 때 여기로 올 것 <App />
에는 이전 버전이 없고placeSingleChild()
가 삽입 플래그를 무시하기 때문에mountChildFibers()
가 사용됨App()
은<div/>
를 반환, 나중에beginWork()
의HostComponent
브랜치에서 처리
3.11. updateHostComponent
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
pushHostContext(workInProgress);
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
// 🗣️ React에서 내부적으로 기본적인 hydration이 작동하는 방식 참조
}
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
// 🗣️ pendingProps 는 <div />의 자식들인 <p>를 보유
const prevProps = current !== null ? current.memoizedProps : null;
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
// 🗣️ <a />와 같이 자식이 정적 텍스트인 경우의 개선
if (isDirectTextChild) {
// We special case a direct text child of a host node. This is a common
// case. We won't handle it as a reified child. We will instead handle
// this in the host environment that also has access to this prop. That
// avoids allocating another HostText fiber and traversing it.
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// If we're switching from a direct text child to a normal child, or to
// empty, we need to schedule the text content to be reset.
workInProgress.flags |= ContentReset;
}
...
markRef(current, workInProgress);
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
- React에서 내부적으로 기본적인 hydration이 작동하는 방식
- 위의 프로세스는
<p/>
에서 반복되지만, nextChildren이 배열이므로reconcileChildrenArray()
가reconcileChildFibers()
내부에서 시작된다는 점을 제외하면 동일 reconcileChildrenArray()
는key
가 존재하기 때문에 조금 더 복잡(key는 어떻게 동작하는가, React에서 List Diffing 참고)key
처리 외에는 기본적으로 첫 번째 자식 Fiber를 반환하고 계속되며, React는 트리 구조를 linked list로 flatten하므로 siblings는 추후 처리(React에서 Fiber Tree 순회 방법 참고)<Link />
의 경우,<App />
으로 이 과정을 반복<a />
및<button />
는 텍스트로 파고듦- 차이점 :
<a />
는 정적 텍스트가 자식 /<button />
JSX 표현식{count}
가 있음 - 따라서 위의 코드에서는
<a />
는nextChildren
이 null이지만,<button />
에는 자식으로 계속 이어짐
- 차이점 :
3.12. updateHostTest
<button/>
의 경우 그 자식은["click me - ", "0"]
배열updateHostText()
는beginWork()
에서 두 가지 모두에 대한 분기
function updateHostText(current, workInProgress) {
if (current === null) {
tryToClaimNextHydratableInstance(workInProgress);
} // Nothing to do here. This is terminal.
// We'll do the completion step immediately after.
return null;
}
- hydration 처리 외에는 아무 것도 하지 않음
<a />
및<button />
의 텍스트는 "Commit" 단계에서 처리됨
3.13. DOM 노드는 completeWork()
인 화면 외부에서 생성
- React에서 Fiber Tree 순회 방법에서 언급했듯이,
completeWork()
는 sibling들이beginWork()
로 시도되기 이전의 fiber에서 호출된다. stateNode
: Fiber Node의 중요한 속성 / 내재적 HTML 태그의 경우 실제 DOM 노드를 참조completeWork()
에서 DOM 노드 실제로 생성
4. Commit 단계에서의 최초 마운트
By now
- Fiber Tree의 workInProgress 버전이 드디어 완성
- 백업 DOM 노드도 생성 및 구성
- 플래그가 안내가 필요한 파이버들에 설정되어 DOM 조작을 가이드
React가 DOM을 조작하는 방법
commitMutationEffects()
commitReconciliationEffects()
: 삽입, 재정렬 등을 처리commitPlacement()
:finishedWork
의 DOM을 부모 컨테이너의 적절한 위치에 삽입하거나 추가하는 것이 핵심
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
// 🗣️ 만약 DOM 엘리먼트면, 그냥 삽입
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (
tag === HostPortal ||
(enableHostSingletons && supportsSingletons ? tag === HostSingleton : false)
) {
// If the insertion itself is a portal, then we don't want to traverse
// down its children. Instead, we'll get insertions from each child in
// the portal directly.
// If the insertion is a HostSingleton then it will be placed independently
} else {
// 🗣️ non-DOM 엘리먼트면, 재귀적으로 자식들을 처리
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
5. 요약
- Fiber Tree는 조정(reconciliation)하는 동안 느리게(lazily) 생성, 백업 DOM 노드는 동시에 생성되고 구성
HostRoot
의 직계 자식은Placement
로 표시- "Commit" 단계에서는
Placement
로 Fiber를 탐색. 부모가HostRoot
이므로, 해당 DOM 노드가 컨테이너에 삽입
총평
순차적이면 좋겠다
저번부터 느꼈지만, 이걸 더 이해하려면 어떤 걸 봐라.. 뭘 이해하려면 저런 걸 봐라... 이렇게 입체적으로 포스트를 오가는 것들이 많은 것 같아요. 처음부터 deep한 내용을 다루다보니 어쩔 수 없는 부분인가 싶다가도 조금 아쉽다는 생각도 듭니다. 하긴 react 공식문서도 비슷했던 것 같아요.
너무 어렵다...
deep dive라는 이름처럼 정말 깊어요. 굉장히 복잡한 코드의 디테일을 다루다보니 코드 레벨에서 무슨 일이 벌어지고 있는지 추적하기 힘들기도 합니다. 정말... 정말 너무 어렵네요. 잘 흡수가 되고 있는지도 잘 모르겠어요.
실제로 스터디 내부적으로도 스터디 대상을 바꾸는 것에 대한 검토가 이루어지고 있어 여기서 마치게 될 수도 있겠네요.