Front-End(Web)/React - 프레임워크(React, Next)

[React] Portal (포탈), Modal 구현하기

ttaeng_99 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/

반응형