(번역) React는 initial mount를 어떻게 수행할까?

Taegyu Hwang·2024년 4월 15일
1
  • Jser.dev의 React Internals Deep Dive 시리즈Initial Mount, how does it work?를 번역한 글입니다.
  • 원글, 본글 모두 react 18.2.0 버전 기준입니다.
  • React 코드의 주석이 아닌, 저자가 추가한 주석엔 '저자'를 앞에 붙였습니다.

Overview of React internals에서 React는 내부적으로 Fiber Tree를 사용하여 최소 DOM 업데이트 사항을 계산하고, 이를 Commit 단계에서 반영한다고 간략히 언급했습니다. 이 글에선 React가 initial mount(최초 렌더링)를 정확히 어떻게 수행하는지 알아보겠습니다. 구체적인 예시로, 아래 코드를 통해 React가 DOM을 어떻게 구성하는지 살펴보겠습니다.

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 Architecture에 대한 간단한 소개

Fiber는 React가 앱 상태의 내부 표현을 유지하는 방식의 아키텍처입니다. 이는 FiberRootNode와 FiberNode로 구성된 트리 형태의 구조입니다. 다양한 종류의 FiberNode가 있으며, 그 중 일부는 backing DOM 노드인 HostComponent를 가지고 있습니다.

React 런타임은 Fiber 트리를 유지 및 업데이트하며, 최소한의 업데이트로 host DOM과 동기화하고자 합니다.

1.1 FiberRootNode

FiberRootNode는 React 루트 역할을 하는 특별한 노드로, 전체 앱의 필수 메타 정보를 가지고 있습니다. 이 노드의 current는 실제 Fiber 트리를 가리키며, 새로운 Fiber 트리가 생성될 때마다 current를 새로운 HostRoot로 재지정합니다.

1.2 FiberNode

FiberNode는 FiberRootNode를 제외한 모든 노드를 의미하며, 몇 가지 중요한 속성은 다음과 같습니다.

tag: FiberNode에는 많은 하위 유형이 있으며 tag로 구분됩니다. 예를 들어, FunctionComponent, HostRoot, ContextConsumer, MemoComponent, SuspenseComponent 등이 있습니다.
stateNode: 다른 backing data를 가리키며, HostComponent의 경우 실제 backing DOM 노드를 가리킵니다.
child, sibling, return : 이들은 함께 트리 구조를 형성합니다.
elementType: 우리가 제공하는 컴포넌트 함수 또는 고유 HTML 태그입니다.
flags: 커밋 단계에서 적용할 업데이트를 나타냅니다. subtreeFlags는 하위 트리를 위한 것입니다.
lanes: 보류 중인 업데이트의 우선 순위를 나타냅니다.childLanes는 하위 트리를 위한 것입니다.
memoizedState: 중요한 데이터를 가리키며, FunctionComponent의 경우 훅을 의미합니다.

2. Initial mount의 Trigger 단계

createRoot()는 React 루트를 생성하며, 이 때 더미 HostRoot FiberNode도 함께 생성되어 current로 지정됩니다.

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  let isStrictMode = false;
  let concurrentUpdatesByDefaultOverride = false;
  let identifierPrefix = '';
  let onRecoverableError = defaultOnRecoverableError;
  let transitionCallbacks = null;
  
  // 저자) createContainer 함수는 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,
  );
  // 저자) HostRoot의 FiberNode가 생성되고, React root의 current에 할당 됩니다.
  root.current = uninitializedFiber; 
  uninitializedFiber.stateNode = root;
  ...
  initializeUpdateQueue(uninitializedFiber);
  return root;
}

root.render()는 HostRoot에 업데이트를 예약합니다. 인자로 전달된 element는 update 페이로드에 저장됩니다.

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".
  // 저자) render의 인자로 주어진 element는 update의 payload로 저장됩니다.
  update.payload = {element};

  // 저자) 그런 다음, 업데이트가 대기열에 추가됩니다. 지금은 이 과정을 자세히 다루지 않고, 업데이트가 처리를 기다리고 있다는 점만 기억합시다.
  const root = enqueueUpdate(current, update, lane);

  if (root !== null) {
    scheduleUpdateOnFiber(root, current, lane);
    entangleTransitions(root, current, lane);
  }
  return lane;

3. Initial mount의 Render 단계

3.1 performConcurrentWorkOnRoot()

Overview에서 언급했듯이, performConcurrentWorkOnRoot()는 initial mount과 재렌더링 관계없이 렌더링을 시작하는 진입점입니다.

한 가지 명심해야 할 점은 함수 이름 concurrent라고 되어 있더라도 내부적으로는 필요하다면 sync 모드로 돌아간다는 것입니다. 일례로, initial mount의 경우 DefaultLane은 'blocking lane'이기 때문에 sync 모드로 동작합니다.

function performConcurrentWorkOnRoot(root, didTimeout) {
  ...
  // Determine the next lanes to work on, using the fields stored
  // on the root.
  let lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  
  ...
  
  // We disable time-slicing in some cases: if the work has been CPU-bound
  // for too long ("expired" work, to prevent starvation), or we're in
  // sync-updates-by-default mode.
  // TODO: We only check `didTimeout` defensively, to account for a Scheduler
  // bug we're still investigating. Once the bug in Scheduler is fixed,
  // we can remove this, since we track expiration ourselves.
  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은 blocking lane 중 하나입니다.
    DefaultLane;
    
  return (lanes & SyncDefaultLanes) !== NoLanes;
}

lane에 대해 더 자세히 알고 싶다면 What are Lanes in React를 참고하세요

위 코드를 보면 initial mount에서는 concurrent 모드가 사용되지 않는다는 것을 알 수 있습니다. initial mount의 경우 가능한 한 빨리 UI를 그리는 것이 중요하며, 이를 지연하는 것은 도움이 되지 않기 때문에 합리적인 처사입니다.

3.2 renderRootSync()

renderRootSync()는 내부적으론 단지 while 루프일 뿐입니다.

function renderRootSync(root: FiberRoot, lanes: Lanes) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  const prevDispatcher = pushDispatcher();
  
  // If the root or lanes have changed, throw out the existing stack
  // and prepare a fresh one. Otherwise we'll continue where we left off.
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    if (enableUpdaterTracking) {
      if (isDevToolsPresent) {
        const memoizedUpdaters = root.memoizedUpdaters;
        if (memoizedUpdaters.size > 0) {
          restorePendingUpdaters(root, workInProgressRootRenderLanes);
          memoizedUpdaters.clear();
        }
        
        // At this point, move Fibers that scheduled the upcoming work from the Map to the Set.
        // If we bailout on this work, we'll move them back (like above).
        // It's important to move them now in case the work spawns more work at the same priority with different updaters.
        // That way we can keep the current update and future updates separate.
        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);
  
  // Set this to null to indicate there's no in-progress render.
  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;
  return workInProgressRootExitStatus;
}

// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  // 저자) 이 while 루프의 의미는 만약 workInProgress가 존재한다면, performUnitOfWork를 수행하게 된다는 것입니다.
  while (workInProgress !== null) {
    // 저자) 이름처럼, 이 함수는 하나의 Fiber 노드 단위에서 작업을 수행합니다.
    performUnitOfWork(workInProgress);
  }
}

workInProgress가 무엇을 의미하는지 설명할 필요가 있습니다. React 코드 베이스에서는 currentworkInProgress라는 접두사가 곳곳에서 사용됩니다. React는 내부적으로 현재 상태를 표현하기 위해 Fiber 트리를 사용하기 때문에 업데이트가 있을 때마다 새로운 트리를 생성하고 이전 트리와 비교해야 합니다. 따라서 current는 UI에 표시되는 현재 버전을 의미하고, workInProgress는 현재 빌드 중이며, 다음 current로 사용될 버전을 의미합니다.

3.3 performUnitOfWork()

여기서 React는 단일 Fiber 노드에서 작동하며 수행해야 할 작업이 있는지 확인합니다.

이 섹션을 더 쉽게 이해하려면 먼저 How does React traverse Fiber tree internally(React는 내부적으로 Fiber 트리를 어떻게 탐색하는가)를 읽어보는 것을 추천합니다.

function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  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) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    // 저자) 앞서 말했듯, workLoopSync는 단지 while 루프일 뿐이며 이 루프는 workInProgress에 대해 completeUnitOfWork를 계속 실행합니다.
	// 여기서 workInProgress에 할당하는 것은 다음 작업할 Fiber 노드를 설정하는 것을 의미힙니다.
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

beginWork()는 실제로 렌더링이 이뤄지는 곳입니다.

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 저자) current가 null이 아니라는 것은, initial mount가 아님을 의미합니다.
  if (current !== null) {
    ...
  } else {
    // 저자) current가 null이라면, initial mount기 때문에 당연히 업데이트가 없습니다.
    didReceiveUpdate = false
    ...
  }
  
  // 저자) 다양한 타입의 element를 다른 방식으로 처리합니다.
  switch (workInProgress.tag) {
    // 저자) IndeterminateComponent는 아직 인스턴스화되지 않은 클래스 컴포넌트나 함수 컴포넌트를 의미합니다. 렌더링이 완료되면 적절한 태그로 결정됩니다. 이에 대해서는 이후 다시 설명하겠습니다.
    case 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,
      );
    }
    // 저자) FiberRootNode 아래의 HostRoot
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    // 저자) p, div 등과 같은 내재된 HTML 태그
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    // 저자) HTML text 노드
    case HostText:
      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;
  // 저자) root.current는 HostRoot의 FiberNode입니다.
  const rootWorkInProgress = createWorkInProgress(root.current, null);

  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 트리의 루트로 작용합니다.

그러므로, 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);
  // 저자) 이 호출은 게시물 초반에 언급했던 업데이트를 처리합니다. 
  // 업데이트는 예약되어 처리되며, 페이로드가 추출되고 element가 memoizedState로 할당된다는 점만 기억합시다.
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);

  const nextState: RootState = workInProgress.memoizedState;
  const root: FiberRoot = workInProgress.stateNode;
  pushRootTransition(workInProgress, root, renderLanes);
  if (enableTransitionTracing) {
    pushRootMarkerInstance(workInProgress);
  }
  // Caution: React DevTools currently depends on this property
  // being called "element".
  // 저자) ReactDOMRoot.render()의 인자입니다.
  const nextChildren = nextState.element;

  if (supportsHydration && prevState.isDehydrated) {
    ...
  } else {
    // Root is not dehydrated. Either this is a client-only root, or it
    // already hydrated.
    resetHydrationState();
    if (nextChildren === prevChildren) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
    // 저자) 여기서, current와 workInProgress 모두 child가 없으며 nextChildren은 <App/>입니다.
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }
  // 저자) 조정(reconciling) 이후, workInProgress에 새로운 child가 생성됩니다.
  // 이 return은  workLoopSync()가 다음에 이를 처리할 것을 의미합니다.
  return workInProgress.child;
}

3.6 reconcileChildren

이것은 React 내부에서 매우 중요한 함수입니다. 이름에서, reconcile을 대략 diff로 생각할 수 있습니다. 이 함수는 새 children과 이전 children을 비교하고, 적절한 childworkInProgress에 설정합니다.

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  // 저자) current가 null이라는 것은 initial mount라는 것을 의미합니다.
  if (current === null) {
    // 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,
    );
    // 저자) current가 존재하면, 리렌더이기 때문에 reconcile을 수행합니다.
  } else {
    // 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를 가지고 있으므로 두 번째 분기인 reconcileChildFibers로 이동합니다. 하지만, 만약 initial mount인 경우에는 그 자식인 current.childnull입니다.
또한, workInProgress가 아직 구축 중이고 child가 없기 때문에 workInProgresschild를 설정하고 있는 것을 볼 수 있습니다.

3.7 reconcileChildFibers() vs mountChildFibers()

reconcile의 목표는 이미 가지고 있는 것들을 재사용하는 것입니다. mount는 항상 모든 것을 새로고침하는 reconcile의 특별한 원시버전으로 취급할 수 있습니다.
사실 코드 내에서 둘은 크게 다르지 않습니다. 둘은 동일한 클로저지만, shouldTrackSideEffects 플래그만 살짝 다릅니다.

export const reconcileChildFibers: ChildReconciler =
  createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);
function createChildReconciler(
  // 저자) 이 flag는 삽입 등을 트래킹해야하는지 여부를 제어합니다.
  shouldTrackSideEffects: boolean,
): ChildReconciler {
  
  ...
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // This indirection only exists so we can reset `thenableState` at the end.
    // It should get inlined by Closure.
    thenableIndexCounter = 0;
    const firstChildFiber = reconcileChildFibersImpl(
      returnFiber,
      currentFirstChild,
      newChild,
      lanes,
    );
    thenableState = null;
    // Don't bother to reset `thenableIndexCounter` to 0 because it always gets
    // set at the beginning.
    // 저자) children들을 reconcile 후 첫번째 자식 Fiber는 반환되어 workInProgress의 자식으로 설정합니다.
    return firstChildFiber;
  }
  return reconcileChildFibers;
}

만약 전체 Fiber 트리가 구성되어야한다고 생각해보면, 조정 후에 모든 노드는 "삽입(insert) 필요"로 표시되어야 할 것입니다. 하지만, 이런 과정은 분명 필요하지 않고, 우리는 root만 삽입하면 됩니다. 그렇기 때문에 mountChildFibers는 수행하는 바를 보다 명확하게 하는 내부적인 개선입니다.

function reconcileChildFibersImpl(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
  lanes: Lanes,
): Fiber | null {
  // This function is not recursive.
  // If the top level item is an array, we treat it as a set of children,
  // not as a fragment. Nested arrays on the other hand will be treated as
  // fragment nodes. Recursion happens at the normal flow.
    
  // Handle top level unkeyed fragments as if they were arrays.
  // This leads to an ambiguity between <>{[...]}</> and <>...</>.
  // We treat the ambiguous cases above the same.
  // TODO: Let's use recursion like we do for Usable nodes?
  const isUnkeyedTopLevelFragment =
    typeof newChild === 'object' &&
    newChild !== null &&
    newChild.type === REACT_FRAGMENT_TYPE &&
    newChild.key === null;
  if (isUnkeyedTopLevelFragment) {
    newChild = newChild.props.children;
  }
    
  // Handle object types
  if (typeof newChild === 'object' && newChild !== null) {
    // 저자) $$typeof는 React Element의 typeof
    switch (newChild.$$typeof) {
      // 저자) children이 <App/>처럼 React Element인 경우 
      case REACT_ELEMENT_TYPE:
        // 저자) 나중에 아래 두 함수(placeSingleChild, reconcileSingleElement)를 파볼 예정입니다.
        return placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      case REACT_PORTAL_TYPE:
        return placeSingleChild(
          reconcileSinglePortal(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      case REACT_LAZY_TYPE:
        const payload = newChild._payload;
        const init = newChild._init;
        // TODO: This function is supposed to be non-recursive.
        return reconcileChildFibers(
          returnFiber,
          currentFirstChild,
          init(payload),
          lanes,
        );
    }
    // 저자) children이 배열인 경우
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    if (typeof newChild.then === 'function') {
      const thenable: Thenable<any> = (newChild: any);
      return reconcileChildFibersImpl(
        returnFiber,
        currentFirstChild,
        unwrapThenable(thenable),
        lanes,
      );
    }
    if (
      newChild.$$typeof === REACT_CONTEXT_TYPE ||
      newChild.$$typeof === REACT_SERVER_CONTEXT_TYPE
    ) {
      ...
    }
    throwOnInvalidObjectType(returnFiber, newChild);
  }
  
  // 저자) 여기서 대부분의 primitive 값을 처리해 Text 노드를 업데이트합니다.
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild,
        lanes,
      ),
    );
  }

  // Remaining cases are all treated as empty.
  return deleteRemainingChildren(returnFiber, currentFirstChild);
}

두 단계가 있음을 알 수 있습니다. reconcileXXX()로 차이를 비교하고, placeSingleChild()로 DOM에 삽입되어야할 fiber를 표시합니다.

3.8 reconcileSingleElement()

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  // 저자) 이 코드는 child가 이미 존재하는 경우의 update를 다루지만, initial mount에서는 없기 때문에 지금은 넘어갑시다.
  while (child !== null) {
    // TODO: If key === null and child.key === null, then this only applies to
    // the first item in the list.
    if (child.key === key) {
      const elementType = element.type;
      if (elementType === REACT_FRAGMENT_TYPE) {
        if (child.tag === Fragment) {
          deleteRemainingChildren(returnFiber, child.sibling);
          const existing = useFiber(child, element.props.children);
          existing.return = returnFiber;
          return existing;
        }
      } else {
        if (
          child.elementType === elementType ||
          // Keep this check inline so it only runs on the false path:
          (__DEV__
            ? isCompatibleFamilyForHotReloading(child, element)
            : false) ||
          // Lazy types should reconcile their resolved type.
          // We need to do this after the Hot Reloading check above,
          // because hot reloading has different semantics than prod because
          // it doesn't resuspend. So we can't let the call below suspend.
          (typeof elementType === 'object' &&
            elementType !== null &&
            elementType.$$typeof === REACT_LAZY_TYPE &&
            resolveLazy(elementType) === child.type)
        ) {
          deleteRemainingChildren(returnFiber, child.sibling);
          const existing = useFiber(child, element.props);
          existing.ref = coerceRef(returnFiber, child, element);
          existing.return = returnFiber;
          if (__DEV__) {
            existing._debugSource = element._source;
            existing._debugOwner = element._owner;
          }
          return existing;
        }
      }
      // Didn't match.
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }
    
  if (element.type === REACT_FRAGMENT_TYPE) {
    const created = createFiberFromFragment(
      element.props.children,
      returnFiber.mode,
      lanes,
      element.key,
    );
    created.return = returnFiber;
    return created;
  } else {
    // 저자) 이전 버전이 없기 때문에 element에서 새 fiber를 만들기만 하면 됩니다.
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }
}

위에서 보이듯이, initial mount에서의 reconcileSingleElement()는 꽤 간단합니다. 새로 생성된 Fiber 노드는 workInProgress의 자식이 된다는 점을 유의하세요.
한 가지 주목할 점은 사용자 정의 컴포넌트에서 Fiber 노드가 생성될 때는 태그가 아직 FunctionConponent가 아닌, IndeterminateComponent라는 점입니다.

export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let owner = null;
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
  return fiber;
}

export function createFiberFromTypeAndProps(
  type: any, // React$ElementType
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let fiberTag = IndeterminateComponent;
  // The resolved type is set if we know what the final type will be. I.e. it's not lazy.
  let resolvedType = type;
  ...
  
  const fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.lanes = lanes;
  return fiber;
}

3.9 placeSingleChild()

reconcileSingleElement()는 Fiber 노드 조정만 수행하며, placeSingleChild가 DOM에 삽입될 자식 Fiber 노드를 표시합니다.

function placeSingleChild(newFiber: Fiber): Fiber {
  // This is simpler for the single child case. We only need to do a
  // placement for inserting new children.
  // 저자) 네 맞아요! 이 flag가 여기서(물론 다른 곳에서도) 쓰입니다.
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    // 저자) Placement는 DOM 서브트리가 삽입되어야 함을 의미합니다.
    newFiber.flags |= Placement | PlacementDEV;
  }
  return newFiber;
}

이 작업은 child에 대해 수행되기 때문에 initial mount에서 HostRoot의 자식은 Placement로 표시됩니다. 데모 코드에서는 <App/>입니다.

3.10 mountIndeterminateComponent()

beginWork()에서 다음으로 살펴볼 분기는 IndeterminateComponent입니다. <App/>이 HostRoot 아래에 위치하고, 앞서 말했듯 사용자 정의 컴포넌트는 처음에 IndeterminateComponent로 표시되기 때문에 <App/>이 처음 조정될 때 이 분기로 오게 됩니다.

function mountIndeterminateComponent(
  _current: null | Fiber,
  workInProgress: Fiber,
  Component: $FlowFixMe,
  renderLanes: Lanes,
) {
  resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);
    
  const props = workInProgress.pendingProps;
  let context;
  if (!disableLegacyContext) {
    const unmaskedContext = getUnmaskedContext(
      workInProgress,
      Component,
      false,
    );
    context = getMaskedContext(workInProgress, unmaskedContext);
  }
    
  prepareToReadContext(workInProgress, renderLanes);
  let value;
  let hasId;
  
  // 저자) 함수 컴포넌트를 실행하고 children elements를 반환합니다. 
  value = renderWithHooks(
    null,
    workInProgress,
    Component,
    props,
    context,
    renderLanes,
  );
  hasId = checkDidRenderIdHook();
    
  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
    
  if (
    // Run these checks in production only if the flag is off.
    // Eventually we'll delete this branch altogether.
    !disableModulePatternComponents &&
    typeof value === 'object' &&
    value !== null &&
    typeof value.render === 'function' &&
    value.$$typeof === undefined
  ) {
    // Proceed under the assumption that this is a class instance
    // 저자) 한 번 렌더되면, 더 이상 IndeterminateComponent가 아닙니다.
    workInProgress.tag = ClassComponent;
   
    ...
  } else {
    // Proceed under the assumption that this is a function component
    // 저자) 한 번 렌더되면, 더 이상 IndeterminateComponent가 아닙니다.
    workInProgress.tag = FunctionComponent;
    
    if (getIsHydrating() && hasId) {
      pushMaterializedTreeId(workInProgress);
    }
    // 저자) 여기서 current가 null이기 때문에, mountChildFibers()가 사용될 것입니다.
    reconcileChildren(null, workInProgress, value, renderLanes);
    
    return workInProgress.child;
  }
}

앞서 말했듯, <App/> 렌더링 시엔 항상 current를 가지고 있는 HostRoot와 달리 <App/>은 이전 버전이 없기 때문에 mountChildFibers()를 사용합니다. 그리고 placeSingleChild()는 삽입 플래그를 무시합니다.
App()<div/>를 반환하는데, 이는 나중에 beginWork()HostComponent 분기에서 처리됩니다.

3.11 updateHostComponent()

function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  pushHostContext(workInProgress);
  if (current === null) {
    tryToClaimNextHydratableInstance(workInProgress);
  }
    
  const type = workInProgress.type;
  // 저자) pendingProps는 <div/>의 자식인 <p/>를 가지고 있습니다.
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
    
  let nextChildren = nextProps.children;
  // 저자) 자식이 <a/>와 같이 정적인 text인 경우에 대한 개선된 기능입니다.
  const isDirectTextChild = shouldSetTextContent(type, nextProps);

  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;
}

hydration에 대해서는 How basic hydration works internally in React?를 참조하세요

위 과정은 <p/>에 대해서도 반복됩니다만, 그떈, nextChildren이 배열이므로 reconcileChildFibers 내부에서 reconcileChildrenArray()가 실행됩니다.

reconcileChildrenArray()는 key의 존재로 조금 더 복잡합니다. key에 대한 자세한 내용은 How does ‘key’ work internally? List diffing in React 글을 참고해주세요.

key 처리를 제외하면, 이 함수는 기본적으로 첫 번째 자식 Fiber를 반환하고 계속 진행하며, 형제 노드들은 나중에 처리됩니다. 이는 React가 트리 구조를 linked list로 평탄화하기 때문입니다. 자세한 내용은 How does React traverse Fiber tree internally? 글을 참고해주세요.

<Link/>의 경우 <App/>과 같은 과정을 반복합니다.

<a/><button/>의 텍스트에 대해서 좀 더 자세히 살펴봅시다.

이 둘은 조금 다릅니다. <a/>는 자식으로 정적 텍스트를 가지고 있는 반면, <button/>은 JSX 표현식 {count}를 자식으로 가집니다.그래서 위 코드에서, <a/>nextChildren이 null이 되지만, <button/>은 자식으로 과정이 계속 이어집니다.

3.12 updateHostText()

<button/>의 자식은 ["click me - ", "0"] 배열이고, beginWork()에서 두 경우 모두 updateHostText()로 분기됩니다.

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()에서 화면 밖에 생성

How does React traverse Fiber tree internally? 글에서 말했듯, completeWork()는 해당 Fiber의 형제 노드가 beginWork()로 처리되기 전에 호출됩니다.

Fiber 노드에는 stateNode라는 중요한 속성을 가지고 있는데, 내재된 HTML의 경우 해당 속성은 실제 DOM 노드를 참조합니다. 그리고 실제 DOM 노드의 생성은 completeWork()에서 이뤄집니다.

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  // Note: This intentionally doesn't check if we're hydrating because comparing
  // to the current tree provider fiber is just as fast and less error-prone.
  // Ideally we would have a special version of the work loop only
  // for hydration.
  popTreeContext(workInProgress);
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      bubbleProperties(workInProgress);
      return null;
    case ClassComponent: {
      const Component = workInProgress.type;
      if (isLegacyContextProvider(Component)) {
        popLegacyContext(workInProgress);
      }
      bubbleProperties(workInProgress);
      return null;
    }
    case HostRoot: {
      ...
      return null;
    }
    ...
    // 저자) HTML 태그 케이스
    case HostComponent: {
      popHostContext(workInProgress);
      const type = workInProgress.type;
      // 저자) current가 있다면, update 분기로 갑니다.
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          renderLanes,
        );
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
        // 저자) 그러나 아직 current가 없기 때문에 mount 분기로 갑니다.
      } else {
        ...
        if (wasHydrated) {
          ...
        } else {
          const rootContainerInstance = getRootHostContainer();
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          // 저자) 중요! DOM 노드가 생성될 떄, 하위트리의 직접 연결된 모든 DOM 노드들의 부모가 되어야 합니다.
          appendAllChildren(instance, workInProgress, false, false);
          workInProgress.stateNode = instance;
          
         if (
            // 저자) 곧 다룰거에요!
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        }
          
        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          markRef(workInProgress);
        }
      }
      bubbleProperties(workInProgress);
        
      ...
      return null;
    }
    case HostText: {
      const newText = newProps;
      if (current && workInProgress.stateNode != null) {
        const oldText = current.memoizedProps;
        // If we have an alternate, that means this is an update and we need
        // to schedule a side-effect to do the updates.
        updateHostText(current, workInProgress, oldText, newText);
      } else {
        ...
        const rootContainerInstance = getRootHostContainer();
        const currentHostContext = getHostContext();
        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          if (prepareToHydrateHostTextInstance(workInProgress)) {
            markUpdate(workInProgress);
          }
        } else {
          workInProgress.stateNode = createTextInstance(
            newText,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }
    ...
  }
}

우리가 넘겼던 하나의 질문은 <a/><button/>간의 차이점입니다. 둘은 텍스트 노드를 다르게 처리합니다.

<button/>의 경우, 위에서 본 것처럼 HostText 분기로 진행되고 createTextIstance()는 새로운 텍스트 노드를 생성합니다. 그러나 <a/>는 조금 다릅니다. 좀 더 자세히 살펴보시죠.

위 코드에서 HostComponentfinalizeInitialChildren() 함수가 있는 것을 주목하세요.

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props);
  ...
}
  
export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document | DocumentFragment,
): void {
  ...
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );
  ...
}
  
function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document | DocumentFragment,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      // Relies on `updateStylesByID` not mutating `styleUpdates`.
      setValueForStyles(domElement, nextProp);
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    } else if (propKey === CHILDREN) {
      // 저자) string이나 number의 자식은 컴포넌트의 text 요소로 처리됩니다. 만약, 표현식을 포함하고 있다면, 그것은 배열이 되기 때문에 이 분기에서 처리하지 않습니다.
      if (typeof nextProp === 'string') {
        // Avoid setting initial textContent when the text is empty. In IE11 setting
        // textContent on a <textarea> will cause the placeholder to not
        // show within the <textarea> until it has been focused and blurred again.
        // https://github.com/facebook/react/issues/6731#issuecomment-254874553
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);
        }
      } else if (typeof nextProp === 'number') {
        setTextContent(domElement, '' + nextProp);
      }
    } else if (
      propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
      propKey === SUPPRESS_HYDRATION_WARNING
    ) {
      // Noop
    } else if (propKey === AUTOFOCUS) {
      // We polyfill it separately on the client during commit.
      // We could have excluded it in the property list instead of
      // adding a special case here, but then it wouldn't be emitted
      // on server rendering (but we *do* want to emit it in SSR).
    } else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

4. Initial mount의 Commit 단계

지금까지,

  1. workInProgress 버전의 Fiber 트리가 드디어 완성되었습니다!
  2. backing DOM 노드들도 생성되고 구조화되었습니다!
  3. 필요한 Fiber에는 DOM 조작을 안내하는 flag들이 설정되었습니다!

이제 React가 어떻게 DOM을 조작하는지 살펴볼 시간입니다.

4.1 commitMutationEffects()

Overview of React internals에서 commit 단계를 간략하게 설명했었습니다. 이제 DOM 변이를 다루는 commitMutationEffects() 함수에 대해 자세히 살펴봅시다.

export function commitMutationEffects(
  root: FiberRoot,
  // 저자) 새로 생성된 Fiber 트리를 가지고 있는 HostRoot의 Fiber 노드입니다.
  finishedWork: Fiber,
  committedLanes: Lanes,
) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
  inProgressLanes = null;
  inProgressRoot = null;
}
function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
    
  // The effect flag should be checked *after* we refine the type of fiber,
  // because the fiber tag is more specific. An exception is any flag related
  // to reconciliation, because those can be set on all fiber types.
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // 저자) 이 재귀 호출은 하위 트리가 먼저 처리되도록 합니다.
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      // 저자) ReconciliationEffects의 의미는 삽입등을 의미합니다.
      commitReconciliationEffects(finishedWork);
      ...
      return;
    }
    ...
    case HostComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      ...
      return;
    }
    case HostText: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      ...
      return;
    }
    case HostRoot: {
      if (enableFloat && supportsResources) {
        prepareToCommitHoistables();
        const previousHoistableRoot = currentHoistableRoot;
        currentHoistableRoot = getHoistableRoot(root.containerInfo);
        recursivelyTraverseMutationEffects(root, finishedWork, lanes);
        currentHoistableRoot = previousHoistableRoot;
        commitReconciliationEffects(finishedWork);
      } else {
        recursivelyTraverseMutationEffects(root, finishedWork, lanes);
        commitReconciliationEffects(finishedWork);
      }
      ...
      return;
    }
    ...
    default: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      return;
    }
  }
}
function recursivelyTraverseMutationEffects(
  root: FiberRoot,
  parentFiber: Fiber,
  lanes: Lanes,
) {
  // Deletions effects can be scheduled on any fiber type. They need to happen
  // before the children effects hae fired.
  // 저자) 삭제는 별도로 처리됩니다.  
  const deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      try {
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }

  const prevDebugFiber = getCurrentDebugFiberInDEV();
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child !== null) {
      setCurrentDebugFiberInDEV(child);
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }
  setCurrentDebugFiberInDEV(prevDebugFiber);
}

4.2 commitReconciliationEffects()

commitReconciliationEffects()는 삽입, 재정렬 등을 처리합니다.

function commitReconciliationEffects(finishedWork: Fiber) {
  // Placement effects (insertions, reorders) can be scheduled on any fiber
  // type. They needs to happen after the children effects have fired, but
  // before the effects on this fiber have fired.
  const flags = finishedWork.flags;
  // 저자) 맞아요! 아까 그 flag가 여기서도 체크됩니다.
  if (flags & Placement) {
    try {
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    // Clear the "placement" from effect tag so that we know that this is
    // inserted, before any life-cycles like componentDidMount gets called.
    // TODO: findDOMNode doesn't rely on this any more but isMounted does
    // and isMounted is deprecated anyway so we should be able to kill this.
    finishedWork.flags &= ~Placement;
  }
  ...
}

우리 데모 상에선, <App/>의 Fiber 노드가 실제로 커밋됩니다.

4.3 commitPlacement()

function commitPlacement(finishedWork: Fiber): void {
  ...
  // Recursively insert all host nodes into the parent.
  const parentFiber = getHostParentFiber(finishedWork);
  // 저자) 부모 Fiber의 타입을 확인하는 것에 주목하세요. 삽입은 부모 노드에 대해 이뤄지기 때문에 그렇습니다.
  switch (parentFiber.tag) {
    case HostSingleton: {
      if (enableHostSingletons && supportsSingletons) {
        const parent: Instance = parentFiber.stateNode;
        const before = getHostSibling(finishedWork);
        // We only have the top Fiber that was inserted but we need to recurse down its
        // children to find all the terminal nodes.
        insertOrAppendPlacementNode(finishedWork, before, parent);
        break;
      }
      // Fall through
    }
    // 저자) initial mount에는 이 분기를 타지 않습니다.
    case HostComponent: {
      const parent: Instance = parentFiber.stateNode;
      if (parentFiber.flags & ContentReset) {
        // Reset the text content of the parent before doing any insertions
        resetTextContent(parent);
        // Clear ContentReset from the effect tag
        parentFiber.flags &= ~ContentReset;
      }
      const before = getHostSibling(finishedWork);
      // We only have the top Fiber that was inserted but we need to recurse down its
      // children to find all the terminal nodes.
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }
    // 저자) initial mount에서 Placement flag를 가지고 있는 Fiber 노드는 <App/>이며, 그 parentFiber는 HostRoot입니다.
    case HostRoot:
    case HostPortal: {
      // 저자) HostRoot의 stateNode는 FiberRootNode를 참조합니다.
      const parent: Container = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }
    default:
      throw new Error(
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
}

이 아이디어는 finishWork()의 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 {
    // 저자) DOM 요소가 아니라면, 재귀적으로 children을 처리합니다.
    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;
      }
    }
  }
}

이렇게 최종적으로 DOM이 삽입됩니다.

5. 요약

좋습니다! 드디어 DOM이 어떻게 생성되고 컨테이너에 삽입되는지 살펴보았습니다.

그러니깐, initial mount의 경우

  1. Fiber 트리는 reconciliation 동안 게으르게 생성되며, backing DOM 노드도 동시에 생성되고 조직됩니다.
  2. HostRoot의 바로 아래 자식은 Placement로 표시됩니다.
  3. commit 단계에서는 Placement 태그가 있는 Fiber를 찾습니다. 이 Fiber의 부모가 HostRoot이므로, 해당 DOM 노드가 컨테이너에 삽입됩니다.

원글의 마지막에 전체적인 과정을 도식화한 장표가 있습니다. 보시는 것을 추천합니다.

0개의 댓글

관련 채용 정보