첫 Fiber 만들기

HostRootFiber가 beginWork에서 처리되는 과정을 살펴보자.

beginWork

beginWork는 커다란 switch-case문으로 workInProgress의 Fiber tag에 따라 서로 다른 update 함수를 호출한다. HostRootFiberHostRoot case로 이동한다.

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  ...
  switch (workInProgress.tag) {
    case FunctionComponent: {
      ...
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    ...
  }
}

updateHostRoot

Update queue를 처리해 <App/>을 얻어낸 뒤 reconcileChildren을 호출한다. 큐의 처리 방법은 넘어가고 다음에 알아보자.

function updateHostRoot(
  current: null | Fiber,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // trigger phase에서 넘겼던 데이터를 추출해 
  // workInProgress의 memoizedState에 할당한다.
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);

  const nextState: RootState = workInProgress.memoizedState;
  // root.render에 넘겼던 element을 얻어냈다!
  const nextChildren = nextState.element;

  if (supportsHydration && prevState.isDehydrated) {
    ...
  } else {
    ...
    if (nextChildren === prevChildren) {
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }
  return workInProgress.child;
}

reconcileChildren

reconcileChildren은 아주 핵심적인 함수이다.

  1. nextChildren(여기서는 <App/>)을 보며 workInProgress 트리의 child 노드를 만든다.
  2. 이 과정에서 기존 UI를 나타내는 current트리와 비교하며 Fiber의 삭제, 수정같은 side effect들을 체크한다.
export function reconcileChildren(
  // updateContainer에서 업데이트 큐에 넣어둔 HostRootFiber
  current: Fiber | null,
  // prepareFreshStack에서 만든 HostRootFiber
  workInProgress: Fiber,
  // <App/>
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 지금 current는 HostRootFiber이므로 여기로 간다.
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

current의 유무에 따라, mountChildFibers 혹은 reconcileChildFibers를 호출한다. 두 함수의 차이점을 이어서 살펴보자.

mountChildFibers vs reconcileChildFibers

두 함수는 createChildReconciler에 전달되는 인자만 다르다.

export const reconcileChildFibers: ChildReconciler =
  createChildReconciler(true);
export const mountChildFibers: ChildReconciler = createChildReconciler(false);

createChildReconciler는 아래와 같이 구현되어있다. shouldTrackSideEffects을 사용하는 함수들이 본문에 정의되어있다.

function createChildReconciler(
  shouldTrackSideEffects: boolean,
): ChildReconciler {
  function deleteChild(){}
  function deleteRemainingChildren(){}
  ...
  function placeChild(){}
  function placeSingleChild(){}
  function updateTextNode(){}
  function updateElement(){}
  ...
  function createChild(){}
  ...
  function reconcileChildrenArray(){}
  ...
  function reconcileSingleTextNode(){}
  function reconcileSingleElement(){}
  ...

  function reconcileChildFibersImpl(){}
  function reconcileChildFibers(){}

  return reconcileChildFibers;
}

reconcileChildFibersshouldTrackSideEffects가 true, shouldTrackSideEffects가 false이다. 원래 없던 Fiber 노드를 새로 마운트하는 경우에는 side effect가 있을 수 없고 그냥 새로 붙이면 되므로 따로 side effect를 추적하지 않음으로서 최적화하는 것이다.

reconcileChildFibers

지금 시점에는 reconcileChildFibers가 호출된다고 했다. reconcileChildFibers에 전달하는 인자들을 돌아보자.

workInProgress.child = reconcileChildFibers(
    // prepareFreshStack에서 만든 HostRootFiber
    workInProgress,
    // current는 updateContainer에서 업데이트 큐에 넣어둔 HostRootFiber
    // current가 아닌 current.child를 넘겨준다.
    current.child,
    // <App/>
    nextChildren,
    renderLanes,
);

reconcileChildFibersreconcileChildFibersImpl을 호출하고 예외처리를 한 것이다. reconcileChildFibers는 넘어가고 바로 reconcileChildFibersImpl을 살펴보자. newChild의 종류에 따라 분기한다.

function reconcileChildFibersImpl(
  // 전달인자명은 workInProgress이었지만 매개변수명은 returnFiber이다
  // 현재 처리중인 Fiber의 부모 노드(return필드)가 workInProgress가 될 것이니 의미상 적절하다
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChild: any,
  lanes: Lanes,
): Fiber | null {
  // Handle object types
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      // 여기서는 React Element를 처리한다.
      // 예를 들어 <App/>의 $$typeof는 REACT_ELEMENT_TYPE이다.
      case REACT_ELEMENT_TYPE: {
        const prevDebugInfo = pushDebugInfo(newChild._debugInfo);
        const firstChild = placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
        currentDebugInfo = prevDebugInfo;
        return firstChild;
      }
      case REACT_PORTAL_TYPE:
        ...
      case REACT_LAZY_TYPE: 
        ...
    }

    ...

    throwOnInvalidObjectType(returnFiber, newChild);
  }

  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number' ||
    typeof newChild === 'bigint'
  ) {
    // 여기서는 텍스트 노드를 처리한다.
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild,
        lanes,
      ),
    );
  }

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

beginWork에서 했던 분기와 헷갈릴 수 있는데 그 때는 Fiber의 종류에 따른 분기, 이번에는 newChild Element의 종류에 따른 분기이다.

$$typeof 구경

$$typeof가 나온김에 살펴보자면 shared/ReactSymbols.js에 아래와 같이 이것저것 심볼로 정의되어있다:

// The Symbol used to tag the ReactElement-like types.
export const REACT_LEGACY_ELEMENT_TYPE: symbol = Symbol.for('react.element');
export const REACT_ELEMENT_TYPE: symbol = renameElementSymbol
  ? Symbol.for('react.transitional.element')
  : REACT_LEGACY_ELEMENT_TYPE;
export const REACT_PORTAL_TYPE: symbol = Symbol.for('react.portal');
export const REACT_FRAGMENT_TYPE: symbol = Symbol.for('react.fragment');
...

console.log(<App/>)만 찍어봐도 바로 확인할 수 있다. $$typeof에 대한 더 많은 내용은 https://overreacted.io/why-do-react-elements-have-typeof-property/ 에서 확인할 수 있다.

Element 종류 무관하게 분기들에 reconcileSingleTextNodeplaceSingleChild가 공통적으로 사용됨을 확인할 수 있다. 이 둘을 살펴보자.

reconcileSingleElement

  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {
      // 초기 렌더시에는 child가 null이므로 해당하지 않는다.
      ...
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      ...
    } else {
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      coerceRef(created, element);
      created.return = returnFiber;
      return created;
    }
  }

간단하다. createFiberFromElement<App/> element를 나타내는 새로운 Fiber를 만들어 반환한다. createFiberFromElement의 내부 코드를 보면 element의 타입에 따라 Fiber의 tag를 결정하는 길고 긴 switch문이 있음을 확인할 수 있다.

자세히는 살펴보지 않겠지만 여기서 App 함수가 실행되는 것은 아니라는 점을 참고하자. 그저 App 함수 컴포넌트를 나타내는 Fiber 객체를 만들 뿐이다.

만들어진 Fiber 객체를 보면 아래처럼 생겼다:

FiberNode {
  ...
  elementType: App,
  tag: 0, 
  type: App,
  ...
}

placeSingleChild

만들어진 Fiber는 placeSingleChild에서 Placement 플래그가 세워진다.

function placeSingleChild(newFiber: Fiber): Fiber {
  // 아까 reconcile vs mount할 때 봤던 shouldTrackSideEffects가 나온다.
  // 지금은 reconcile중이라 true이므로 newFiber에 flag를 세운다.
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.flags |= Placement | PlacementDEV;
  }
  return newFiber;
}

다음 순회

placeSingleChild에서 반환한 값은 beginWork에서 그대로 반환하고 performUnitOfWork에서 다음 workInProgress로 설정된다.

결과적으로 다음 performUnitOfWork에서는 방금 새로 만든 Fiber에 대해 작업을 시작한다.

이전글목록으로다음글

로그인 중...

seongyeol