React

[ Frontend > React ]

[리액트 19] 5편. React의 시작 과정

 Carrot Yoon
 2025-05-06
 23

React의 시작 과정

4편에서는 React 흐름의 핵심인 reconciler의 동작 흐름을 4단계로 정리해봤습니다. 5편에서는 React 애플리케이션의 시작 과정을 설명하고자 합니다. 이 과정은 4편의 입력 단계에 해당하며 react-dom 패키지에 대한 설명입니다.

우선 본격적인 분석에 앞서 React 앱의 시작 방식에 대해 설명드리겠습니다.

1. React 시작 모드(mode)

React 19버전 기준으로는 크게 2가지(Legacy, Concurrent) 입니다.

React 19 모드들 소스코드

1.1 Legacy 모드 (React 16~18 버전에서 사용)

React 19.1에서 render 함수 소스코드

ReactDOM.render(<App />, document.getElementById('root'), (dom) => {}); // 콜백 함수 지원, 매개변수로는 DOM 객체가 전달됨

1.2 Concurrent 모드 (React 18부터 본격 사용)

React 19 부터는 기본으로 사용되는 모드입니다. 여러 컴포넌트들을 Concurrent하게 렌더링할 수 있습니다.

import { createRoot } from 'react-dom/client'// 1. ReactDOMRoot 객체 생성const reactDOMRoot = createRoot(document.getElementById('root'));// 2. render 호출reactDOMRoot.render(<App />); // 콜백 미지원

2. React 시작 흐름

진입 함수 render을 하기 전까지는 DOM 요소와 <App/>(리액트 엘리먼트, createElement로 변환된 것) 사이에는 아무런 연결관계가 존재하지 않습니다. 그리고 React의 시작은 react-dom 패키지에서 시작되어 내부적으로 react-reconciler 패키지를 호출합니다.

2.1 전역 객체 생성

모드에 관계없이 React는 시작 시 3가지의 전역 객체를 생성합니다.

  1. ReactDOMRoot (createRoot 함수 , RootType 타입, ReactDOMRoot 생성자 함수)

    • react-dom 패키지에 속합니다.

    • 이 객체는 render와 unmount 메서드만 갖습니다.

  2. FiberRoot (createContainer 함수, createFiberRoot 함수, FiberRootNode 생성자 함수 )

    • react-reconciler 패키지에 속합니다.

    • react-reconciler 실행 중 전역 컨텍스트 역할을 합니다.

    • Fiber를 구성하는 과정에서 필요한 전역 상태를 저장합니다.

    • 대부분의 인스턴스 변수는 Fiber 작업 루프 과정에서 상태를 저장합니다.

    • React 내부적으로 위 변수들의 값에 따라 로직이 제어됩니다.

  3. HostRootFiber (createHostRootFiber 함수, HostRoot 태그)

    • react-reconciler 패키지에 속합니다.

    • React 내에서 첫 번째 Fiber 객체 입니다.

    • Fiber 트리의 Root 노드이며 Node Type은 HostRoot 입니다.

이 세 가지 객체는 React 시스템이 동작할 수 있는 핵심 기반입니다. unmount하지 않는 경우 소멸되지 않으며, React의 전체 생명주기를 통틀어 유지됩니다.

위 3가지 객체는 createRoot 함수를 통해 생깁니다. 최초로 다음과 같은 구조를 갖게 됩니다.

리액트8.webp

위 구조 생성 과정을 그림으로 그리면 다음과 같습니다.

legacyRenderSubtreeIntoContainer, legacyCreateRootFromDOMContainer,

리액트10.webp

2.2 ReactDOMRoot 객체 생성

React 애플리케이션을 시작하는 2가지 모드의 API는 서로 다릅니다. 그래서 각 소스 코드는 각기 다른 파일(Legacy, Concurrent)에 존재합니다. 하지만 최종적으로는 createContainer 함수를 이용하여 ReactDOMRoot 인스턴스(this._internalRoot == this._react_root_container)를 생성하는 점은 똑같습니다

Legacy 모드의 ReactDOMRoot 생성
function legacyRenderSubtreeIntoContainer(  parentComponent: ?React$Component<any, any>,  children: ReactNodeList,  container: Container,  forceHydrate: boolean,  callback: ?Function,): React$Component<any, any> | PublicInstance | null {  // container._reactRootContainer에 이미 ReactDOMRoot 존재하는지 확인.  const maybeRoot = container._reactRootContainer;  let root: FiberRoot;  if (!maybeRoot) {    // Initial mount, 초기 호출 - root가 아직 초기화되지 않은 경우, 이 분기로 진입    // 1. ReactDOMRoot 객체 생성 - React 애플리케이션 환경 초기화    root = legacyCreateRootFromDOMContainer(      container,      children,      parentComponent,      callback,      forceHydrate,    );  } else {    // root가 이미 초기화 된 경우 - 두 번째 render 호출 시 이 분기로 진입    // 1. FiberRoot 객체 가져오기    root = maybeRoot;    if (typeof callback === 'function') {      const originalCallback = callback;      callback = function () {        const instance = getPublicRootInstance(root);        originalCallback.call(instance);      };    }    // 2. Update 호출    updateContainer(children, root, parentComponent, callback);  }  return getPublicRootInstance(root);}
function legacyCreateRootFromDOMContainer(  container: Container,  initialChildren: ReactNodeList,  parentComponent: ?React$Component<any, any>,  callback: ?Function,  isHydrationContainer: boolean,): FiberRoot {  if (isHydrationContainer) {    if (typeof callback === 'function') {      const originalCallback = callback;      callback = function () {        const instance = getPublicRootInstance(root);        originalCallback.call(instance);      };    }    const root: FiberRoot = createHydrationContainer(      initialChildren,      callback,      container,      LegacyRoot,      null, // hydrationCallbacks      false, // isStrictMode      false, // concurrentUpdatesByDefaultOverride,      '', // identifierPrefix      wwwOnUncaughtError,      wwwOnCaughtError,      noopOnRecoverableError,      // TODO(luna) Support hydration later      null,      null,    );    container._reactRootContainer = root; // 여기서 ReactDOMRoot 저장    markContainerAsRoot(root.current, container);    const rootContainerElement =      !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE        ? container.parentNode        : container;    // $FlowFixMe[incompatible-call]    listenToAllSupportedEvents(rootContainerElement);    flushSyncWork();    return root;  } else {    // First clear any existing content.    clearContainer(container);    if (typeof callback === 'function') {      const originalCallback = callback;      callback = function () {        const instance = getPublicRootInstance(root);        originalCallback.call(instance);      };    }    const root = createContainer(      container,      LegacyRoot,      null, // hydrationCallbacks      false, // isStrictMode      false, // concurrentUpdatesByDefaultOverride,      '', // identifierPrefix      wwwOnUncaughtError,      wwwOnCaughtError,      noopOnRecoverableError,      null, // transitionCallbacks    );    container._reactRootContainer = root; // 여기서 ReactDOMRoot 저장    markContainerAsRoot(root.current, container);    const rootContainerElement =      !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE        ? container.parentNode        : container;    // $FlowFixMe[incompatible-call]    listenToAllSupportedEvents(rootContainerElement);    // Initial mount should not be batched.    updateContainerSync(initialChildren, root, parentComponent, callback);    flushSyncWork();    return root;  }}

Concurrent 모드의 ReactDOMRoot 생성
function ReactDOMRoot(internalRoot: FiberRoot) {  this._internalRoot = internalRoot;}export function createRoot(  container: Element | Document | DocumentFragment,  options?: CreateRootOptions,): RootType {  const root = createContainer(    container,    ConcurrentRoot,    null,    isStrictMode,    concurrentUpdatesByDefaultOverride,    identifierPrefix,    onUncaughtError,    onCaughtError,    onRecoverableError,    transitionCallbacks,  ); // 이게 ReactDOMRoot 본체로 보면 됌.  return new ReactDOMRoot(root); // this._internalRoot = root 가 내부적으로 수행.}
// render가 별도로 나뉘어져 updateContainer가 여기서 실행.ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =  // $FlowFixMe[missing-this-annot]  function (children: ReactNodeList): void {    const root = this._internalRoot;    updateContainer(children, root, null, null);  };

2.3 FiberRoot 생성

ReactDOMRoot을 생성 과정에 React의 첫 번째 Fiber 객체인 FiberRoot를 생성합니다. (createContainer 함수 실행하며 createFiberRoot 함수 실행) 이때 legacy모드이면 tag 그대로 Fiber가 생성되고 아니라면 concurrent로 FiberRoot가 생성됩니다.

Legacy FiberRoot (create-root-impl)

, createRoot 함수, createRootImpl 함수(Concurrent에서 사용하는 createRoot함수와 같음)

export function createRoot(  container: Element | Document | DocumentFragment,  options?: CreateRootOptions,): RootType {  return createRootImpl(    container,    assign(      ({        onUncaughtError: wwwOnUncaughtError,        onCaughtError: wwwOnCaughtError,      }: any),      options,    ),  );}
// createFiberRoot 함수 축약export function createFiberRoot(  containerInfo: Container,  tag: RootTag,  hydrate: boolean,  initialChildren: ReactNodeList,  hydrationCallbacks: null | SuspenseHydrationCallbacks,  isStrictMode: boolean,  identifierPrefix: string,  onUncaughtError: (    error: mixed,    errorInfo: {+componentStack?: ?string},  ) => void,  onCaughtError: (    error: mixed,    errorInfo: {      +componentStack?: ?string,      +errorBoundary?: ?React$Component<any, any>,    },  ) => void,  onRecoverableError: (    error: mixed,    errorInfo: {+componentStack?: ?string},  ) => void,  transitionCallbacks: null | TransitionTracingCallbacks,  formState: ReactFormState<any, any> | null,): FiberRoot {  // 1. FiberRoot 객체 생성. RootTag가 전달되어 FiberRoot 타입 Fiber가 생성.  const root: FiberRoot = (new FiberRootNode(    containerInfo,    tag,    hydrate,    identifierPrefix,    onUncaughtError,    onCaughtError,    onRecoverableError,    formState,  ): any);  const uninitializedFiber = createHostRootFiber(tag, isStrictMode);  root.current = uninitializedFiber;  uninitializedFiber.stateNode = root;  return root;}
function FiberRootNode(  this: $FlowFixMe,  containerInfo: any,  // $FlowFixMe[missing-local-annot]  tag,  hydrate: any,  identifierPrefix: any,  onUncaughtError: any,  onCaughtError: any,  onRecoverableError: any,  formState: ReactFormState<any, any> | null,) {  this.tag = disableLegacyMode ? ConcurrentRoot : tag;  this.containerInfo = containerInfo;  this.pendingChildren = null;  this.current = null;  this.pingCache = null;  this.timeoutHandle = noTimeout;  this.cancelPendingCommit = null;  this.context = null;  this.pendingContext = null;  this.next = null;  this.callbackNode = null;  this.callbackPriority = NoLane;  this.expirationTimes = createLaneMap(NoTimestamp);  this.pendingLanes = NoLanes;  this.suspendedLanes = NoLanes;  this.pingedLanes = NoLanes;  this.warmLanes = NoLanes;  this.expiredLanes = NoLanes;  this.errorRecoveryDisabledLanes = NoLanes;  this.shellSuspendCounter = 0;  this.entangledLanes = NoLanes;  this.entanglements = createLaneMap(NoLanes);  this.hiddenUpdates = createLaneMap(null);  this.identifierPrefix = identifierPrefix;  this.onUncaughtError = onUncaughtError;  this.onCaughtError = onCaughtError;  this.onRecoverableError = onRecoverableError;  this.pooledCache = null;  this.pooledCacheLanes = NoLanes;  this.formState = formState;  this.incompleteTransitions = new Map();}

2.4 HostRootFiber 생성

HostRootFiber는 createHostRootFiber로 생성됩니다. 처음에는 1개만 생성되며 추후 더블 버퍼 구조로 복사본을 만들어 상태관리를 하여 화면을 그리는데 사용됩니다. Fiber 트리에서 모든 노드의 mode는 HostRootFiber의 mode와 동일합니다. 왜냐하면 새로 생성되는 Fiber 노드의 mode는 부모 노드에서 상속되기 때문입니다.

export function createHostRootFiber(  tag: RootTag,  isStrictMode: boolean,): Fiber {  let mode;  if (disableLegacyMode || tag === ConcurrentRoot) {    mode = ConcurrentMode;    if (isStrictMode === true) {      mode |= StrictLegacyMode | StrictEffectsMode;    }  } else {    mode = NoMode;  }  return createFiber(HostRoot, null, null, mode); //HostRoot타입으로 생성}

2.5 객체들 생성 후 모습 간략도 및 정리

legacy 모드

legacy모드에서는 DOM 객체에 _reactRootContainer 프로퍼티를 통해서 ReactFiberRoot를 추가적으로 참조한다.

Legacy Mode.webp
concurrent 모드

Concurrent Node.webp

2.6 render, update 함수

React19.1에서는 Legacy와 Concurrent 모두 같은 updateContainer 함수를 사용합니다. 그리고 이 updateContainer를 통해 react-dom과 react-reconciler가 연결됩니다. 다만 render 함수 진입 소스 코드가 다릅니다. 그리고 첫 마운트시 Legacy 코드는 Sync한 방식으로 Concurrent는 일반적인 방식으로 updateContainerImpl 함수를 수행합니다. 중요한 점은 마지막에 scheduleUpdateOnFiber 함수를 실행하여 Reconciler의 "입력" 단계의 진입점을 실행한다는 것입니다.

Legacy의 updateContainer

Legacy의 render 함수 updateContainer 함수

export function render(  element: React$Element<any>,  container: Container,  callback: ?Function,): React$Component<any, any> | PublicInstance | null {  if (!isValidContainer(container)) {    throw new Error('Target container is not a DOM element.');  }  return legacyRenderSubtreeIntoContainer(    null,    element,    container,    false,    callback,  );}
function legacyRenderSubtreeIntoContainer(  parentComponent: ?React$Component<any, any>,  children: ReactNodeList,  container: Container,  forceHydrate: boolean,  callback: ?Function,): React$Component<any, any> | PublicInstance | null {  const maybeRoot = container._reactRootContainer;  let root: FiberRoot;  if (!maybeRoot) {    // Initial mount (내부적으로 업데이트)    root = legacyCreateRootFromDOMContainer(      container,      children,      parentComponent,      callback,      forceHydrate,    );  } else {    root = maybeRoot;    if (typeof callback === 'function') {      const originalCallback = callback;      callback = function () {        const instance = getPublicRootInstance(root);        originalCallback.call(instance);      };    }    // 업데이트 컨테이너    updateContainer(children, root, parentComponent, callback);  }  return getPublicRootInstance(root);}

function legacyCreateRootFromDOMContainer(  container: Container,  initialChildren: ReactNodeList,  parentComponent: ?React$Component<any, any>,  callback: ?Function,  isHydrationContainer: boolean,): FiberRoot {  if (isHydrationContainer) {    if (typeof callback === 'function') {      const originalCallback = callback;      callback = function () {        const instance = getPublicRootInstance(root);        originalCallback.call(instance);      };    }    const root: FiberRoot = createHydrationContainer(      initialChildren,      callback,      container,      LegacyRoot,      null, // hydrationCallbacks      false, // isStrictMode      false, // concurrentUpdatesByDefaultOverride,      '', // identifierPrefix      wwwOnUncaughtError,      wwwOnCaughtError,      noopOnRecoverableError,      // TODO(luna) Support hydration later      null,      null,    );    container._reactRootContainer = root;    markContainerAsRoot(root.current, container);    const rootContainerElement =      !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE        ? container.parentNode        : container;    // $FlowFixMe[incompatible-call]    listenToAllSupportedEvents(rootContainerElement);    flushSyncWork();    return root;  } else {    // First clear any existing content.    clearContainer(container);    if (typeof callback === 'function') {      const originalCallback = callback;      callback = function () {        const instance = getPublicRootInstance(root);        originalCallback.call(instance);      };    }    const root = createContainer(      container,      LegacyRoot,      null, // hydrationCallbacks      false, // isStrictMode      false, // concurrentUpdatesByDefaultOverride,      '', // identifierPrefix      wwwOnUncaughtError,      wwwOnCaughtError,      noopOnRecoverableError,      null, // transitionCallbacks    );    container._reactRootContainer = root;    markContainerAsRoot(root.current, container);    const rootContainerElement =      !disableCommentsAsDOMContainers && container.nodeType === COMMENT_NODE        ? container.parentNode        : container;    // $FlowFixMe[incompatible-call]    listenToAllSupportedEvents(rootContainerElement);    // Initial mount should not be batched. Sync하게 업데이트    updateContainerSync(initialChildren, root, parentComponent, callback);    flushSyncWork();    return root;  }}

export function updateContainerSync(  element: ReactNodeList,  container: OpaqueRoot,  parentComponent: ?React$Component<any, any>,  callback: ?Function,): Lane {  if (!disableLegacyMode && container.tag === LegacyRoot) {    flushPendingEffects();  }  const current = container.current;  updateContainerImpl(    current,    SyncLane, //Sync한 레인으로 할당    element,    container,    parentComponent,    callback,  );  return SyncLane;}

Concurrent의 updateContainer

Concurrent의 render 함수

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);  updateContainerImpl(    current,    lane, // 적절한 비동기 Lane 할당    element,    container,    parentComponent,    callback,  );  return lane;}

// updateContainerImpl 함수function updateContainerImpl(  rootFiber: Fiber,  lane: Lane,  element: ReactNodeList,  container: OpaqueRoot,  parentComponent: ?React$Component<any, any>,  callback: ?Function,): void {  const context = getContextForSubtree(parentComponent);  if (container.context === null) {    container.context = context;  } else {    container.pendingContext = context;  }  const update = createUpdate(lane); // lane에 맞는 update 생성  callback = callback === undefined ? null : callback;  if (callback !== null) {    update.callback = callback;  }   const root = enqueueUpdate(rootFiber, update, lane); //업데이트 추가  if (root !== null) {    startUpdateTimerByLane(lane); // 업데이트 타이머 시작    scheduleUpdateOnFiber(root, rootFiber, lane); // Reconciler 동작 흐름의 '입력' 단계 진입.    entangleTransitions(root, rootFiber, lane);  }}

3. 정리

  • Legacy 모드

    • HostRootFiber.mode가 NoMode로 실행됩니다.

    • Interruptible Rendering(중단 가능한 렌더링)이 불가능 합니다.

    • 최초 렌더링이나 후속 업데이트에 동기 작업 루프만을 사용합니다.

    • Reconciliation이 중단될 수 없어 생명주기 함수는 한번만 실행됩니다.

    • 호환용으로만 존재하며 React 19에서는 사용할 수 없는 상태입니다.

  • Concurrent 모드

    • HostRootFiber.mode가 Current 모드로 실행합니다.

    • 중단 가능한 렌더링이 가능하여 일부 생명주기 함수가 여러번 실행될 수 있습니다.

    • 여러번의 프레임에 걸쳐서 업데이트를 완료할 수 있습니다.

    • Concurrent 모드가 도입되며 데이터 로딩도 Suspense 적용이 가능해졌습니다.

리액트7.webp