아래부터는 내용을 요약한 부분입니다!
반말은 내용의 요약, 존댓말은 제가 작성한 내용입니다.
- 이전 장 : 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> ); }
- React가 앱 상태를 내부적으로 표현하는 방식
FiberRootNode와FiberNodes으로 구성된 Tree 같은 구조- Fiber에는 모든 종류의 FiberNode가 있고, 이들 중 일부는 백업 DOM 노드인
HostComponent를 가짐 - React 런타임은 Fiber Tree를 유지 및 업데이트, 그리고 최소한의 업데이트로 Host DOM과 동기화
- 전체 앱의 필수적인 메타정보를 가지는,
React root처럼 동작하는 특별한 노드 - 이 노드의
current는 실제 Fiber Tree를 가리킴 - 새로운 FiberTree가 생성될 때마다,
current는 새로운HostRoot를 가리킴
: FiberRootNode를 제외한 모든 노드
tagtag별로 구분되는 많은 하위 유형이 있음- 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을 의미
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; }
- 초기 렌더링과 리렌더링의 렌더링 시작 진입점(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; }
- 위의 코드를 보면 최초 마운트는 동시성 모드를 사실상 사용하지 않음을 알 수 있다.
- 최초 마운트의 경우 최대한 빨리 UI에게 고통을 줘야 하며, 지연(defer)시키는 것은 도움이 되지 않는다.
저의 의역: "최초 마운트는 블로킹을 만드므로 그냥 빨리 시작하고 마치는 게 좋다."
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로써 사용될 버전을 의미
- 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: ... // 🗣️ 여러 타입들이 더 있음 } }
렌더링 단계를 더 살펴보자.
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()는 그 다음 단계
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()가 맡게될 것을 의미 }
- 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를 설정하고 있음
reconcile의 목표 : 이미 가지고 있는 것을 재사용mount는 '모든 것을 새로고치는reconcile의 특별한 원시(primitive) 버전으로 취급- 이 둘은 크게 다르지 않고, 동일한 클로저이지만
shouldTrackSideEffects플래그가 살짝 다름
여기부터 조금 지쳤어요....
코드레벨 분석은 배제하고 함수별 정리나 전체 플로우에 집중하기로 했습니다.
- 최초 마운트를 위한 함수
- 새로 생성된 Fiber Node가
workInProgress의child노드가 됨 - 주목할 점 : 사용자 정의 컴포넌트에서 Fiber Node를 생성할 때 해당 태그가 아직
FunctionComponent가 아닌IndeterminateComponent
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/>
beginWork()에서 다음으로 살펴볼 분기는IndeterminateComponent<App />이 HostRoot 아래에 있고, (언급했듯이) 사용자 정의 컴포넌트는 처음에IndeterminateComponent로 표시- 따라서
<App />이 처음 조정될 때 여기로 올 것 <App />에는 이전 버전이 없고placeSingleChild()가 삽입 플래그를 무시하기 때문에mountChildFibers()가 사용됨App()은<div/>를 반환, 나중에beginWork()의HostComponent브랜치에서 처리
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 />에는 자식으로 계속 이어짐
- 차이점 :
<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" 단계에서 처리됨
- React에서 Fiber Tree 순회 방법에서 언급했듯이,
completeWork()는 sibling들이beginWork()로 시도되기 이전의 fiber에서 호출된다. stateNode: Fiber Node의 중요한 속성 / 내재적 HTML 태그의 경우 실제 DOM 노드를 참조completeWork()에서 DOM 노드 실제로 생성
- Fiber Tree의 workInProgress 버전이 드디어 완성
- 백업 DOM 노드도 생성 및 구성
- 플래그가 안내가 필요한 파이버들에 설정되어 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; } } } }
- Fiber Tree는 조정(reconciliation)하는 동안 느리게(lazily) 생성, 백업 DOM 노드는 동시에 생성되고 구성
HostRoot의 직계 자식은Placement로 표시- "Commit" 단계에서는
Placement로 Fiber를 탐색. 부모가HostRoot이므로, 해당 DOM 노드가 컨테이너에 삽입
저번부터 느꼈지만, 이걸 더 이해하려면 어떤 걸 봐라.. 뭘 이해하려면 저런 걸 봐라... 이렇게 입체적으로 포스트를 오가는 것들이 많은 것 같아요. 처음부터 deep한 내용을 다루다보니 어쩔 수 없는 부분인가 싶다가도 조금 아쉽다는 생각도 듭니다. 하긴 react 공식문서도 비슷했던 것 같아요.
deep dive라는 이름처럼 정말 깊어요. 굉장히 복잡한 코드의 디테일을 다루다보니 코드 레벨에서 무슨 일이 벌어지고 있는지 추적하기 힘들기도 합니다. 정말... 정말 너무 어렵네요. 잘 흡수가 되고 있는지도 잘 모르겠어요.
실제로 스터디 내부적으로도 스터디 대상을 바꾸는 것에 대한 검토가 이루어지고 있어 여기서 마치게 될 수도 있겠네요.
