React에서 Modal 만들 때 Portal를 왜 사용해야 할까?

React에서 Modal 만들 때 Portal를 왜 사용해야 할까?

·

2 min read

💣 겪고 있는 문제...

아래와 같이 모달을 포함한 코드가 있다.

// App.js
const App = () => {
  return (
    <div>
      <h1>Title</h1>
      <Modal />
    </div>
  );
};

// Modal.js
const Modal = () => {
  return (
    <>
      <div id="overlay"></div>
      <aside id="modal">
        <h1>modal</h1>
        <p>contents</p>
      </aside>
    </>
  );
};

위 코드를 실제 돔으로 렌더링하면 아래와 같은 결과를 얻을 수 있다.

모달 컴포넌트에 스타일을 제대로 부여한다면 화면 상에서 문제는 없어 보인다.

하지만 스크린리더가 렌더링되는 HTML 코드를 해석할 때 모달이라는 존재를 인식할 수 없게 된다. 또한 의미적이나 구조적인 관점에서 모달이 모든 영역 위에 있는 지 알 수 없다.

이러한 문제는 모달 뿐만 아니라 side drawer, dialog, overlay 등에서도 나타날 수 있다.

이 문제는 Portal을 사용하면 싹- 해결할 수 있다.

Portal이 뭔데?

Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드 자식을 렌더링하는 최고의 방법을 제공한다. 무슨 말인지 잘 모르겠어요!!

React는 부모 컴포넌트가 렌더링되면 자식 컴포넌트가 렌더링되는 Tree 구조를 따른다. 이런 구조는 자식 컴포넌트가 모달일 경우에 부모 컴포넌트의 스타일 속성에 제약을 받아 번거로운 후처리를 해줘야 한다는 단점이 있다. (💣에서 설명한 것과 같이.)

이때 부모-자식 관계를 유지하면서 자식이 독립적인 위치에서 렌더링 하도록 도와주는 것이 Portal이다. (탈캥거루족🦘)

🎢 어떻게 사용하는 건데?

  1. 렌더링 될 위치(root) 만들기

     <!-- index.html -->
     <body>
         <div id="backdrop-root"></div>
         <div id="modal-root"></div>
         <div id="root"></div>
         <!-- 중략 -->
     </body>
    

    배경 오버레이 영역과 모달 영역을 body 태그 바로 아래에 위치 시키고자 위와 같은 위치에 div 요소를 추가한다.

  2. Portal 컴포넌트 만들기

     import ReactDom from "react-dom";
    
     const Portal = ({ children, root }) => {
         const el = document.getElementById(root)
         return ReactDom.createPortal(children, el)
     }
    
     export default ModalPortal
    

    childrenroot라는 props를 만들어 재사용성을 높인다. children은 모달이나 오버레이와 같은 콘텐츠 정보가 들어있고, root에는 렌더링 될 위치에 대한 id 정보가 들어가게 된다.

    참고로 ReactDom.createPortal의 첫 번째 매개변수는 렌더링 할 자식 컴포넌트를, 두 번째 매개변수에는 렌더링 될 DOM Element를 넣어준다.

  1. Modal 및 Backdrop 컴포넌트에 Portal 연결하기

     const Modal = () => {
       return (
         <Portal root="modal-root">
           <aside id="modal">
             <h1>modal</h1>
             <p>contents</p>
           </aside>
         </Portal>
       )
     }
    
     const Backdrop = () => {
       return (
         <Portal root="backdrop-root">
           <div id="overlay"></div>
         </Portal>
       )
     }
    

    각자의 root 안에 들어갈 콘텐츠를 넣고 Portal 로 감싼다.

🌷 아름답게 바뀌었다

마치며

단순히 화면에 보이는 스타일만 제대로 적용하면 끝이라는 생각이 완전히 바뀌게 되는 계기가 되었다. 컴포넌트 트리 구조와 이벤트 전파에 대해 배우게 되는 시간이었다.