React Portal로 모달 생성하기

2023. 10. 19. 17:27프레임워크̸라이브러리/React

리액트는 index.html에서 "root"라는 id를 가진 div에 App.js를 렌더링 시켜 사용자에게 보여줄 화면을 정의한다. 즉, 모든 리액트 컴포넌트/페이지가 root라는 컨테이너 안에 담기게 된다.

그런데 서비스를 만들다보면 modal, dialog, overlay 등 메인 컨테이너가 아닌 별도의 컨테이너에 내용을 담아야 하는 경우가 생긴다. 이 때 z-index만으로 화면을 구성해도 되지만, z-index에 대한 정의가 제대로 되지 않을 경우 원하는 결과가 나오지 않을 수 있다.

그래서 사용하는 것이 리액트의 Portal이다.

 

 

Portals

Portal은 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링하는 최고의 방법을 제공합니다.

 

1. Portal을 사용하지 않고 모달을 생성했을 때 발생하는 오류

[page.jsx]

import React, { useState } from 'react';
import Modal from './Modal';

import styles from './modal.module.scss';

const page = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div>
      <div className={styles.modalWrapperStyle}>
        <button onClick={() => setIsModalOpen(true)}>open</button>

        {/* modal */}
        <Modal open={isModalOpen} onClose={() => setIsModalOpen(false)}>
          modal contents
        </Modal>
      </div>

      <div className={styles.higherIndexWrapperStyle}>Z-index 2</div>
    </div>
  );
};

export default page;

 

[Modal.jsx]

import React from 'react';

import styles from './modal.module.scss';

const Modal = ({ open, onClose, children }) => {
  if (!open) return null;

  return (
    <>
      <div className={styles.overlayStyle} />
      <div className={styles.modalStyle}>
        <button onClick={onClose}>close</button>
        {children}
      </div>
    </>
  );
};

export default Modal;

 

[modal.module.scss]

.modalWrapperStyle {
  position: relative;
  z-index: 1;
}

.higherIndexWrapperStyle {
  position: relative;
  z-index: 2;
  background-color: cyan;
  padding: 10px;
}

.modalStyle {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background-color: #fff;
  padding: 50px;
  z-index: 999;
}

.overlayStyle {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.7);
  z-index: 999;
}

z-index가 각각 1, 2인 Wrapper div를 두 개 만들고 z-index: 1;인 div에 모달 컴포넌트를 넣었다.

 

실행 결과

z-index가 1로 설정된 부모의 하위에 들어있는 자식들은 아무리 z-index를 높게 설정해도(ex. z-index: 999;) 1보다 커질 수 없다. 따라서 z-index 2로 설정된 부분이 위의 결과처럼 overlay부분을 뚫고 나오게된다.

 

 

2. Portals 적용하기

🔗 createPortal

<div>
  <SomeComponent />
  {createPortal(children, domNode, key?)}
</div>

children: 엘리먼트, 문자열, 혹은 fragment와 같이 어떤 종류든 렌더링할 수 있는 요소

domNode: DOM 엘리먼트

key: portal의 고유한 문자열 또는 숫자로 사용될 키 (옵션)

 

 

1) Container 생성

[public/index.html]

...
<div id="root"></div>
<div id="portal"></div>
...

 

2) createPortal 이용하기

[Modal.jsx]

import React from 'react';
import { createPortal } from 'react-dom';

import styles from './modal.module.scss';

const Modal = ({ open, onClose, children }) => {
  if (!open) return null;

  return createPortal(
    <>
      <div className={styles.overlayStyle} />
      <div className={styles.modalStyle}>
        <button onClick={onClose}>close</button>
        {children}
      </div>
    </>,
    document.getElementById('portal'),
  );
};

export default Modal;

실행 결과

따란~🤗 이렇게 createPortal로 설정한 부분이 원하는 DOM 엘리먼트 안으로 쏙 들어간 걸 확인할 수 있다!

 

 

Portal을 통한 이벤트 버블링

포탈은 이벤트 버블링이 가능하다. 비록 포탈로 생성한 부분이 부모 DOM 밖에서 생성되더라도 (DOM 트리에서의 위치와 상관없이) portal은 여전히 React 트리에 존재하기 때문에 React 트리에 포함된 상위로 이벤트 버블링이 가능하다.

* Event Bubbling: 중첩된 자식 요소에서 이벤트가 발생할 때 그 이벤트가 부모로 전달되는 것.

 

∴ 비록 상위 DOM 트리가 아니더라도, React 트리에서 상위이면 이벤트 버블링이 가능하다.

 

이벤트 버블링 예시

[page.jsx]

import React, { useState } from 'react';
import Modal from './Modal';

import styles from './modal.module.scss';

const page = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <div>
      <div
        className={styles.modalWrapperStyle}
        onClick={() => console.log('clicked')} // here
      >
        <button onClick={() => setIsModalOpen(true)}>open</button>

        {/* modal */}
        <Modal open={isModalOpen} onClose={() => setIsModalOpen(false)}>
          modal contents
        </Modal>
      </div>

      <div className={styles.higherIndexWrapperStyle}>Z-index 2</div>
    </div>
  );
};

export default page;

모달을 클릭하면 이벤트가 상위로 전달되어서 상위 요소인 div에도 이벤트가 전달되고, div에 있는 핸들러가 호출된다.