logo
Search검색어를 포함하는 게시물들이 최신순으로 표시됩니다.
    Table of Contents
    2. React의 최초 마운트

    이미지 보기

    2. React의 최초 마운트

    React의 최초 마운트는 어떻게 동작할까?

    • 25.02.04 작성

    • 읽는 데 26

    TOC

    들어가며

    아래부터는 내용을 요약한 부분입니다!
    반말은 내용의 요약, 존댓말은 제가 작성한 내용입니다.

    복습

    • 이전 장 : 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 architecture
    Fiber란?
    • React가 앱 상태를 내부적으로 표현하는 방식
    • FiberRootNodeFiberNodes으로 구성된 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를 제외한 모든 노드

    몇 가지 중요한 속성
    1. tag
      1. tag별로 구분되는 많은 하위 유형이 있음
      2. FunctionComponent, HostRoot, ContextConsumer,MemoComponent,SuspenseComponent 등
    2. stateNode
      1. 다른 백업 데이터를 가리킴
      2. HostComponent의 경우, stateNode는 실제 백업 DOM 노드를 가리킴
    3. child, sibling, return : 함께 Tree 구조를 형성
    4. elementType : 우리가 제공하는 컴포넌트 함수 or 고유 HTML 태그
    5. flags
      1. Commit 단계에서 적용할 업데이트를 나타냄
      2. subtreeFlagsflags의 하위 트리
    6. lanes
      1. 보류중인 업데이트들의 우선순위를 나타냄
      2. childLaneslanes의 하위 트리
    7. memoizedState
      1. 중요한 데이터를 가리킴
      2. 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 코드 베이스에서 currentworkInProgress의 접두사는 어디에나 있음
    • React는 내부적으로 현재 상태를 표현하기 위해 Fiber Tree를 사용
    • 때문에 업데이트가 있을 때마다 새로운 트리를 생성하고 이전 트리와 비교해야 한다.
    • 그래서 current는 UI에 그려진 현재 버전을 의미하고, workInProgress는 빌드중이며, 다음 current로써 사용될 버전을 의미

    3.3. performUnitOfWork

    • React가 단일 Fiber Node에서 작동하여 완료되어야 하는 것들이 있는지 확인
    • 이 구역을 더 쉽게 이해하고 싶다면 먼저 React는 Fiber Tree를 어떻게 순회하는가를 먼저 확인하는 걸 추천
    • 저의 의견 짧으니까 참고하면 좋습니다. (아래 사진으로 한눈에 이해 가능)
    fiber traverse
    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가 없으므로 우리가 workInProgresschild를 설정하고 있음

    3.7. reconcileChildFibers vs mountChildFibers

    • reconcile의 목표 : 이미 가지고 있는 것을 재사용
    • mount는 '모든 것을 새로고치는 reconcile의 특별한 원시(primitive) 버전으로 취급
    • 이 둘은 크게 다르지 않고, 동일한 클로저이지만 shouldTrackSideEffects 플래그가 살짝 다름

    여기부터 조금 지쳤어요....
    코드레벨 분석은 배제하고 함수별 정리나 전체 플로우에 집중하기로 했습니다.

    3.8. reconcileSingleElement

    • 최초 마운트를 위한 함수
    • 새로 생성된 Fiber Node가 workInProgresschild 노드가 됨
    • 주목할 점 : 사용자 정의 컴포넌트에서 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. 요약

    1. Fiber Tree는 조정(reconciliation)하는 동안 느리게(lazily) 생성, 백업 DOM 노드는 동시에 생성되고 구성
    2. HostRoot의 직계 자식은 Placement로 표시
    3. "Commit" 단계에서는 Placement로 Fiber를 탐색. 부모가 HostRoot이므로, 해당 DOM 노드가 컨테이너에 삽입

    총평

    순차적이면 좋겠다

    저번부터 느꼈지만, 이걸 더 이해하려면 어떤 걸 봐라.. 뭘 이해하려면 저런 걸 봐라... 이렇게 입체적으로 포스트를 오가는 것들이 많은 것 같아요. 처음부터 deep한 내용을 다루다보니 어쩔 수 없는 부분인가 싶다가도 조금 아쉽다는 생각도 듭니다. 하긴 react 공식문서도 비슷했던 것 같아요.

    너무 어렵다...

    deep dive라는 이름처럼 정말 깊어요. 굉장히 복잡한 코드의 디테일을 다루다보니 코드 레벨에서 무슨 일이 벌어지고 있는지 추적하기 힘들기도 합니다. 정말... 정말 너무 어렵네요. 잘 흡수가 되고 있는지도 잘 모르겠어요.

    실제로 스터디 내부적으로도 스터디 대상을 바꾸는 것에 대한 검토가 이루어지고 있어 여기서 마치게 될 수도 있겠네요.

    References

    profile

    FE Developer 박승훈

    노력하는 자는 즐기는 자를 이길 수 없다