개발 공부 일지/React

[React] React Portal을 이용한 모달(Modal) 구현

dev-hpk 2025. 1. 8. 18:15

최근 프로젝트에서 모달 컴포넌트를 사용하는데 많은 불편함을 느껴 React Portal을 사용하게 되었습니다.

 

제가 프로젝트를 진행하면서 겪은 불편함을 간단하게 정리했으니 필요하신 분들은 아래 예시를 확인해 주세요😊

더보기

어떤 불편함이 있었냐구요🤔

아래 사진은 chrome의 개발자 도구를 통해 확인한 모달입니다.

modal의 depth가 깊어지는 문제
modal의 depth가 깊어지는 문제

 

단순히 detph가 깊어지는데 무슨 불편함이 있는지 궁금하실 수 있겠네요❗

modal의 depth에 의한 z-index 문제

 

위 예시를 보시면 DOM의 계층 구조에 의해 모달의 z-index에 문제가 생긴 경우입니다.

import styled from "styled-components";

function PortalExample() {
  const Title = styled.h1`
    position: fixed;
    top: 0;
    left: 0;
    z-index: 1;

    background-color: #fff;
  `;

  return <Title>Portal 예시입니다!</Title>;
}

function App() {
  const Container = styled.div`
    position: relative;

    width: 100vw;
    height: 100vh;
  `;

  const Background = styled.div`
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1;

    width: 100%;
    height: 100%;

    background-color: #000;
  `;

  return (
    <Container>
      React Portal Example
      <Background />
      <PortalExample />
    </Container>
  );
}

export default App;
계층 구조에 의한 z-index의 불편함

 

h1과 div의 z-index가 1로 같은데, 계층 구조에 의해 h1이 안 보이네요.

h1을 상위 계층으로 끌어올리면 어떻게 될까요🤔🤔

상위 계층으로 h1을 올려 z-index를 해결

상위 계층으로 끌어올리니 z-index 문제가 해결되었네요❗

상위 계층으로 요소를 끌어올린다고 z-index가 더 작아도 되는 것은 아닙니다.

하지만 z-index 문제를 우회하거나 이벤트 버블링을 제어하는 등 어느 정도 유리한 점은 있겠죠👀

React Portal을 선택한 이유💡

React로 프로젝트를 진행하다 보면 부모 DOM 계층구조를 무시하고 다른 DOM 노드에 컴포넌트를 렌더링해야 할 때가 있습니다. 제 경우에는 모달을 body 하위에 렌더링 하기 위해 React Portal을 선택했습니다.

React Portal이란?

React Portal은 React 컴포넌트를 DOM 트리의 다른 위치에 렌더링 할 수 있도록 해주는 기능입니다. 일반적인 React 컴포넌트는 부모 컴포넌트의 DOM 트리에 렌더링되지만, Portal을 사용하면 부모 DOM 트리 외부에 컴포넌트를 렌더링할 수 있습니다.

 

React Portal 사용법

createPortal(children, domNode)

: createPortal을 호출하여 일부 JSX와 렌더링할 DOM 노드를 전달합니다.

import { createPortal } from 'react-dom';

{createPortal(
    <p>This child is placed in the document body.</p>,
    document.body
)}
  • children : 렌더링할 React 노드 (React 엘리먼트, 문자열, 숫자 등).
  • domNode : 렌더링할 DOM 노드

 

이제 Modal을 만들고 React Portal을 사용해 보겠습니다❗

Modal 컴포넌트 생성

import { createPortal } from "react-dom";
import styled from "styled-components";

function Modal({ children }) {
  const Modal = styled.div`
    position: fixed;
    top: 0;
    bottom: 0;
    z-index: 100;

    display: flex;
    align-items: center;
    justify-content: center;

    width: 100%;
    height: 100%;
  `;

  const ModalOverlay = styled.div`
    position: absolute;
    top: 0;
    left: 0;

    width: 100%;
    height: 100%;

    background-color: rgba(0, 0, 0, 0.5);
  `;

  const ModalContent = styled.div`
    overflow: hidden;
    position: relative;
    z-index: 1;

    max-width: 1000px;
    width: 80%;
    max-height: 80%;
    height: fit-content;

    padding: 20px;

    border-radius: 2rem;
    background-color: #fff;
  `;

  return createPortal(
    <Modal>
      <ModalOverlay />
      <ModalContent>{children}</ModalContent>
    </Modal>,
    document.body
  );
}
export default Modal;

 

Modal 컴포넌트를 App 컴포넌트에서 import해 사용해 보겠습니다.

어떤 결과가 나올까요👀

function App() {
  const Container = styled.div`
    position: relative;

    width: 100vw;
    height: 100vh;
  `;

  return (
    <Container>
      React Portal Example
      <Modal>모달 컴포넌트</Modal>
    </Container>
  );
}

모달 컴포넌트

Modal 컴포넌트가 body 하위에 잘 렌더링 되었네요👍

모달이 렌더링 되는 것을 확인했으니, 열고 닫는 기능도 추가해 보겠습니다.

 

모달은 자주 사용하는 컴포넌트이다 보니 커스텀 훅으로 만들어 볼게요❗❗

 

useModal 커스텀 훅 생성

import { useState } from "react";

function useModal() {
  const [isOpen, setIsOpen] = useState(false); // 모달의 열고 닫힘을 관리하기 위한 state

  const openModal = () => setIsOpen(true); // 모달 열기
  const closeModal = () => setIsOpen(false); // 모달 닫기

  return {
    isOpen,
    openModal,
    closeModal,
  };
}

export default useModal;

 

useModal 커스텀 훅을 만들었으니, Modal에 적용해야겠죠😁

import { createPortal } from "react-dom";
import styled from "styled-components";

function Modal({ children, isOpen, closeModal }) {
  {/* 스타일은 생략 하겠습니다. */}

  if (!isOpen) return null; // isOpen이 false인 경우 null을 리턴, 모달 렌더링 X

  return createPortal(
    <Modal>
      <ModalOverlay onClick={closeModal} /> // Dim 처리 된 영역을 클리하면 모달이 닫히도록 클릭 이벤트 추가
      <ModalContent>{children}</ModalContent>
    </Modal>,
    document.body
  );
}
export default Modal;

 

function App() {
  const { isOpen, openModal, closeModal } = useModal();

  {/* 스타일 생략 */}

  return (
    <Container>
      <button type='button' onClick={openModal}>
        모달 열기
      </button>
      <Modal isOpen={isOpen} closeModal={closeModal}>
        모달 컴포넌트
      </Modal>
    </Container>
  );
}

export default App;

useModal 커스텀 훅 적용 화면

모달을 열고 닫는 기능이 잘 동작하네요!

앞으로 진행하는 프로젝트에서 유용하게 사용할 수 있을 것 같습니다❗

 

React Portal을 사용해 보고 느낀 점📕

  • 유연한 DOM 구조 : DOM 계층을 벗어나 렌더링이 가능해 UI 구현이 매우 편리하다.
  • CSS 스타일 충돌 최소화 : 부모 컴포넌트의 스타일 영향을 받지 않아 독립적인 디자인이 가능해 편리하다.
  • 학습 곡선이 낮음 : React에서 기본으로 제공하는 기능이고, API가 간단해서 처음 사용해도 쉽게 적응할 수 있었다.

 

 

[React] Emotion - React 컴포넌트 스타일링

Emotion🤔Emotion은 리액트에서 스타일을 관리하기 위한 CSS-in-JS 라이브러리로, 자바스크립트 코드 내에서 직접 CSS를 작성하고 적용할 수 있게 해 줍니다. Emotion 외에도 Styled-Components가 주로 사용됩

dev-hpk.tistory.com

 

 

[JS] finally 이해하기

자바스크립트에서 try - catch - finally 구문은 예외 처리를 위한 강력한 도구입니다. 이 구문에서 finally 블록은 예외 발생 여부와 관계없이 항상 실행되는 코드 블록으로, 주로 리소스 정리나 마무

dev-hpk.tistory.com