🌱 ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] Fiber Architecture : 어떤 것이 개선되었고, Fiber는 어떤 구조일까?
    Front-End/React 2024. 11. 16. 07:27
    🍀목차
    글의 목적
    Virtual DOM?
    Stack Reconciler
    Fiber Reconciler
    코드로 이해하기

     

    글의 목적

     React의 Fiber 아키텍처는 React 16 버전부터 도입되어 현재까지 업데이트되고 있는 코어 아키텍처이다.

    기존 스택 기반의 알고리즘을 개선한 아키텍처이며 어떤 부분을 고민했고, 어떤 구조인지 알고 싶어 글을 작성하게 되었다. 부족한 부분이 많겠지만 나와 같은 상황의 학습자에게 도움이 되는 글이었으면 한다.

     

    Virtual DOM?

     

     HTML(HyperText Markup Language)은 웹을 이루는 가장 기초적인 구성 요소 중 하나이며 여러 요소들로 이루어져 있다. 이 요소들은 구문 분석 과정을 거쳐 의미 있는 토큰으로 만들어진다. 토큰들은 프로그래밍 언어가 이해할 수 있게 객체 형태로 모델링하게 되는데 그게 문서 객체 모델(Document Object Model, DOM)이다. DOM이 있기에 JavaScript와 같은 스크립팅 언어가 웹 페이지 요소에 접근하여 문서의 구조, 스타일, 콘텐츠를 변경할 수 있게 되었다.

     

     그런데 만약 변경사항이 생겼다고 생각해 보자.

    DOM Tree를 매번 직접 조작(수정)하는 것은 권장할 만한 방식일까? 더 나아가, 개발자가 이것을 신경 써야 할까?

     

    React는 여기서 가상 문서 객체 모델을 이용하여 UI를 직접 다루는 대신, 개발자는 UI의 결과만 신경 쓰고(상태 선언) 과정은 React가 관리하도록 하였다. 선언적으로 상태에 따라 UI가 어떻게 보일 지를 정의할 뿐, 세부적인 DOM 조작을 React가 가져가도록 한 것이다.

     

    그렇게 React에서는 상태가 변할 때마다 이 값을 기반으로 Virtual DOM에 변경 사항을 적용하고, 실제 DOM에 필요한 부분만 반영하도록 했다. 이 과정에서 실제 DOM과 Virtual DOM 간의 차이점을 찾아내어 동기화하는 과정을 재조정(Reconciliation)이라고 한다.

     

    다음 문단부터 기존의 재조정과 Fiber의 도입 이후의 재조정에 대해 알아보자.

     

    📌Virtual DOM이라는 표현에 대해

      Virtual DOM이라는 표현은 사람마다 의미하는 바가 다르다. 특정 기술이라기보다는 패턴에 가까우며, React 내부에서도 Virtual DOM이라는 개념은 존재하지 않는다. 
     현재 Virtual DOM은 여러 의미로 혼용되고 있는데, 대표적인 예가 Virtual DOM은 성능을 획기적으로 높여주는, 기존의 DOM에 문제가 있어 만들어진 것이라는 것이다. 적합한 표현은 아닌데, DOM API를 통한 실제 DOM 조작이 더 빠른 경우도 있기 때문이다. Virtual DOM이라는 표현을 React에서 계속 사용하는 것에 대해서는 여러 의견이 많은 것 같다. 

     


    Stack Reconciler

     Fiber 도입 전의 React의 비교(diffing) 알고리즘에 대해 알아보자.

    state, props가 갱신되면 render() 함수는 기존과 다른 React 엘리먼트 트리를 반환할 것이다. 이 트리에 맞게 가장 효과적으로 UI를 갱신하는 방법을 만들기 위해 React는 O(n) 복잡도의 놀라운 알고리즘을 구현했다.

    두 가지 가정을 기반으로 해서 말이다.

     

    1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다. ➡️ 노드의 타입이 변경되면(div -> span) 해당 노드는 완전히 새로 그린다. 완전히 새로운 노드는 그 하위 트리를 비교할 필요도 없어진다.

    2. 개발자가 key prop을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다. ➡️ 고유식별자인 key로 각 엘리먼트의 위치를 정확히 파악한다. 위치가 바뀌지 않았다면 그대로 둔다. 빠르게 매칭해 필요한 부분만 업데이트하도록 한다.

     

    이 두 가지 가정으로 React는 전체 트리를 깊이 비교하지 않아도 되고, 필요한 곳만 빠르게 찾아 갱신할 수 있게 되었다.

     

    과정을 좀 더 자세히 보자. 우선 root부터 비교하는데, 유형에 따라 달라진다.

     

    • root DOM 엘리먼트 타입이 다른 경우 : 완전히 새로운 트리를 구축한다. 이전 DOM 노드들은 모두 파괴된다. componentWillUnmount()가 실행되고 새로운 DOM 노드들이 삽입된다. 그에 따라 UNSAFE_componentWillMount()(레거시 생명주기 메서드) -> componentDidMount가 이어서 실행된다. 
    • root DOM 엘리먼트 타입이 같은 경우 : 동일 내역은 유지, 변경 속성만 갱신한다. 아래의 코드에서는 className만 수정하는 것이다. 해당 DOM 노드 처리가 끝나면 이어서 해당 노드의 자식들을 비교하며 처리한다. 재귀적으로 처리하며 기본적으로 두 리스트를 동시에 순회, 차이점이 있으면 변경을 생성한다.
    // https://ko.legacy.reactjs.org/docs/reconciliation.html
    
    <div className="before" title="stuff" />
    <div className="after" title="stuff" />

     

     

    또한 key 속성을 통해 각 엘리먼트의 위치를 파악한다.

    // (1)
    <ul>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>
    
    // (2)
    <ul>
      <li key="2014">Connecticut</li>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>

     

    모든 자식을 변경하는 것이 아닌, 같은 key를 가진 엘리먼트는 이동만 한다. 배열의 인덱스를 key로 사용하면 재배열 시 항목의 순서(key)가 바뀔 것이고, 의도치 않은 방식으로 동작할 것이라는 것도 알 수 있다. 

     

    Fiber 도입 전에는 이렇게 Tree의 root부터 시작하여 재귀 형식으로 모든 컴포넌트의 render()를 호출하여 만들어진 Tree의 변경 사항을 확인하고 업데이트가 필요한 경우를 계속해서 파악해 나갔다. 이를 Stack Reconciler라고 하는데, 그 이유는 아래 표로 확인해보자.

     

     

    (2)번 코드의 컴포넌트가 호출되는 과정을 표현해 보자면 

    <ul /> <li>Connecticut</li>
    <ul />
    <li>Duke</li>
    <li>Connecticut</li>
    <ul />

    <li>Villanova</li>
    <li>Duke</li>
    <li>Connecticut</li>
    <ul />
    <li>Duke</li>
    <li>Connecticut</li>
    <ul />
    <li>Connecticut</li>
    <ul />
    <ul />

     

    FILO(First In Last Out), 스택의 구조인 것을 볼 수 있다. 동기적으로 하나씩 업데이트를 처리한다.

     


    Fiber Reconciler

    참고 문서

     

     

    Fiber Reconciler 방식도 위에서 보았던 diffing 알고리즘의 두 가정은 그대로 가져간다.

     

    기존의 Stack Reconciler 방식은 컴포넌트 트리를 재귀적으로 깊이 우선 탐색(DFS)하여 모든 관련 render 함수를 호출하는 구조였다. 이 과정에서 컴포넌트 트리가 깊어질수록 호출된 render 함수에 의해 콜 스택 사용 빈도가 높아졌다. 이로 인해 한 작업이 길어지면 16ms(1/60초, 일반 모니터 갱신 속도)를 초과해 프레임 드랍을 초래할 수 있었다. 

     

    Fiber 아키텍처의 주요 목표는 증분 렌더링(Incremental Rendering)을 가능하게 하여, 작업을 보다 작은 단위로 나누고 Fiber Reconciler가 지정한 우선순위를 기준으로 실행, 중단하거나 재개할 수 있도록 하는 것이었다. 동시성(여러 작업을 동시에 처리하는 것처럼 보이게 하는 것, 실제로 여러 작업을 동시에 처리하는 병렬성과는 다름)이 가능하다는 점에서 Stack 방식의 Reconciler와 큰 차이가 있다. 

     

    이 전의 React는 스케쥴링을 크게 활용하지 않아 업데이트 시 전체 하위 트리가 즉시 다시 렌더링 되었으나, Fiber를 도입하여 실행할 work(작업)을 결정하는 스케쥴링을 활용하도록 개선하였다. 

     

    Fiber 구조는 React 컴포넌트에 특화된 스택 재구현체로, React가 자체적으로 관리할 수 있는 가상 스택 프레임으로 작동한다. 이를 통해 아래와 같은 작업(work)이 가능하도록 work 자체를 더욱 세분화, 그것이 work를 이루고 있는 Fiber이다.

    • 작업 중단 및 재개, 재사용, 버림(pause work and come back it later, reuse previosly completed work, abort work if it's no longer needed) : Fiber는 링크드 리스트 형태로 구현되어 이전 작업 상태를 유지할 수 있다. 이 정보를 통해 특정 작업의 진행 상태를 추적, 필요할 때 이어서 작업을 진행할 수 있다. 이미 완료된 작업을 재사용하거나 더 이상 필요 없게 되면 버릴 수 있다.
    • 우선순위 부여(assign priority to different types of work) : 개편하여 다른 종류의 작업마다 우선순위를 부여하고, 중요도에 따라 작업을 스케쥴링할 수 있다.

     

     

    📌재조정자(Reconciler) vs 렌더러(Renderer)
    재조정자는 트리의 변경 부분을 확인하여 업데이트가 필요한 부분을 계산하는 역할을 수행한다.
    렌더러는 재조정에서 받은 컴포넌트 트리 정보를 바탕으로 실제 DOM에 마운트 하는 역할을 수행한다.
    그렇기에 Fiber는 실제 DOM에 반영하는 역할을 수행하는 것이 아니다. 렌더러가 최적화된 방식으로 활용하도록 돕는 역할이다.

     

    📌React 동작 단계
    1️⃣ Render 단계 : JSX를 선언하거나, React.createElement()를 통해 React Element를 생성하는 단계
    2️⃣ Reconcile 단계 : 이전 실제 DOM 트리와 새로운 결과로 만들어진 React Element를 비교하여 변경점을 적용하는 단계
    3️⃣ Commit 단계 : 만들어진 DOM 엘리먼트를 브라우저 뷰에 반영하는 단계
    4️⃣ Update 단계 : props, state 변경 시 해당 컴포넌트와 하위 컴포넌트에 대해 위 과정을 반복하는 단계

     

    코드로 이해하기

     

     Fiber는 컴포넌트의 정보를 담고 있는 JavaScript 객체이다. 어떤 type으로 정의되고 있는지 살펴보자.

     

    // react/packages/react-reconciler/src/ReactInternalTypes.js
    
    export type Fiber = {
      // Tag identifying the type of fiber.
      // fiber의 종류에 따라 재조정의 처리가 달라진다.
      // tag(WorkTag)로 판단하는데, 종류가 궁금하다면
      // https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
      // 위 링크를 들어가보자.
      tag: WorkTag,
    
      // Unique identifier of this child.
      // 재조정에서 재사용 가능 여부를 결정하기 위해 사용된다.
      key: null | string,
    
      // The value of element.type which is used to preserve the identity during
      // reconciliation of this child.
      elementType: any,
    
      // The resolved function/class/ associated with this fiber.
      // 해당 컴포넌트를 설명하는 type이다. element에서 fiber를 생성하면 직접 복사된다. 
      type: any,
    
      // The local state associated with this fiber.
      stateNode: any,
    
      // Conceptual aliases
      // parent : Instance -> return The parent happens to be the same as the
      // return fiber since we've merged the fiber and instance.
    
      // Remaining fields belong to Fiber
    
      // The Fiber to return to after finishing processing this one.
      // This is effectively the parent, but there can be multiple parents (two)
      // so this is only the parent of the thing we're currently processing.
      // It is conceptually the same as the return address of a stack frame.
      // 주석 참고, 현재 fiber을 처리한 다음 반환해야 하는 fiber이다. 
      // 개념적으로는 스택 프레임의 반환 주소이다. 
      // 부모 fiber라고 생각해도 된다. 
      return: Fiber | null,
    
      // Singly Linked List Tree Structure.
      // 단일 링크드 리스트 구조라는 것을 알 수 있다.
      child: Fiber | null,
      sibling: Fiber | null,
      index: number,
    
      // The ref last used to attach this node.
      // I'll avoid adding an owner field for prod and model that as functions.
      ref:
        | null
        | (((handle: mixed) => void) & {_stringRef: ?string, ...})
        | RefObject,
    
      refCleanup: null | (() => void),
    
      // Input is the data coming into process this fiber. Arguments. Props.
      // 실행이 시작되면 설정되는 pendingProps, 끝날 때 설정되는 memorizedProps.
      pendingProps: any, // This type will be more specific once we overload the tag.
      memoizedProps: any, // The props used to create the output.
    
      // A queue of state updates and callbacks.
      updateQueue: mixed,
    
      // The state used to create the output
      memoizedState: any,
    
      // Dependencies (contexts, events) for this fiber, if it has any
      dependencies: Dependencies | null,
    
      // Bitfield that describes properties about the fiber and its subtree. E.g.
      // the ConcurrentMode flag indicates whether the subtree should be async-by-
      // default. When a fiber is created, it inherits the mode of its
      // parent. Additional flags can be set at creation time, but after that the
      // value should remain unchanged throughout the fiber's lifetime, particularly
      // before its child fibers are created.
      mode: TypeOfMode,
    
      // Effect
      flags: Flags,
      subtreeFlags: Flags,
      deletions: Array<Fiber> | null,
    
      lanes: Lanes,
      childLanes: Lanes,
    
      // This is a pooled version of a Fiber. Every fiber that gets updated will
      // eventually have a pair. There are cases when we can clean up pairs to save
      // memory if we need to.
      // alternate를 사용하여 fiber 레퍼런스가 담겨 참조하게 된다.
      alternate: Fiber | null,
    
      // Time spent rendering this Fiber and its descendants for the current update.
      // This tells us how well the tree makes use of sCU for memoization.
      // It is reset to 0 each time we render and only updated when we don't bailout.
      // This field is only set when the enableProfilerTimer flag is enabled.
      actualDuration?: number,
    
      // If the Fiber is currently active in the "render" phase,
      // This marks the time at which the work began.
      // This field is only set when the enableProfilerTimer flag is enabled.
      actualStartTime?: number,
    
      // Duration of the most recent render time for this Fiber.
      // This value is not updated when we bailout for memoization purposes.
      // This field is only set when the enableProfilerTimer flag is enabled.
      selfBaseDuration?: number,
    
      // Sum of base times for all descendants of this Fiber.
      // This value bubbles up during the "complete" phase.
      // This field is only set when the enableProfilerTimer flag is enabled.
      treeBaseDuration?: number,
    
      // 중략
    };

     

    주석을 통해 단일 링크드 리스트 구조라는 것을 알 수 있는데, 표현하면 left에는 child Fiber Node가, right에는 sibling Fiber Node가 존재한다고 할 수 있고, 이를 LCRS(Left Children Right Sibling) Tree라고 표현하기도 한다. 

     

    만들어지는 과정을 조금 더 이해해 보자.

     

    ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

    main.jsx
    main.jsx

     

     React 프로젝트를 생성해 봤다면 알고 있는 익숙한 코드이다. 관련 메서드들을 탐구해 보자.

    createElement()부터 살펴볼 것인데, JSX가 변환되는 함수이다.

     

    jsx -&gt; js, 바벨로 트랜스파일 시
    jsx -> js, 바벨로 트랜스파일 시

     

     

    1. createElement()

    // https://github.com/facebook/react/blob/0480cdb58c867c62586ff602fdb06a06c1d63f0c/packages/react/src/jsx/ReactJSXElement.js#L641
    
    export function createElement(type, config, children) {
      // 중략
    
      let propName;
    
      // Reserved names are extracted
      const props = {};
    
      let key = null;
    
      if (config != null) {
        if (hasValidKey(config)) {
        // 예약된 속성 제외 나머지를 props 객체에 저장한다. 
        // 예약된 속성 : key, __self, __source
        // Remaining properties are added to a new props object
        for (propName in config) {
          if (
            hasOwnProperty.call(config, propName) &&
            // Skip over reserved prop names
            propName !== 'key' &&
            // Even though we don't use these anymore in the runtime, we don't want
            // them to appear as props, so in createElement we filter them out.
            // We don't have to do this in the jsx() runtime because the jsx()
            // transform never passed these as props; it used separate arguments.
            propName !== '__self' &&
            propName !== '__source'
          ) {
            props[propName] = config[propName];
          }
        }
      }
    
    
      // Children can be more than one argument, and those are transferred onto
      // the newly allocated props object.
      // -2 해주는 이유 : createElement가 전달받는 argument에서 3 ~ n번까지 자식들이 들어온다.
      // length 0 : type, length 1 : config.
      const childrenLength = arguments.length - 2;
      if (childrenLength === 1) {
        props.children = children;
      } else if (childrenLength > 1) {
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
          childArray[i] = arguments[i + 2];
        }
        props.children = childArray;
      }
    
      // Resolve default props
      if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
          if (props[propName] === undefined) {
            props[propName] = defaultProps[propName];
          }
        }
      }
      
    
      return ReactElement(
        type,
        key,
        undefined,
        undefined,
        getOwner(),
        props,
        __DEV__ && enableOwnerStacks ? Error('react-stack-top-frame') : undefined,
        __DEV__ && enableOwnerStacks ? createTask(getTaskName(type)) : undefined,
      );
    }

     

     

     createElement는 컴포넌트 종류(type)와 props를 담고 있는 객체(config)를 전달받고 children에 자식을 전달받아 그것을 ReactElement로 만들어 반환하는 역할을 하고 있다.

    babel로 트랜스파일한 파일과 React 코드를 매칭시켜 보면 이해가 더 잘 될 것이다.

     

    // (1) js
    function App() {
      const [name, setName] = useState('Young');
      return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("h1", null, "practice react"), /*#__PURE__*/React.createElement("section", null, /*#__PURE__*/React.createElement("p", null, `my name is ... ${name}`), /*#__PURE__*/React.createElement("button", {
        onClick: e => setName(name === 'Young' ? 'Ju' : 'Young')
      }, "switch name!")));
    }
    
    // (2) jsx
    function App() {
      const [name, setName] = useState('Young');
    
      return (
        <div>
          <h1>practice react</h1>
          <section>
            <p>{`my name is ... ${name}`}</p>
            <button onClick={e => setName(name === 'Young' ? 'Ju' : 'Young')}>
              switch name!
            </button>
          </section>
        </div>
      );
    }

     

    children 인자를 통해 계속해서 ReactElement들이 자식 요소로 만들어지는 것을 볼 수 있다. button의 경우는 onClick 이벤트가 config로 전달되는 것도 확인할 수 있다.

     

    ReactElement에 대해서도 알아보자.

     

    function ReactElement(
      type,
      key,
      self,
      source,
      owner,
      props,
      debugStack,
      debugTask,
    ) {
    
     // 중략
     
      let element;
      
        element = {
          // This tag allows us to uniquely identify this as a React Element
          $$typeof: REACT_ELEMENT_TYPE,
    
          // Built-in properties that belong on the element
          type,
          key,
          ref,
    
          props,
        };
      }
    
     // 중략
    
      return element;
    }

     

    $$typeof는 fiber를 만들 때 element 종류를 확인하는 데 쓰인다. 

    직접 확인해보고 싶다면 App을 console에 찍어보면 확인해 볼 수 있다.

     

    Symbol(react.element)
    Symbol(react.element)

     

     

     

    2. createRoot()

    // https://github.com/facebook/react/blob/main/packages/react-dom/src/client/ReactDOMRoot.js#L161
    
    export function createRoot(
      container: Element | Document | DocumentFragment,
      options?: CreateRootOptions,
    ): RootType {
    
     // 중략
     
      const root = createContainer(
        container,
        // ConcurrentRoot는 기본적으로 Concurrent 모드가 설정된 Root이다.
        ConcurrentRoot,
        null,
        isStrictMode,
        concurrentUpdatesByDefaultOverride,
        identifierPrefix,
        onUncaughtError,
        onCaughtError,
        onRecoverableError,
        transitionCallbacks,
      );
      // root.current(HostRoot)를 넘겨받음. <div id="root">에 HostRoot를 연결시키는 부분.
      markContainerAsRoot(root.current, container);
    
     // <div id="root">에 모든 이벤트리스너를 붙인다.
      const rootContainerElement: Document | Element | DocumentFragment =
        container.nodeType === COMMENT_NODE
          ? (container.parentNode: any)
          : container;
      listenToAllSupportedEvents(rootContainerElement);
    
      // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
      return new ReactDOMRoot(root);
    }
    
    // $FlowFixMe[missing-this-annot]
    function ReactDOMRoot(internalRoot: FiberRoot) {
      this._internalRoot = internalRoot;
    }

     

    전달받은 container(<div id="root">)를 createContainer()에 전달하여 root라는 상수에 저장, markContainerAsRoot를 통해 <div id="root">에 연결, 마지막으로 FiberRootNode를 this._internalRoot에 할당하는 것을 볼 수 있다. 

     

    createContainer()을 봐야겠다.

    // https://github.com/facebook/react/blob/5c56b873efb300b4d1afc4ba6f16acf17e4e5800/packages/react-reconciler/src/ReactFiberReconciler.js#L228C1-L269C2
    
    export function createContainer(
      containerInfo: Container,
      tag: RootTag, // 이 태그에 ConcurrentRoot 플래그가 들어간다. (0(Blocking) 또는 1(Concurrent))
      hydrationCallbacks: null | SuspenseHydrationCallbacks,
      isStrictMode: boolean,
      // TODO: Remove `concurrentUpdatesByDefaultOverride`. It is now ignored.
      concurrentUpdatesByDefaultOverride: null | 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,
    ): OpaqueRoot {
    	// Opaque(불투명한) Root : Root Node에 대한 정보를 추상화시킨다는 것을 나타낸다.
      const hydrate = false;
      const initialChildren = null;
      return createFiberRoot(
        containerInfo,
        tag,
        hydrate,
        initialChildren,
        hydrationCallbacks,
        isStrictMode,
        identifierPrefix,
        onUncaughtError,
        onCaughtError,
        onRecoverableError,
        transitionCallbacks,
        null,
      );
    }

     

     

    createFiberRoot()를 리턴하는 것을 볼 수 있다. 또 들어가 보자~

     

    // https://github.com/facebook/react/blob/5c56b873efb300b4d1afc4ba6f16acf17e4e5800/packages/react-reconciler/src/ReactFiberRoot.js#L144C1-L149C30
    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. FiberRootNode
      const root: FiberRoot = (new FiberRootNode(
        containerInfo,
        tag,
        hydrate,
        identifierPrefix,
        onUncaughtError,
        onCaughtError,
        onRecoverableError,
        formState,
      ): any);
      
      // 중략
    
      // 2. HostRootFiber
      const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
      
      // 순환 참조 가능하도록
      root.current = uninitializedFiber;
      uninitializedFiber.stateNode = root;
    
     // 중략
    
      initializeUpdateQueue(uninitializedFiber);
    
      return root;
    }

     

     

    여기서 FiberRootNodeHostRoot라는 두 가지 주요 객체가 생성되고 있는 것을 볼 수 있다.

    FiberRootNode부터 확인해 보자.

     

    https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberRoot.js

     

     

    FiberRootNode는 current라는 속성이 존재한다. 앞에서 uninitializedFiber가 할당되었으니 current는 Fiber라는 것을 알 수 있고, 그 uninitializedFiber의 stateNode에는 root를 할당하여 순환참조를 시켰다. 실제로 확인해 보자.

     

     

    div#root의 속성 확인
    div#root의 속성 확인

     

     

    정말 FiberRootNode가 할당되어 있다. 그렇다면 root.current에 할당된 uninitializedFiber는? FiberNode(HostRoot)라는 것을 위에서 볼 수 있다. 아래 createHostRootFiber를 보면 createFiber의 결과로 만들어진 Fiber를 return 시키는 것을 직접 확인할 수 있다.

     

    // https://github.com/facebook/react/blob/5c56b873efb300b4d1afc4ba6f16acf17e4e5800/packages/react-reconciler/src/ReactFiber.js#L527C1-L549C2
    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;
      }
    
      if (enableProfilerTimer && isDevToolsPresent) {
        // Always collect profile timings when DevTools are present.
        // This enables DevTools to start capturing timing at any point–
        // Without some nodes in the tree having empty base times.
        mode |= ProfileMode;
      }
    
      return createFiber(HostRoot, null, null, mode);
    }

     

     

    추가적으로, FiberRootNode인 root는 정적 상태인 노드이다. 그에 반해 HostRootFiber인 uninitializedFiber는 변경을 파악하기 위해 사용되는 Fiber 노드이다. 또한 HostRootFiber는 루트 Fiber로 트리의 최상단에 위치하는 Fiber 노드이다. 모든 컴포넌트는 여기에서부터 하위로 연결된다. 이것도 직접 확인해 보자.

     

     

    child를 타고 내려가보자...

     

     여기서 Fiber의 sibling, child를 확실하게 이해할 수 있었다.

     

    이런 구조
    이런 구조

     

    <h1>의 sibling은 <section>이다. 그리고 section의 <p>와 <button>중 누가 child가 될까? 두 개 다?

     

     

    <p> 만이 child가 된다. 관계를 그림으로 정리해 봤다.

    이런 구조이다.
    이런 구조이다.

     

    child와 sibling으로 fiber의 재귀적 트리 구조를 설명하게 되는데, child에는 첫 번째 자식만 연결되게 되고, sibling으로 동일 레벨 Node들이 연결된다. 만약 section의 button밑에 새로운 p 엘리먼트를 추가하면 그것은 button의 sibling에 연결되게 된다.

     

    연결되고 있는 sibling
    연결되고 있는 sibling

     

     

     

    여기까지 어떻게 root가 만들어지는지, 그리고 만들어진 Fiber Tree가 어떤 형태인지 알게 되었다.

    createRoot() 과정에서 root가 생성되는 과정을 마지막으로 요약해 보자.

     

    1. RootFiberNode를 생성한다. (createContainer -> createFiberRoot)

    2. FiberRootNode, HostRoot가 생성되며 순환 참조됨.

    3. <div id="root">에 root.current(HostRoot)를 연결.

    4. <div id="root">에 React에서 발생하는 모든 이벤트, 업데이트를 할당한다.

     

     

     

     

    해당 글에서는 React Fiber 아키텍처가 기존의 어떤 점을 개선했고, 내부적으로 어떤 구조를 가지고 있는지 살펴보았다. 학습하며 이전에 작성했던 useState, useEffect 등도 직접 코드로 확인해 보자고 느꼈다.

     

    다음 글부터는 해당 글에서 사용된 코드를 사용하여 useState를 통해 상태 변화가 생긴 컴포넌트가 어떤 식으로 재조정을 수행하는지 학습해보려고 한다. 

     

    참고자료

    https://d2.naver.com/helloworld/2690975

    https://github.com/acdlite/react-fiber-architecture

    https://github.com/facebook/react

    https://goidle.github.io/react/in-depth-react-reconciler_1/

    https://jasonkang14.github.io/react/how-react-renders-jsx

     

     

    댓글

🍀 Y0ungZ dev blog.
스크롤 버튼