본문 바로가기
프론트엔드

[React]React-dom 과 render

by ISA(류) 2022. 7. 17.

리액트 18기준 레거시가 된 render 함수의 구조를 간단히 들여다 보자

render함수는 react-dom 패키지의 ReactDomLegacy로 분류 되어 있다.

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {
  if (__DEV__) {
    console.error(
      'ReactDOM.render is no longer supported in React 18. Use createRoot ' +
        'instead. Until you switch to the new API, your app will behave as ' +
        "if it's running React 17. Learn " +
        'more: https://reactjs.org/link/switch-to-createroot',
    );
  }

  if (!isValidContainerLegacy(container)) {
    throw new Error('Target container is not a DOM element.');
  }

  if (__DEV__) {
    const isModernRoot =
      isContainerMarkedAsRoot(container) &&
      container._reactRootContainer === undefined;
    if (isModernRoot) {
      console.error(
        'You are calling ReactDOM.render() on a container that was previously ' +
          'passed to ReactDOMClient.createRoot(). This is not supported. ' +
          'Did you mean to call root.render(element)?',
      );
    }
  }
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

첫번째 인자로 리액트 엘리먼트를 타입으로 받고, 두번째 인자로 해당 리액트 엘리먼트를 렌더해줄 컨테이너(DOM)을 받는다. 그리고 잘 쓰지는 않지만 3번째 인자로 콜백 함수를 받을 수있다. 개발 모드 일때 한정으로 render가 createRoot로 교체 되었다는 안내와 render를 createRoot처럼 호출하려고 할때 지원하지 않는다는 안내를 해준다.또 container가 DOM Element가 아닐때 또 fiber에 루트 컨테이너가 등록 안된 경우 에러를 출력해준후 정상적인 접근이라면legacyRenderSubtreeInfoContainer을 호출한다. 해당 코드도 동일한 네임스페이스(파일) 내에 존재하는데 코드는 아래와 같다.

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {
  if (__DEV__) {
    topLevelUpdateWarnings(container);
    warnOnInvalidCallback(callback === undefined ? null : callback, 'render');
  }

  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);
      };
    }
    // Update
    updateContainer(children, root, parentComponent, callback);
  }
  return getPublicRootInstance(root);
}

루트를 render하는 것이다 보니 부모 컴포넌트는 null로 넘긴다.(컴포넌트 타입은 리액트) 자식으로 받은 2번째 인자인 리액트 컴포넌트 객체 (보통은 APP 컴포넌트다.)를 그대로 3번째인자로 컨테이너 객체로 받은 HTML DOM을 그대로 넘긴다.

거기에 서버사이드 렌더링에 쓰는 Hydrate 옵션을 false로 줘서 끄고 콜백으로 넘어온 함수도 그대로 콜백으로 넘겨받은후

레거시 렌더 서브트리 인포 컨테이너 함수는 개발 모드일때 콜백에 대한 경고를 하고, 컨테이너가 리액트 루트 컨테이너인지 maybeRoot로 판별해서 맞을시에는 업데이트를 수행하고 루트 컨테이너가 아닐시에는 legacyCreateRootFromDOMContainer를 수행해서 레거시 루트를 생성해준다. 그렇게 생성해준 또는 업데이트한 root를 getPublicRootInstance 넘겨서 인스턴스를 생성해서 반환해주면 render 과정이 끝난다.

 

legacyCreateRootFromDOMContainer 함수의 코드는 아래와 같다.

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 = createHydrationContainer(
      initialChildren,
      callback,
      container,
      LegacyRoot,
      null, // hydrationCallbacks
      false, // isStrictMode
      false, // concurrentUpdatesByDefaultOverride,
      '', // identifierPrefix
      noopOnRecoverableError,
      // TODO(luna) Support hydration later
      null,
    );
    container._reactRootContainer = root;
    markContainerAsRoot(root.current, container);

    const rootContainerElement =
      container.nodeType === COMMENT_NODE ? container.parentNode : container;
    listenToAllSupportedEvents(rootContainerElement);

    flushSync();
    return root;
  } else {
    // First clear any existing content.
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }

    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
      noopOnRecoverableError, // onRecoverableError
      null, // transitionCallbacks
    );
    container._reactRootContainer = root;
    markContainerAsRoot(root.current, container);

    const rootContainerElement =
      container.nodeType === COMMENT_NODE ? container.parentNode : container;
    listenToAllSupportedEvents(rootContainerElement);

    // Initial mount should not be batched.
    flushSync(() => {
      updateContainer(initialChildren, root, parentComponent, callback);
    });

    return root;
  }
}

먼저 hydrate이냐 그냥 render냐에 따라서 달라지는데 hydrate가 아닌 경우 컨테이너를 모두 비운후(sibling) 콜백 함수가 있을 시 처리하고 컨테이너로 루트를 만들어준다. 그후 그 만들어준 루트를 업데이트해서 반환해주는 과정을 통해서 렌더링을 실행한다. 이후 과정은 React Fiber에 대한 내용인데 해당 내용의 경우 양이 많아서 따로 추려서 글을 써봐야겠다. 참고로 여기서 반환해주는 root를 통해서 렌더링을 해제 할 수 있다.

 

리액트 render는 결국 리액트 컴포넌트(노드,엘레먼트)라는 객체들을 실제 DOM(Root 컨테이너)과 연결하여서 렌더링을 구현한다.

그리고 그 과정에서 여러 에러 상황에 대한 피드백(경고)를 제공하고 해당 인스턴스가 중복 생성되지 않도록 관리한다. 지속적으로 해당 하는 컨테이너(자료구조)와 인스턴스(실제)를 생성하고 그 내용을 업데이트한다는 구조를 계속 따르고 있는데 해당 부분은 Fiber의 WorkLoop에 의해서 관리된다.

그리고 해당 부분은 최소 몇만줄 정도 되는 코드로 이루어져 있어서(파일 하나가 3천줄 이상인 케이스도 많고) 어떻게 정리해야할지는 조금 더 고민해봐야겠다.

*새로 추가된 create Root의 경우 해당 경우보다 로직이 깔끔하게 분리되서 처리되고 있긴하지만 사실 별차이는 없다.

반응형