[리액트] createPortal과 createRoot

Date:     Updated:

카테고리:

태그:

🚀주제

포탈?

포탈을 만든다? 이게 무슨뜻일까?

메이플스토리 히든포탈처럼, 리액트의 숨겨진 두가지 보석, createPortalcreateRoot에 대해 알아보도록 하겠다.


🚀createRoot

리액트 공식홈페이지에 가보면 다음과 같은 코드를 볼 수 있다.

리액트공홈

npx로 만들어도, vite로 만들어도, RCA로 만들어도 모두 동일한 뿌리를 얻게된다. 많은 분들이 알고있겠지만, React는 본인이 웹에서보여지게 될지, 모바일에서 보여지게될 지 모른다. 우리가 리액트의 마법이라고 생각되는 대부분의 기능들은 사실상 React-Dom 혹은 ReactNative와 같이 렌더링해주는 라이브러리에 들어있는 것이다.

무튼, 통상적으로 앱을 만들 때, 이처럼 React-Dom을 이용해서 만든다. 다음 틀-코드를 한번 살펴보자.

ReactDOM.render(
  <React.StrictMode>
      <App />
  </React.StrictMode>,
  document.getElementById("root")
);

위와 같은 방법으로 만드는것을 보았는가? 아마 최근에 리액트를 공부하신 분들은 처음보는 코드일 것이다. (필자의 이야기다)

ReactDom.render()은 React 16 이전의 기존 렌더링 방식을 사용한다. 이 방식은에서는 옛날 리액트 틀리액트 방식인 callStack Architecture을 이용한 동기식 처리를 통해 렌더링한다. 따라서 작업처리중 userInteraction을 처리하지 못해 사용성이 떨어지게 될 수 있다. 렌더링도 LIFO방식인 것이다.

당연하게도, ReactDom.createRoot.render()을 이용하면 우리에게 익숙한 fiber Architecture을 이용한 비동기식 처리가 가능하다. react18부터는 createRoot를 사용하지 않으면, 공부하고오라고 화낸다…

18에서의warning

무튼 16버전부터 도입된 fiber Architecture가 성숙해졌고, 18버전부터는 createRoot를 통한 concurrent렌더링이 안정적으로 지원되게 된 것이다. 최근에는 (공홈에 나와있듯)모두가 사용하는 아래와 같은 방법을 쓴다.

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


🚀createPortal

포탈이란 무엇일까? 여기에서 저기로 가는것이다.

여기to저기

사용법은 다음과 같다.

//루트
<html>
  <body>
    <div id="app-root"></div>
    <div id="모달이있을곳"></div>
  </body>
</html>
//모달
function myModal(){
    return createPortal(
      this.props.children,  //첫번째인자
      document.getElementById("모달이있을곳") //두번째인자
    );
}

다음 영상 처럼 정말 모달이있을곳으로 이동시키는 것이다

포탈실행

하지만 공식문서(링크 )에서는 조금 다른 방법을 이용한다

루트에 만들어둔 <div id="모달이있을곳"></div> 에 바로 달아주는게 아니라, “모달이 있을곳”에 child div를 하나 더 만들어 두고 그곳에 createPortal을 사용한다. 코드내용은 위 링크에 codePen과 함께 첨부되어있다. 굳이 굳이 새로운 child를 이용하는 이유가 뭘까?

충돌

바로 위처럼 특정 Dom의 child로 들어가는게 위험할 수 있기 때문이다. 다른 모달들이 있을수도 있고, (다른 누군가의 코드로)우리가 예상치 못한 Dom들이 존재할 수도 있는 것이다. 따라서 리액트 공홈에서는 다음과 같은 방식으로 작성한 것이다.

안충돌

이젠 모달이있을곳(div)에게로의 직접적인 추가가 아니라, 각각의 div children에 추가됨으로 더 안전하게 접근을 할 수 있는 것이다. 이와같은 방식으로 각 모달이 겹치지 않고 제각각의 상태와 이벤트를 처리할 수 있다.


🚀딥다이브

우리는 createRoot와 createPortal에 대해 알아보았다. 그렇다면 위와 같은 api들이 없을때는 어땠을까? 둘의 차이점과 공통점은 무엇일까? 도대체 어떻게 작동하는 것일까? 많은 부분들이 궁금할 것이다. 안궁금 할 수도 있다. 하지만 필자가 궁금했었던 내용들을 공유해보고자 한다.

🚀createPortal 좀더 자세히 보기

다음은 createPortal내부 구현체이다

export function createPortal(
  children: ReactNodeList,
  containerInfo: any,
  implementation: any,
  key: ?string = null,
): ReactPortal {
  return {
    $$typeof: REACT_PORTAL_TYPE,
    key: key == null ? null : '' + key,
    children,
    containerInfo,
    implementation,
  };
}

함수는 이처럼 간단하게 되어있다. 렌더링하고자 하는 자식노드를 받아서, ReactPortal객체를 반환한다. (일반적인 react element들처럼 선택적으로 키를 받을 수 있는 모습도 보인다)

라이브러리에서는 생각보다 별게 없었다. 조금 눈에 띄는 점은 Iframe을 사용할 때 createPortal을 사용한다는 것이다.

코드보기(클릭)👇
class IframePortal extends React.Component {
  iframeRef = null;

  handleRef = ref => {
    if (ref !== this.iframeRef) {
      this.iframeRef = ref;
      if (ref) {
        if (ref.contentDocument && this.props.head) {
          ref.contentDocument.head.innerHTML = this.props.head;
        }
        setTimeout(() => {
          this.forceUpdate();
        });
      }
    }
  };

  render() {
    const ref = this.iframeRef;
    let portal = null;
    if (ref && ref.contentDocument) {
      portal = ReactDOM.createPortal(
        this.props.children,
        ref.contentDocument.body
      );
    }

    return (
      <div>
        <iframe
          title="Iframe portal"
          style=
          ref={this.handleRef}
        />
        {portal}
      </div>
    );
  }
}

이처럼 Iframe을 이용하여 외부 컨텐츠들(유튜브, 광고 등)을 삽입할 때, 리액트 내부에서 createPortal을 사용하여 렌더링해준다는 사실을 알 수 있다. 그러면… 이걸 또 어디에 쓸까? 그걸 넘어서 이걸 왜 쓸까? createPortal의 핵심이 무엇인지에 대한 질문으로 이어진다.

필자가 생각하는 createPortal의 핵심은 다음과 같다.

실제 DOM노드에 렌더링하면서도 react컴포넌트 트리에서는 본인의 고향(?)에 남아 본연의 역할을 수행

이게 무슨뜻일까?

<App>
  <Header />
  <Content>
    <Modal /> //위로 올라가고 싶은 모달
  </Content>
  <Footer />
</App>

<Modal/>은 컨텐츠 내부에 있지만, createPortal을 이용하여 원하는 위치에 렌더링 시킬 수 있다. 하지만, 비록 모달 컴포넌트가 고향(?)을 떠났을지라도, 여전히 `Contents`의 자식으로 남아있고 이벤트버블링도 Content로 전달된다.

조금 더 압축을 하자면, createPortal은 그저, “렌더링위치만” 변경시켜줄 뿐인 것이다. 이처럼 특정 위치에 있어야 할 컴포넌트를 그저 원하는 위치로 이동시켜줄 뿐이다. 포탈이 무엇을 옮겨주듯, 그저 렌더링 위치를 옮겨준것일 뿐인 것이다.


🚀createRoot 좀 더 자세히 보기

다음은 createRoote()의 내부 라이브러리를 입맛대로 요약해 본 코드이다.

코드보기(클릭)👇
export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
){
  // 1. 유효한 컨테이너인지 확인
  checkValidContainer(container);

  // 2. 옵션을 적용
  const {
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  } = applyOptions(options);

  // 3. 컨테이너 생성
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );

  // 4. 컨테이너와 DOM 연결 및 이벤트 리스너 추가
  markContainerAsRoot(root.current, container); //(이제부터 너가 루트임)
  const rootContainerElement = getRootContainerElement(container);

  // 5. ReactDOMRoot 객체 생성 및 반환
  return new ReactDOMRoot(root);
}

그냥 뿌리가 될 부분을 받아서 옵션을 적용해 주는 모습이다. 딱히 인상적이거나 주목할만한점은 없는 것 같다. 굳이 하나를 꼽자면, 코드 내에 “ConcurrentRoot”타입의 컨테이너를 만들어 주는 부분이다.


🚀돔접근

마지막으로 돔접근에 대해 조금 더 살펴보고 글을 마치도록 하겠다.

그렇다면 createPortal이 생기기 전의 돔접근 방법에는 어떤게 있을까?

채널톡과제때useRef썼던거

얼마 전 스크롤링 관련된 함수를 구현할 때, useRefgetElementById중 어떤걸 사용할지 고민했던 기억이 있다.

둘다 DOM요소에 대한 참조를 얻는 방식이지만 조금 다르다. useRef도 Hook이며 값이 변경되어도 리렌더링이 되지 않는 변수를 저장하기 위해 사용된다. getElementById는 모두가 아는 DOM을 찾아주는 DOM API이다. 리액트에서는 컴포넌트 내부에서는 useRef를 사용하는 것을 권장하지만, 공식문서의 코드들에서도 getElementById가 떡하니 들어가 있는 모습을 종종 볼 수 있는것으로 보아, 실 사용에서는 크게 차이가 없지 않을까 싶다.

관련된 틀-메소드를 하나 소개하고 끝내도록 하겠다.

  focusInput() {
    const inputElement = findDOMNode(this.inputRef.current);
    inputElement.focus();
  }

findDomNode라는 메소드는 무려 리액트 0.13v부터 존재해왔던 메소드다. 지금은 ref를 이용해 완전히 대체할 수 있고, 성능도 구리고 문제도 많기 때문에 권장되지 않는 사용법이라고 한다. 곧 deprecated될 예정이라고 애초에 쓰지 말라고 리액트에서 경고하고 있다. (링크)

아래처럼 직접 돔을 조작 할 수 있다. (돔 선택 후 속성 직접 바꾸기) findDomNode로 색바꾸기

결과

옛날 옛적에는 이와 같은 함수들을 이용해 createPortal을 수동으로 구현했다기도 하지만(카더라 통신), 뭐 이제는 몰라도 되는 메소드일 것 같다.

Dom접근을 할 때에는 권장되는 방식인 useRef와 권장된다는 말은 없지만 공홈에서도 자주 쓰는 DomAPI들을 사용해서 접근하면 될 것 같다.


🚀마무리

우리는 리액트의 숨겨진(?)기능 createPortal과 createRoot를 살펴보았다. 프로젝트를 셋업할 때, 외부 컨텐츠들을 볼 때 사용되지만 직접적으로 찾아보고 자주 사용하는 함수들은 아닐 것이다.

하지만 18버전 이후로 안정적으로 concurrent mode를 지원해주는 createRoot과 컴포넌트 위치를 유지하며 다른곳에 렌더링 시켜줄 수 있는 createPortal을 알고있다면, 조금 더 나은 코드를 작성하는데 도움이 될 것이다. 남다른 실력자들은 조약돌 속의 보석을 알고있는 사람들이기 때문이다. (사실 영영 쓸일이 없을수도…?)


맨 위로 이동하기

react 카테고리 내 다른 글 보러가기

댓글 남기기