나머지 Fiber 만들기

복습

잠시 복습을 해보자. 아래 App 컴포넌트가 렌더되는 과정을 따라가고 있었다.

리액트 렌더링 과정은 새로운 Fiber 트리를 만들어내는 과정이다. workInProgress 변수는 새로운 Fiber 트리에서 현재 작업중인 Fiber를 가리킨다. current는 기존 Fiber 트리에서 workInProgress에 대응되는 기존 노드를 가리킨다.

performUnitOfWorkworkInProgress를 인자로 받아 작업을 수행하며 리턴할 시점에 workInProgress는 다음 작업해야할 Fiber 노드를 가리키게 된다.

// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
  // Perform work without checking if we need to yield between fiber.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

초기 렌더링 시에 current는 ReactDOM.createRoot 시점에 만들어진 HostRootFiber를 가리키며 workInProgressprepareFreshStack에서 만들어진 HostRootFiber를 가리킨다.

이후 reconcileChildFibers에서는 workInProgress를 처리하며 <App/>을 나타내는 Fiber를 만들고 이를 workinProgress의 child 노드로 설정한다. 이후 작업을 마치면 workInProgress = workInProgress.child로 설정된다.

따라서 두번째 performUnitOfWork의 인자로는 <App/>을 나타내는 Fiber가 건네진다.

performUnitOfWork

function performUnitOfWork(unitOfWork: Fiber): void {
  // `<App/>`에 대응되는 기존 Fiber는 없다.
  // 즉 current는 null이다.
  const current = unitOfWork.alternate;

  const next = beginWork(current, unitOfWork, entangledRenderLanes);

  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

여기서 unitOfWork = workInProgress = <App/> Fiber 임을 기억하자.

beginWork

App을 나타내는 Fiber는 FunctionComponent 태그(0)를 가지고 있다. 따라서 이번에는 HostRoot가 아닌 FunctionComponent 분기로 빠진다.

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  ...
  switch (workInProgress.tag) {
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        disableDefaultPropsExceptForClasses ||
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultPropsOnNonClassComponent(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    ...
  }
}

우리가 만든 App 함수가 Component라는 이름으로 등장한다! 해당 함수를 호출할 때 필요한 prop도 추출되었다. 프린트를 찍어보면 각각 App, {}(비어있음)임을 확인할 수 있다.

updateFunctionComponent

updateHostRoot때와 동일하게 reconcileChildren이 호출되지만 업데이트 큐에서 Element를 꺼내 nextChildren을 얻어낸 지난번과 다르게 이번에는 renderWithHooks을 호출해 얻어낸다.

function updateFunctionComponent(
  // null
  current: null | Fiber,
  // <App/>에 대응되는 Fiber
  workInProgress: Fiber,
  // App 함수
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
  let context;
  let nextChildren;
  let hasId;

  prepareToReadContext(workInProgress, renderLanes);

  nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps,
    context,
    renderLanes,
  );

  if (current !== null && !didReceiveUpdate) {
    bailoutHooks(current, workInProgress, renderLanes);
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  if (getIsHydrating() && hasId) {
    pushMaterializedTreeId(workInProgress);
  }

  // current가 null이므로 이번에는 mountChildFibers가 호출된다.
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  // App 하위의 div가 workInProgress.child로 설정되었다.
  return workInProgress.child;
}

renderWithHooks의 구현은 훅을 살펴봐야하기에 나중에 살펴보겠지만 아래처럼 우리가 만든 Component 함수를 호출하는 부분이 있음만 짚고 넘어가자.

let children = __DEV__
    ? callComponentInDEV(Component, props, secondArg)
    : Component(props, secondArg);

nextChildren을 로그 찍어보면 아래와 같으니 참고. App의 반환값에 최상단 div를 나타내는 Element가 있음을 확인할 수 있다. div의 자식 요소들은 props.children에 담겨있다.

{
  $$typeof: Symbol(react.transitional.element),
  type: 'div',
  key: null,
  props: {…},
  _owner: FiberNode,
  ...
}

reconcileChildren

reconcileChildren에는 최적화를 위해 기존 Fiber 트리의 유무에 따라 side effect를 추적하는 플래그를 기록할지 말지 결정하는 분기가 있음을 살펴봤었다.

App fiber에 해당하는 기존 Fiber 노드가 없으므로 이₩번에는 mountChildFibers가 호출되어 따로 수정사항에 대한 플래그를 기록하지 않는다 👍

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  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,
    );
  } else {
   ...
  }
}

mountChildFibers 호출 이후 workInProgress.child는 div에 해당하는 Fiber를 가리키게 되고 다음 performUnitOfWork에서는 이 Fiber를 처리할 것이다.

mountChildFibers의 내부는 지난번과 유사하니 넘어가고, 반환값만 찍어보자.

FiberNode {tag: 5, key: null, elementType: 'div', type: 'div',… }

지난번에 App Fiber 때와는 다르게 tag가 이번에는 0(FunctionComponent, App)이 아니고 5(HostComponent, div)임을 확인할 수 있다.

나머지...

div, p, a, button에 해당하는 Fiber를 처리할 때는 기존과 유사하다. updateHostComponent도 결국 mountChildFibersreconcileChildren을 호출한다.

다만 p를 처리할 때는 배열을 자식으로 가지기에 reconcileChildren에서 reconcileChildrenArry가 호출되는데 이건 key에 연관되어 복잡해지므로 다음에 살펴보자.

a와 button 모두 텍스트를 자식으로 가지지만 처리가 조금씩 다르니 이 부분만 확인해보자. 참고로 두 부분은 각각 이렇게 생겼었다:

// a
<a href="https://yeolyi.com">yeolyi.com</a>

// button
<button onClick={() => setCount((count) => count + 1)}>
    click me - {count}
</button>

텍스트 자식을 가진 경우

workInProgress가 a에 해당하는 Fiber일 때 workInProgress를 찍어보면 아래와 같다:

FiberNode {
    ...
    pendingProps: {
        href: 'https://yeolyi.com',
        children: 'yeolyi.com'
    },
    ...
}

updateHostComponent에서 이 Fiber를 어떻게 처리하는지 확인해보자:

function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  ...
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;

  let nextChildren = nextProps.children;
  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 {
    ...
  }

  ...
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

주석이 말하는대로 텍스트 자식을 가지는 경우에는 nextChildren을 null로 설정하고 reconcileChildren를 호출한다.

텍스트 자식들을 가지는 경우

workInProgress가 button에 해당하는 Fiber일 때 workInProgress를 찍어보면 아래와 같다:

FiberNode {
    ...
    pendingProps: {
        onClick: ƒ,
        children: ['click me - ', 0]
    },
    ...
}

updateHostComponent에서 이번에는 null이 아닌 이 배열을 reconcileChildren에 건네준다. 이후 아래와 같이 텍스트를 나타내는 Fiber가 두 개 만들어진다:

FiberNode { tag: 6, pendingProps: "click me -"}
FiberNode { tag: 6, pendingProps: "0"}

나중에 이들 Fiber를 처리할 때가 되면 updateHostTest가 호출된다. 의외로 updateHostTest는 별다른 처리 없이 그냥 리턴한다. 텍스트를 추가하는 작업은 Commit phase에서 하기 때문이다.

function updateHostText(current: null | Fiber, workInProgress: Fiber) {
  if (current === null) {
    tryToClaimNextHydratableTextInstance(workInProgress);
  }
  // Nothing to do here. This is terminal. We'll do the completion step
  // immediately after.
  return null;
}

이를 통해 Fiber 순회 순서를 로그 찍어봤을 때 a 하위에 Text Fiber는 없는데 button에는 있는 이유를 알 수 있다.

HostRootFiber
FunctionComponentFiber (App)
HostComponentFiber (div)
HostComponentFiber (p)
FunctionComponentFiber (Link)

// a만 호출하고 끝난다.
HostComponentFiber (a)

HostComponentFiber (br)

// button과 두 child를 호출한다.
HostComponentFiber (button)
HostTextFiber 
HostTextFiber 
이전글목록으로다음글

로그인 중...

seongyeol