React

[ Frontend > React ]

[리액트 19] 4편. Reconciler 동작 흐름

 Carrot Yoon
 2025-04-30
 36

4편. Reconciler 동작 흐름

지금까지 거시적인 패키지 구조와 주요 두 작업루프에 대한 설명을 통해 react-reconciler 패키지에 대해 어느 정도 이해했습니다.

4편에서는 react-reconciler 패키지의 주요 역할을 정리하며 주요 기능들을 4가지로 나눠서 설명하겠습니다.

  1. 입력: schedulerUpdateOnFiber와 같은 API 함수를 외부에 노출하여, react 패키지 등 다른 패키지에서 호출할 수 있도록 합니다.

  2. 스케줄링 Task 등록: scheduler 패키지와 상호작용하여 task를 등록하고 콜백을 기다립니다.

  3. task 콜백 실행: fiber 트리를 구성하고, 동시에 렌더러(react-dom 또는 react-native)와 상호작용하여 fiber에 대응하는 DOM 노드를 생성합니다.

  4. 출력: 렌더러와 상호작용하여 실제 DOM 노드를 렌더링합니다. (커밋)

리액트 6.webp

위 1~4번 과정을 그림에서 나타냈습니다. (ReactFiberWorkLoop.js, ReactFiberRootScheduler.js)

1. 입력

ReactFiberWorkLoop.js에서 입력을 담당하는 sccheduleUpdateOnFiber 뿐입니다. react-reconciler 패키지에서 외부에 노출되는 api 함수들 중에서 fiber를 변경해야 하는 작업이 포함된다면 모두다 최종적으로는 scheduleUpdateOnFiber를 호출하게 됩니다. 따라서 scheduleUpdateOnFiber 함수는 입력 흐름에서 반드시 거쳐야 하는 경로입니다.

export function scheduleUpdateOnFiber(  root: FiberRoot,  fiber: Fiber,  lane: Lane,) {  // Check if the work loop is currently suspended and waiting for data to  // finish loading.  if (    // Suspended render phase    (root === workInProgressRoot &&      (workInProgressSuspendedReason === SuspendedOnData ||        workInProgressSuspendedReason === SuspendedOnAction)) ||    // Suspended commit phase    root.cancelPendingCommit !== null  ) {    // The incoming update might unblock the current render. Interrupt the    // current attempt and restart from the top.    prepareFreshStack(root, NoLanes);    const didAttemptEntireTree = false;    markRootSuspended(      root,      workInProgressRootRenderLanes,      workInProgressDeferredLane,      didAttemptEntireTree,    );  }  // Mark that the root has a pending update.  markRootUpdated(root, lane);  if (    (executionContext & RenderContext) !== NoLanes &&    root === workInProgressRoot  ) {    // This update was dispatched during the render phase. This is a mistake    // if the update originates from user space (with the exception of local    // hook updates, which are handled differently and don't reach this    // function), but there are some internal React features that use this as    // an implementation detail, like selective hydration.    warnAboutRenderPhaseUpdatesInDEV(fiber);    // Track lanes that were updated during the render phase    workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(      workInProgressRootRenderPhaseUpdatedLanes,      lane,    );  } else {    if (root === workInProgressRoot) {      // Received an update to a tree that's in the middle of rendering. Mark      // that there was an interleaved update work on this root.      if ((executionContext & RenderContext) === NoContext) {        workInProgressRootInterleavedUpdatedLanes = mergeLanes(          workInProgressRootInterleavedUpdatedLanes,          lane,        );      }      if (workInProgressRootExitStatus === RootSuspendedWithDelay) {        // The root already suspended with a delay, which means this render        // definitely won't finish. Since we have a new update, let's mark it as        // suspended now, right before marking the incoming update. This has the        // effect of interrupting the current render and switching to the update.        // TODO: Make sure this doesn't override pings that happen while we've        // already started rendering.        const didAttemptEntireTree = false;        markRootSuspended(          root,          workInProgressRootRenderLanes,          workInProgressDeferredLane,          didAttemptEntireTree,        );      }    }    // 스케줄러(Scheduler) 패키지를 통한 스케줄링 작업 등록,    // 간접적으로 `fiber 구성` 수행    ensureRootIsScheduled(root);}

scheduleUpdateOnFiber 함수로 진입한 이후 항상 ensureRootIsScheduled가 실행되는 것을 알 수 있습니다. (React 18버전 이전에는 SyncLane인지 확인한 후에 스케줄링을 거치지 않고 직접 fiber를 구성하는 함수가 있었습니다. 하지만 현재는 모든 업데이트가 Scheduler를 거쳐서 fiber를 구성합니다.)

2. 스케줄링 Task 등록

위 코드 예시를 보면 scheduleUpdateOnFiber 함수 실행 후 ensureRootIsScheduled 함수를 실행합니다.

// 중복된 마이크로태스크가 예약되는 것을 방지하기 위해 사용됩니다.let didScheduleMicrotask: boolean = false;// 동기화 작업(sync work)이 없으면 flushSync에서 빠르게 탈출하기 위해 사용됩니다.(let mightHavePendingSyncWork: boolean = false;export function ensureRootIsScheduled(root: FiberRoot): void {  // 이 함수는 root가 update를 받을때마다 항상 실행됩니다. 이 함수는 2가지 기능을 수행합니다.  // 1) root가 root schedule에 포함되어 있는지 확인합니다.  // 2) root schedule을 처리하기 위한 pending microtask가 있는지 확인합니다.  //  // 실제 스케줄링 로직의 대부분은 `scheduleTaskForRootDuringMicrotask`가 실행될 때 수행됩니다.  // Add the root to the schedule  if (root === lastScheduledRoot || root.next !== null) {    // Fast path. This root is already scheduled.  } else {    if (lastScheduledRoot === null) {      firstScheduledRoot = lastScheduledRoot = root;    } else {      lastScheduledRoot.next = root;      lastScheduledRoot = root;    }  }  // Any time a root received an update, we set this to true until the next time  // we process the schedule. If it's false, then we can quickly exit flushSync  // without consulting the schedule.  mightHavePendingSyncWork = true;  if (!didScheduleMicrotask) {    didScheduleMicrotask = true;    scheduleImmediateRootScheduleTask();  }}

function scheduleImmediateRootScheduleTask() {  // TODO: Can we land supportsMicrotasks? Which environments don't support it?  // Alternatively, can we move this check to the host config?  if (supportsMicrotasks) {    scheduleMicrotask(() => {      // In Safari, appending an iframe forces microtasks to run.      // https://github.com/facebook/react/issues/22459      // We don't support running callbacks in the middle of render      // or commit so we need to check against that.      const executionContext = getExecutionContext();      if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {        // Note that this would still prematurely flush the callbacks        // if this happens outside render or commit phase (e.g. in an event).        // Intentionally using a macrotask instead of a microtask here. This is        // wrong semantically but it prevents an infinite loop. The bug is        // Safari's, not ours, so we just do our best to not crash even though        // the behavior isn't completely correct.        Scheduler_scheduleCallback(          ImmediateSchedulerPriority,          processRootScheduleInImmediateTask,        );        return;      }      // 처리할 작업이 없으면 실행.      processRootScheduleInMicrotask();    });  } else {    // micarotask가 지원되지 않는 브라우저나 환경에서 scheduler 바로 예약    Scheduler_scheduleCallback(      ImmediateSchedulerPriority,      processRootScheduleInImmediateTask,    );  }}

ensureRootIsScheduled는 scheduled되었는지 확인하고 scheduleImmediateRootScheduleTask를 통해 Scheduler로 task가 예약되는 것을 확인할 수 있습니다.

3. task 콜백 실행

task 콜백은 performWorkOnRoot를 통해 실행됩니다. 이 함수의 파라미터 값 중에 forceSync 값이 true이면 performSyncWorkOnRoot를 실행하고, false면 performConcurrentWorkOnRoot 함수를 실행합니다.

이 단계에서는 아래와 같이 3가지 과정을 거칩니다.

  1. shouldTimeSlice(Concurrent 여부) 체크 후 Fiber 트리 구성.

    • renderRootSync일 경우 renderRootSync 함수(React19에서는 Legacy 호환용으로 거의 사용 X)

      • renderRootSync는 렌더링 중간에 중단이 불가능합니다.

    • remderRootConcurrent일 경우 renderRootConcurrent 힘수

      • renderRootConcurrent는 렌더링 중간에 중단이 가능하며, 렌더링 과정 중인지, 이전 렌더링을 복구해야 하는지 확인합니다.

      • 중간에 중단되었다면, performWorkOnRootViaSchedulerTask 함수를 다시 스케줄링하여 이어서 처리합니다.

  2. 예외 처리(Suspense일 경우 해당 상태로 변경하여 출력시 반영)

  3. performWorkOnRoot => finishConcurrentRender => commitRootWhenReady => commitRoot 함수로 출력 (commit)

performWorkOnRoot
export function performWorkOnRoot(  root: FiberRoot,  lanes: Lanes,  forceSync: boolean,): void {  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {    throw new Error('Should not already be working.');  }  // 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.  const shouldTimeSlice =    (!forceSync &&      !includesBlockingLane(lanes) &&      !includesExpiredLane(root, lanes)) ||    // If we're prerendering, then we should use the concurrent work loop    // even if the lanes are synchronous, so that prerendering never blocks    // the main thread.    // TODO: We should consider doing this whenever a sync lane is suspended,    // even for regular pings.    (enableSiblingPrerendering && checkIfRootIsPrerendering(root, lanes));  // 1. TimeSlice할지(Conccurent하게 할지) 여부에 따라 실행하는 함수가 달라진다.  // 1. Fiber 트리를 구성.  let exitStatus = shouldTimeSlice    ? renderRootConcurrent(root, lanes)    : renderRootSync(root, lanes, true);  let renderWasConcurrent = shouldTimeSlice;  // 2. Fiber 트리 구성 과정에서 예외가 발생시 예외 처리. Suspense 처리.  do {    if (exitStatus === RootInProgress) {      // Render phase is still in progress.      if (        enableSiblingPrerendering &&        workInProgressRootIsPrerendering &&        !shouldTimeSlice      ) {        // We're in prerendering mode, but time slicing is not enabled. This        // happens when something suspends during a synchronous update. Exit the        // the work loop. When we resume, we'll use the concurrent work loop so        // that prerendering is non-blocking.        //        // Mark the root as suspended. Usually we do this at the end of the        // render phase, but we do it here so that we resume in        // prerendering mode.        // TODO: Consider always calling markRootSuspended immediately.        // Needs to be *after* we attach a ping listener, though.        const didAttemptEntireTree = false;        markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);      }      break;    } else {      // The render completed.      // Check if this render may have yielded to a concurrent event, and if so,      // confirm that any newly rendered stores are consistent.      // TODO: It's possible that even a concurrent render may never have yielded      // to the main thread, if it was fast enough, or if it expired. We could      // skip the consistency check in that case, too.      const finishedWork: Fiber = (root.current.alternate: any);      if (        renderWasConcurrent &&        !isRenderConsistentWithExternalStores(finishedWork)      ) {        if (enableProfilerTimer && enableComponentPerformanceTrack) {          setCurrentTrackFromLanes(lanes);          logInconsistentRender(renderStartTime, renderEndTime);          finalizeRender(lanes, renderEndTime);        }        // A store was mutated in an interleaved event. Render again,        // synchronously, to block further mutations.        exitStatus = renderRootSync(root, lanes, false);        // We assume the tree is now consistent because we didn't yield to any        // concurrent events.        renderWasConcurrent = false;        // Need to check the exit status again.        continue;      }      // 에러 발생했는지 확인      if (        (disableLegacyMode || root.tag !== LegacyRoot) &&        exitStatus === RootErrored      ) {        const lanesThatJustErrored = lanes;        const errorRetryLanes = getLanesToRetrySynchronouslyOnError(          root,          lanesThatJustErrored,        );        if (errorRetryLanes !== NoLanes) {          if (enableProfilerTimer && enableComponentPerformanceTrack) {            setCurrentTrackFromLanes(lanes);            logErroredRenderPhase(renderStartTime, renderEndTime, lanes);            finalizeRender(lanes, renderEndTime);          }          lanes = errorRetryLanes;          exitStatus = recoverFromConcurrentError(            root,            lanesThatJustErrored,            errorRetryLanes,          );          renderWasConcurrent = false;          // Need to check the exit status again.          if (exitStatus !== RootErrored) {            // The root did not error this time. Restart the exit algorithm            // from the beginning.            // TODO: Refactor the exit algorithm to be less confusing. Maybe            // more branches + recursion instead of a loop. I think the only            // thing that causes it to be a loop is the RootSuspendedAtTheShell            // check. If that's true, then we don't need a loop/recursion            // at all.            continue;          }        }      }      if (exitStatus === RootFatalErrored) {        prepareFreshStack(root, NoLanes);        // Since this is a fatal error, we're going to pretend we attempted        // the entire tree, to avoid scheduling a prerender.        const didAttemptEntireTree = true;        markRootSuspended(root, lanes, NoLane, didAttemptEntireTree);        break;      }      // We now have a consistent tree. The next step is either to commit it,      // or, if something suspended, wait to commit it after a timeout.      // 3. 아래 함수 사용하여 출력.(commit)      finishConcurrentRender(        root,        exitStatus,        finishedWork,        lanes,        renderEndTime,      );    }    break;  } while (true);  // 종료 전에 다시 확인: 다른 업데이트가 있는지, 새 스케줄이 필요한지.  ensureRootIsScheduled(root);}

performSyncWorkOnRoot, performWorkOnRootViaSchedulerTask
function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes) {  // This is the entry point for synchronous tasks that don't go  // through Scheduler.  const didFlushPassiveEffects = flushPendingEffects();  if (didFlushPassiveEffects) {    // If passive effects were flushed, exit to the outer work loop in the root    // scheduler, so we can recompute the priority.    return null;  }  const forceSync = true;  performWorkOnRoot(root, lanes, forceSync);}// SchedulerTask를 통해 workfunction performWorkOnRootViaSchedulerTask(  root: FiberRoot,  didTimeout: boolean,): RenderTaskFn | null {  // This is the entry point for concurrent tasks scheduled via Scheduler (and  // postTask, in the future).  if (hasPendingCommitEffects()) {    // We are currently in the middle of an async committing (such as a View Transition).    // We could force these to flush eagerly but it's better to defer any work until    // it finishes. This may not be the same root as we're waiting on.    // TODO: This relies on the commit eventually calling ensureRootIsScheduled which    // always calls processRootScheduleInMicrotask which in turn always loops through    // all the roots to figure out. This is all a bit inefficient and if optimized    // it'll need to consider rescheduling a task for any skipped roots.    root.callbackNode = null;    root.callbackPriority = NoLane;    return null;  }  // Flush any pending passive effects before deciding which lanes to work on,  // in case they schedule additional work.  const originalCallbackNode = root.callbackNode;  const didFlushPassiveEffects = flushPendingEffects(true);  if (didFlushPassiveEffects) {    // Something in the passive effect phase may have canceled the current task.    // Check if the task node for this root was changed.    if (root.callbackNode !== originalCallbackNode) {      // The current task was canceled. Exit. We don't need to call      // `ensureRootIsScheduled` because the check above implies either that      // there's a new task, or that there's no remaining work on this root.      return null;    } else {      // Current task was not canceled. Continue.    }  }  // Determine the next lanes to work on, using the fields stored on the root.  // TODO: We already called getNextLanes when we scheduled the callback; we  // should be able to avoid calling it again by stashing the result on the  // root object. However, because we always schedule the callback during  // a microtask (scheduleTaskForRootDuringMicrotask), it's possible that  // an update was scheduled earlier during this same browser task (and  // therefore before the microtasks have run). That's because Scheduler batches  // together multiple callbacks into a single browser macrotask, without  // yielding to microtasks in between. We should probably change this to align  // with the postTask behavior (and literally use postTask when  // it's available).  const workInProgressRoot = getWorkInProgressRoot();  const workInProgressRootRenderLanes = getWorkInProgressRootRenderLanes();  const rootHasPendingCommit =    root.cancelPendingCommit !== null || root.timeoutHandle !== noTimeout;  const lanes = getNextLanes(    root,    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,    rootHasPendingCommit,  );  if (lanes === NoLanes) {    // No more work on this root.    return null;  }  // Enter the work loop.  // 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 forceSync = !disableSchedulerTimeoutInWorkLoop && didTimeout;  //performWorkOnRoot 실행.  performWorkOnRoot(root, lanes, forceSync);  // The work loop yielded, but there may or may not be work left at the current  // priority. Need to determine whether we need to schedule a continuation.  // Usually `scheduleTaskForRootDuringMicrotask` only runs inside a microtask;  // however, since most of the logic for determining if we need a continuation  // versus a new task is the same, we cheat a bit and call it here. This is  // only safe to do because we know we're at the end of the browser task.  // So although it's not an actual microtask, it might as well be.  scheduleTaskForRootDuringMicrotask(root, now());  if (root.callbackNode != null && root.callbackNode === originalCallbackNode) {    // The task node scheduled for this root is the same one that's    // currently executed. Need to return a continuation.    return performWorkOnRootViaSchedulerTask.bind(null, root);  }  return null;}

4. 출력

출력 단계에서는 commitRoot 함수를 통해 Effetcs가 실행되고, 화면에 그려집니다. 크게 3단계로 나눠진다고 볼 수 있습니다.

  1. commitBeforeMutationEffects

    • DOM 변경 이전 단계. fiber.flags가 Snapshot, Passive에 해당하는 경우 처리합니다.

  2. commitMutationEffects

    • host tree(DOM 또는 RN)을 변경하는 단계. fiber.flags가 Placement, Update, Deletion, Hydrating에 해당하는 경우 처리합니다.

  3. commitLayoutEffects

    • DOM 변경 이후, Effects(Side Effects)를 실행하는 단계입니다. fiber.flags가 Update, Callback인 경우 처리합니다.

ReactFiberFlags.js FiberWorkLoop.js(commitRoot)

finishConcurrentRender, commitRootWhenReady
function finishConcurrentRender(  root: FiberRoot,  exitStatus: RootExitStatus,  finishedWork: Fiber,  lanes: Lanes,  renderEndTime: number, // Profiling-only) {  // TODO: The fact that most of these branches are identical suggests that some  // of the exit statuses are not best modeled as exit statuses and should be  // tracked orthogonally.  switch (exitStatus) {    case RootInProgress:    case RootFatalErrored: {      throw new Error('Root did not complete. This is a bug in React.');    }    case RootSuspendedWithDelay: {      if (!includesOnlyTransitions(lanes)) {        // Commit the placeholder.        break;      }    }    // Fallthrough    case RootSuspendedAtTheShell: {      const didAttemptEntireTree = !workInProgressRootDidSkipSuspendedSiblings;      markRootSuspended(        root,        lanes,        workInProgressDeferredLane,        didAttemptEntireTree,      );      return;    }    case RootErrored: {      // This render errored. Ignore any recoverable errors because we weren't actually      // able to recover. Instead, whatever the final errors were is the ones we log.      // This ensures that we only log the actual client side error if it's just a plain      // error thrown from a component on the server and the client.      workInProgressRootRecoverableErrors = null;      break;    }    case RootSuspended:    case RootCompleted: {      break;    }    default: {      throw new Error('Unknown root exit status.');    }  }    if (      includesOnlyRetries(lanes) &&      (alwaysThrottleRetries || exitStatus === RootSuspended)    ) {      // This render only included retries, no updates. Throttle committing      // retries so that we don't show too many loading states too quickly.      const msUntilTimeout =        globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();      // Don't bother with a very short suspense time.      if (msUntilTimeout > 10) {        const didAttemptEntireTree =          !workInProgressRootDidSkipSuspendedSiblings;        markRootSuspended(          root,          lanes,          workInProgressDeferredLane,          didAttemptEntireTree,        );        const nextLanes = getNextLanes(root, NoLanes, true);        if (nextLanes !== NoLanes) {          // There's additional work we can do on this root. We might as well          // attempt to work on that while we're suspended.          return;        }        // The render is suspended, it hasn't timed out, and there's no        // lower priority work to do. Instead of committing the fallback        // immediately, wait for more data to arrive.        // TODO: Combine retry throttling with Suspensey commits. Right now they        // run one after the other.        root.timeoutHandle = scheduleTimeout(          commitRootWhenReady.bind(            null,            root,            finishedWork,            workInProgressRootRecoverableErrors,            workInProgressTransitions,            workInProgressRootDidIncludeRecursiveRenderUpdate,            lanes,            workInProgressDeferredLane,            workInProgressRootInterleavedUpdatedLanes,            workInProgressSuspendedRetryLanes,            workInProgressRootDidSkipSuspendedSiblings,            exitStatus,            THROTTLED_COMMIT,            renderStartTime,            renderEndTime,          ),          msUntilTimeout,        );        return;      }    }    // 출력: Fiber 트리를 기반으로 실제 렌더링(Coomit).    commitRootWhenReady(      root,      finishedWork,      workInProgressRootRecoverableErrors,      workInProgressTransitions,      workInProgressRootDidIncludeRecursiveRenderUpdate,      lanes,      workInProgressDeferredLane,      workInProgressRootInterleavedUpdatedLanes,      workInProgressSuspendedRetryLanes,      workInProgressRootDidSkipSuspendedSiblings,      exitStatus,      IMMEDIATE_COMMIT,      renderStartTime,      renderEndTime,    );}
function commitRootWhenReady(  root: FiberRoot,  finishedWork: Fiber,  recoverableErrors: Array<CapturedValue<mixed>> | null,  transitions: Array<Transition> | null,  didIncludeRenderPhaseUpdate: boolean,  lanes: Lanes,  spawnedLane: Lane,  updatedLanes: Lanes,  suspendedRetryLanes: Lanes,  didSkipSuspendedSiblings: boolean,  exitStatus: RootExitStatus,  suspendedCommitReason: SuspendedCommitReason, // Profiling-only  completedRenderStartTime: number, // Profiling-only  completedRenderEndTime: number, // Profiling-only) {  root.timeoutHandle = noTimeout;  // TODO: Combine retry throttling with Suspensey commits. Right now they run  // one after the other.  const BothVisibilityAndMaySuspendCommit = Visibility | MaySuspendCommit;  const subtreeFlags = finishedWork.subtreeFlags;  const isViewTransitionEligible =    enableViewTransition && includesOnlyViewTransitionEligibleLanes(lanes); // TODO: Use a subtreeFlag to optimize.  const isGestureTransition = enableSwipeTransition && isGestureRender(lanes);  const maySuspendCommit =    subtreeFlags & ShouldSuspendCommit ||    (subtreeFlags & BothVisibilityAndMaySuspendCommit) ===      BothVisibilityAndMaySuspendCommit;  if (isViewTransitionEligible || maySuspendCommit || isGestureTransition) {    // Before committing, ask the renderer whether the host tree is ready.    // If it's not, we'll wait until it notifies us.    startSuspendingCommit();    // This will walk the completed fiber tree and attach listeners to all    // the suspensey resources. The renderer is responsible for accumulating    // all the load events. This all happens in a single synchronous    // transaction, so it track state in its own module scope.    // This will also track any newly added or appearing ViewTransition    // components for the purposes of forming pairs.    accumulateSuspenseyCommit(finishedWork);    if (isViewTransitionEligible || isGestureTransition) {      // If we're stopping gestures we don't have to wait for any pending      // view transition. We'll stop it when we commit.      if (!enableSwipeTransition || root.stoppingGestures === null) {        suspendOnActiveViewTransition(root.containerInfo);      }    }    // At the end, ask the renderer if it's ready to commit, or if we should    // suspend. If it's not ready, it will return a callback to subscribe to    // a ready event.    const schedulePendingCommit = waitForCommitToBeReady();    if (schedulePendingCommit !== null) {      // NOTE: waitForCommitToBeReady returns a subscribe function so that we      // only allocate a function if the commit isn't ready yet. The other      // pattern would be to always pass a callback to waitForCommitToBeReady.      // Not yet ready to commit. Delay the commit until the renderer notifies      // us that it's ready. This will be canceled if we start work on the      // root again.      root.cancelPendingCommit = schedulePendingCommit(        commitRoot.bind(          null,          root,          finishedWork,          lanes,          recoverableErrors,          transitions,          didIncludeRenderPhaseUpdate,          spawnedLane,          updatedLanes,          suspendedRetryLanes,          exitStatus,          SUSPENDED_COMMIT,          completedRenderStartTime,          completedRenderEndTime,        ),      );      const didAttemptEntireTree = !didSkipSuspendedSiblings;      markRootSuspended(root, lanes, spawnedLane, didAttemptEntireTree);      return;    }  }  // Otherwise, commit immediately.;  commitRoot(    root,    finishedWork,    lanes,    recoverableErrors,    transitions,    didIncludeRenderPhaseUpdate,    spawnedLane,    updatedLanes,    suspendedRetryLanes,    exitStatus,    suspendedCommitReason,    completedRenderStartTime,    completedRenderEndTime,  );}
commitRoot
function commitRoot(  root: FiberRoot,  finishedWork: null | Fiber,  lanes: Lanes,  recoverableErrors: null | Array<CapturedValue<mixed>>,  transitions: Array<Transition> | null,  didIncludeRenderPhaseUpdate: boolean,  spawnedLane: Lane,  updatedLanes: Lanes,  suspendedRetryLanes: Lanes,  exitStatus: RootExitStatus,  suspendedCommitReason: SuspendedCommitReason, // Profiling-only  completedRenderStartTime: number, // Profiling-only  completedRenderEndTime: number, // Profiling-only): void {  root.cancelPendingCommit = null;  do {    // `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which    // means `flushPassiveEffects` will sometimes result in additional    // passive effects. So we need to keep flushing in a loop until there are    // no more pending effects.    // TODO: Might be better if `flushPassiveEffects` did not automatically    // flush synchronous work at the end, to avoid factoring hazards like this.    flushPendingEffects();  } while (pendingEffectsStatus !== NO_PENDING_EFFECTS);  flushRenderPhaseStrictModeWarningsInDEV();  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {    throw new Error('Should not already be working.');  }  if (finishedWork === null) {    if (enableSwipeTransition) {      // Stop any gestures that were completed and is now being reverted.      if (root.stoppingGestures !== null) {        stopCompletedGestures(root);      }    }    return;  }  if (finishedWork === root.current) {    throw new Error(      'Cannot commit the same tree as before. This error is likely caused by ' +        'a bug in React. Please file an issue.',    );  }  // Check which lanes no longer have any work scheduled on them, and mark  // those as finished.  let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);  // Make sure to account for lanes that were updated by a concurrent event  // during the render phase; don't mark them as finished.  const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();  remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);  if (enableSwipeTransition && root.pendingGestures === null) {    // Gestures don't clear their lanes while the gesture is still active but it    // might not be scheduled to do any more renders and so we shouldn't schedule    // any more gesture lane work until a new gesture is scheduled.    remainingLanes &= ~GestureLane;  }  markRootFinished(    root,    lanes,    remainingLanes,    spawnedLane,    updatedLanes,    suspendedRetryLanes,  );  // Reset this before firing side effects so we can detect recursive updates.  didIncludeCommitPhaseUpdate = false;  if (root === workInProgressRoot) {    // We can reset these now that they are finished.    workInProgressRoot = null;    workInProgress = null;    workInProgressRootRenderLanes = NoLanes;  } else {    // This indicates that the last root we worked on is not the same one that    // we're committing now. This most commonly happens when a suspended root    // times out.  }  // workInProgressX might be overwritten, so we want  // to store it in pendingPassiveX until they get processed  // We need to pass this through as an argument to commitRoot  // because workInProgressX might have changed between  // the previous render and commit if we throttle the commit  // with setTimeout  pendingFinishedWork = finishedWork;  pendingEffectsRoot = root;  pendingEffectsLanes = lanes;  pendingEffectsRemainingLanes = remainingLanes;  pendingPassiveTransitions = transitions;  pendingRecoverableErrors = recoverableErrors;  pendingDidIncludeRenderPhaseUpdate = didIncludeRenderPhaseUpdate;  if (enableSwipeTransition && isGestureRender(lanes)) {    // This is a special kind of render that doesn't commit regular effects.    commitGestureOnRoot(      root,      finishedWork,      recoverableErrors,      enableProfilerTimer        ? suspendedCommitReason === IMMEDIATE_COMMIT          ? completedRenderEndTime          : commitStartTime        : 0,    );    return;  }  // If there are pending passive effects, schedule a callback to process them.  // Do this as early as possible, so it is queued before anything else that  // might get scheduled in the commit phase. (See #16714.)  // TODO: Delete all other places that schedule the passive effect callback  // They're redundant.  let passiveSubtreeMask;  if (enableViewTransition) {    pendingViewTransitionEvents = null;    if (includesOnlyViewTransitionEligibleLanes(lanes)) {      // Claim any pending Transition Types for this commit.      // This means that multiple roots committing independent View Transitions      // 1) end up staggered because we can only have one at a time.      // 2) only the first one gets all the Transition Types.      pendingTransitionTypes = ReactSharedInternals.V;      ReactSharedInternals.V = null;      passiveSubtreeMask = PassiveTransitionMask;    } else {      pendingTransitionTypes = null;      passiveSubtreeMask = PassiveMask;    }  } else {    passiveSubtreeMask = PassiveMask;  }   // If we don't have passive effects, we're not going to need to perform more work  // so we can clear the callback now.  root.callbackNode = null;  root.callbackPriority = NoLane;  resetShouldStartViewTransition();  // The commit phase is broken into several sub-phases. We do a separate pass  // of the effect list for each phase: all mutation effects come before all  // layout effects, and so on.  // Check if there are any effects in the whole tree.  // TODO: This is left over from the effect list implementation, where we had  // to check for the existence of `firstEffect` to satisfy Flow. I think the  // only other reason this optimization exists is because it affects profiling.  // Reconsider whether this is necessary.  const subtreeHasBeforeMutationEffects =    (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask)) !==    NoFlags;  const rootHasBeforeMutationEffect =    (finishedWork.flags & (BeforeMutationMask | MutationMask)) !== NoFlags;  if (subtreeHasBeforeMutationEffects || rootHasBeforeMutationEffect) {    const prevTransition = ReactSharedInternals.T;    ReactSharedInternals.T = null;    const previousPriority = getCurrentUpdatePriority();    setCurrentUpdatePriority(DiscreteEventPriority);    const prevExecutionContext = executionContext;    executionContext |= CommitContext;    try {      // 첫번째 "before mutaion" 단계.      // The first phase a "before mutation" phase. We use this phase to read the      // state of the host tree right before we mutate it. This is where      // getSnapshotBeforeUpdate is called.      commitBeforeMutationEffects(root, finishedWork, lanes);    } finally {      // Reset the priority to the previous non-sync value.      executionContext = prevExecutionContext;      setCurrentUpdatePriority(previousPriority);      ReactSharedInternals.T = prevTransition;    }  }  let willStartViewTransition = shouldStartViewTransition;  if (enableSwipeTransition) {    // Stop any gestures that were completed and is now being committed.    if (root.stoppingGestures !== null) {      stopCompletedGestures(root);      // If we are in the process of stopping some gesture we shouldn't start      // a View Transition because that would start from the previous state to      // the next state.      willStartViewTransition = false;    }  }  pendingEffectsStatus = PENDING_MUTATION_PHASE;  const startedViewTransition =    enableViewTransition &&    willStartViewTransition &&    startViewTransition(      root.containerInfo,      pendingTransitionTypes,      flushMutationEffects,      flushLayoutEffects,      flushAfterMutationEffects,      flushSpawnedWork,      flushPassiveEffects,      reportViewTransitionError,    );  if (!startedViewTransition) {    // Flush synchronously.    // 두번째 "mutaion" 단계. host tree(Dom 트리)를 변경하는 단계    flushMutationEffects();    // 세번째 "layout" 단계. host tree 변경 후 effects를 실행하는 단계    flushLayoutEffects();    // Skip flushAfterMutationEffects    flushSpawnedWork();  }}

정리

이번 편에서는 reconciler의 동작 과정을 4단계로 나누어 거시적으로 분석했습니다. 한 문장으로 쉽게 요약하면 setState같은 함수로 입력이 일어나고, scheduler에 task로 등록이 되고, task가 실행되고, 마지막으로 commit을 통해 화면에 출력이 되며 Effects까지 실행됩니다. 다음 편에서는 React의 시작 과정에 대해 설명하도록 하겠습니다.