ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [React] Portal (포탈), Modal 구현하기
    Front-End(Web)/React - 프레임워크(React, Next) 2022. 2. 6. 16:08
    반응형

    회사에서 선배와 React로 모달 구현에 대해 고민하다가 알게된 내용이었다.

     

    우리 회사 프로젝트는 Vue를 사용하기 때문에, 플러그인(this.$modal) 메서드를 통해 루트에 모달 컴포넌트를 주입, 삭제해서 표현한다.

    하지만 React의 경우엔 어떻게 구현되는지 몰랐으며, 전역모달이 구현되는 영역에 Context API를 적용하면 되나 이 영역들이 리렌더링된다는 단점이 있었다.

     

    그렇기에 전역상태를 통해 모달을 on/off 하는 방법을 찾아봤으며, 이 때 React의 Portal 기능을 통해 좀 더 효율적인 구현이 가능했다.


    💙 Portal 이란?

    React 공식문서에 따르면, Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법이라고 소개하고 있다. (React v16 출시)

     

    통상은, React의 index.html의 #root <div> 외에, 다른 루트에 Modal, Dialog, Tooltip 등을 띄우기 위해 많이 사용되나,

    반드시 제2의 루트가 아니라 단순히 부모 컴포넌트 DOM 외부의 DOM노드에 렌더링하기 위한 목적으로도 사용될 수 있다.

     

    - Portal을 사용하는 이유

    전역모달을 띄우는 범용적인 방법은, <App> 바로 하위에 <Modal> 컴포넌트를 만든 뒤 전역상태(Boolean)을 통해 show/hide 하는 것이다.

     

    이 역시도 상위단에 DOM노드를 생성했을뿐만 아니라, 단순한 알람모달의 경우에도 전역상태에 의존한다는 불합리함이 존재한다.

    또한, 모달이 켜진 상태에서 업데이트 등으로 부모 엘리먼트가 리렌더된다면, tree구조에 따라 자식인 모달 역시 영향을 받게 될 것이다.

    그리고, 부모 컴포넌트의 스타일링에 제약을 받아 z-index 와 같은 후처리를 해야한다는 점도 있다.

     

    이러한 DOM Tree 상에서의 부모-자식 간의 제약에서 자유로워지기 위해 Portal 기능을 사용하게 되는 것이다.

    또한, Portal에 렌더링된 자식요소는 이벤트 버블링이 반영되기 때문에 부모요소와의 통신 측면에서도 유용하다.


    💙 Portal로 Modal 구현하기

    기본적으로, ReactDOM.createPortal() 메서드를 통해 Portal 기능을 사용할 수 있다.

    이 Portal이 가장 많이 사용되는 Modal 구현방법에 대해, 실습해본 내 코드를 기반으로 사용법을 익히는 것이 일타쌍피라고 생각한다!

     

    1. Portal 렌더 노드 심어주기

    <!-- public/index.html -->
    
    <!DOCTYPE html>
    <html lang="en">
      <!-- ... -->
      <body>
        <div id="root"></div>
        <div id="modal-root"></div>
      </body>
    </html>

    최상단 요소인 #root와 형제관계인 #modal-root 요소를 만들어준다. 여기에, Portal을 통해 모달 컴포넌트가 렌더링될 것이다.

     

     

    2. ModalPortal.tsx 만들기

    // ModalPortal.tsx
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    const ModalPortal: React.FC = ({ children }) => {
      const modalRoot = document.getElementById('modal-root');
      return ReactDOM.createPortal(children, modalRoot);
    };
    
    export default ModalPortal;

    Portal을 통해 #modal-root에 child 컴포넌트를 렌더링해주는 Container 컴포넌트를 만들어준다.

    ReactDOM.createPortal() 메서드로 렌더링이 가능하며, 인자는 렌더링할 컴포넌트, 타겟노드 2개를 넣어준다.

     

     

    3. ModalFrame.tsx 만들기

    // ModalFrame.tsx
    
    import React from 'react';
    import ModalPortal from './ModalPortal';
    import styles from './modal.module.less';
    
    type Props = {
      children: string;
      setOnModal: (state: boolean) => void;
    };
    
    const ModalFrame: React.FC<Props> = ({ children, setOnModal }: Props) => {
      return (
        <ModalPortal>
          <div className={styles.modalDim} onClick={() => setOnModal(false)}>
            <div className={styles.modalBox}>
              {children}
              <button className="close" onClick={() => setOnModal(false)}>
                X
              </button>
            </div>
          </div>
        </ModalPortal>
      );
    };
    
    export default ModalFrame;

    다음으로, Modal에 공통으로 사용될 레이아웃 Container 컴포넌트를 만들어주었다. 

    <ModalPortal>로 랩핑하여 내부요소를 포탈에 띄워준다. 이제 <ModalFrame> 으로 감싼 내부요소를 모달창으로 띄울 수 있다.

    // TestModal.tsx
    
    import React from 'react';
    import ModalFrame from './ModalFrame';
    
    type Props = {
      setOnModal: (state: boolean) => void;
    };
    
    const TestModal: React.FC<Props> = ({ setOnModal }) => {
      return <ModalFrame setOnModal={setOnModal}>테스트 모달!</ModalFrame>;
    };
    export default TestModal;

     

    또, setOnModal 세터함수를 Props로 받아왔는데, dim영역 및 X버튼의 클릭 이벤트가 버블링되기 때문이다.

    이를 통해, 모달을 사용한 부모 컴포넌트로 모달 표현여부(Boolean)를 false로 갱신하여 모달을 끄는 동작까지 구현할 수 있다.

     

     

    4. Modal 조건부 렌더링

    import React, { useState } from 'react';
    import PrimaryButton from '@/views/components/common/button/PrimaryButton';
    import TestModal from '@/views/components/common/modal/TestModal';
    import styles from './Example.module.less';
    
    const Example = () => {
      const [onModal, setOnModal] = useState(false);
    
      return (
        <div className={styles.example}>
          <PrimaryButton theme="green" onClick={() => setOnModal(true)}>모달</PrimaryButton>
          {onModal && <TestModal setOnModal={(bool) => setOnModal(bool)} />}
        </div>
      );
    };
    
    export default Example;

    먼저, 모달 표현여부를 onModal 상태값(Boolean)으로 토글링한다. 여기에 따라, <TestModal>이 조건부 렌더링된다.

     

    특히, TestModal > ModalFrame > ModalPortal 을 통해 setOnModal() 메서드를 Props로 넘겨줬으며,

    클릭 이벤트가 버블링되며 onModal을 false로 바꾸며 모달을 끄는 동작까지 구현할 수 있었다.

     

    모달이 구현된 모습이다! 위 코드는 특정 컴포넌트에서 모달 렌더링 여부(onModal)이 한정적으로 조회 및 제어된다.

    로그인 모달과 같은 전역모달은 Redux나 Recoil과 같은 전역상태를 통해 렌더링해야 할것이며, 이러한 제어는 useModal() Hooks API로 유용하게 구현할 수 있다. (참고링크)

     

     

    💙 Portal과 Render 의 차이점

    RenderReactDOM.render(element, container) 문법으로 사용하며, Portal과 매우 유사하다.

    하지만 가장 큰 차이점은 Render는 새로운 React Lifecycle을 생성하기 때문에, React App이나 독자적인 컴포넌트 렌더링에 사용된다. (ex) Notification)

    구분 Portal Render
    공통점 특정 DOM으로 컴포넌트를 렌더링
    호출 위치 React Render 내부* 어디서든 가능
    LifeCycle 호출한 부모 컴포넌트에 종속됨 새로운 생명주기로 관리됨
    Unmount Lifecycle에 의해 언마운트됨 직접 ReactDOM.unmountComponentAtNode() 사용
    event 실제 렌더링 위치는 다르나, 부모요소와 버블링/캡처링 가능 최상위 컴포넌트로 동작
    활용 예시 특정 컴포넌트를 부모요소 외부에 렌더링하고자 할 때 React App을 새로 생성할 때

    Portal에 대한 간단한 포스팅이 끝났다.

    Portal의 개념에 대해서도 공부했지만, 무엇보다 이전과는 다른 방법으로 React에서 모달을 구현해본 것이 큰 경험치가 되었다!

    위에서 언급했듯이, 전역모달을 만들기 위한 Recoil의 접목과 useModal() Hooks API까지 이 글에 첨부하거나 새로이 포스팅해봐야겠다.

     

    📎 출처

    - [Portals] React 공식문서 : https://ko.reactjs.org/docs/portals.html

    - [Portal로 Modal 구현] bomdong 님의 블로그 : https://dev-bomdong.tistory.com/m/21  

    - [Portal로 전역모달 구현] 마이구미 님의 블로그 : https://mygumi.tistory.com/406

    - [Portal과 Render] 김재서 님의 블로그 : https://jaeseokim.dev/React/React-Portal_Render%EC%9D%98_%EC%B0%A8%EC%9D%B4%EC%A0%90_%ED%99%9C%EC%9A%A9%EB%B0%A9%EC%95%88_%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0/

    반응형
Designed by Tistory.