✨ Portal이란?

React에서 일반적으로 컴포넌트를 렌더링하면, 그 컴포넌트는 부모 컴포넌트의 DOM 계층 구조 안에 포함된다.
예를 들어 App.tsx의 루트 컴포넌트는 보통 public/index.html 파일 안의 #root 엘리먼트에 렌더링되며, 자식 컴포넌트도 모두 그 안에서 렌더링된다.

하지만 어떤 UI는 이와 같은 구조에서 벗어나 "다른 DOM 위치에 렌더링되어야" 할 때가 있다.
대표적인 예로는 다음과 같은 UI들이 있다:

  • 모달 창 (Modal)
  • 툴팁 (Tooltip)
  • 알림창 / 토스트 (Toast)
  • 드롭다운 메뉴 (Dropdown)

이러한 UI 요소들은 시각적으로 항상 최상위에 떠 있어야 하고,
부모 요소의 CSS 속성(overflow: hidden, z-index, position: relative 등)의 영향을 받지 않아야 한다.

이때 사용하는 것이 바로 React Portal이다.


🔍 공식 문서에서의 설명

"Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component."

즉, 포털은 부모 컴포넌트의 DOM 계층 외부에 있는 다른 DOM 노드에 자식 컴포넌트를 렌더링하는 방법이다.

📚 React 공식 문서 - Portals


📌 왜 Portal을 써야 할까?

상황Portal이 필요한 이유
모달을 띄우고 싶은데 부모의 z-indexoverflow에 가려진다Portal로 DOM 위치를 이동시켜 해결 가능
툴팁/알림창이 레이아웃의 제약 없이 떠 있어야 한다부모 컴포넌트의 스타일 영향 없이 최상단에서 렌더링
시각적인 계층과 코드 구조를 분리하고 싶다코드 구조는 유지하되, 렌더링은 독립된 DOM에 수행

🧪 Portal 기본 사용법

1. index.html에 렌더링 위치를 만든다

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

### 2. React에서 createPortal을 사용한다

```tsx
// Modal.tsx
import ReactDOM from 'react-dom';
import React from 'react';

const Modal = ({ children }: { children: React.ReactNode }) => {
  const modalRoot = document.getElementById('modal-root');
  if (!modalRoot) return null;

  return ReactDOM.createPortal(
    <div className="modal-backdrop">
      <div className="modal-content">
        {children}
      </div>
    </div>,
    modalRoot
  );
};

export default Modal;

3. 사용 시 일반 컴포넌트처럼 감싸서 사용한다

<Modal>
  <h1>정말 삭제하시겠습니까?</h1>
  <button>취소</button>
  <button>삭제</button>
</Modal>

이렇게 사용하면 컴포넌트 트리 상으로는 부모-자식 구조이지만,
실제로는 #modal-root에 렌더링되기 때문에 레이아웃 충돌 없이 최상단에 위치할 수 있다.


💼 실제 프로젝트에서 Portal 적용한 사례

나는 이번 프로젝트에서 공통 모달 컴포넌트를 만들면서 처음으로 React Portal을 사용하였다.

초기에는 일반적인 구조로 모달을 렌더링했지만,
부모 컴포넌트의 overflowz-index 속성에 가려지는 문제가 발생하였다.

따라서 모달을 루트 DOM에서 분리된 별도의 위치에 렌더링해야 했고,
이때 React Portal을 도입하게 되었다.


🧩 내가 작성한 모달 코드

import ReactDOM from "react-dom";
import React from "react";
import styled from "@emotion/styled";
import Button from "./Button";

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  onConfirm: () => void;
  message: string;
  leftButtonText: string;
  rightButtonText: string;
}

const Modal = ({
  isOpen,
  onClose,
  onConfirm,
  message,
  leftButtonText,
  rightButtonText,
}: ModalProps) => {
  if (!isOpen) return null;

  const modalRoot = document.getElementById("modal-root");
  if (!modalRoot) return null;

  const handleConfirm = () => {
    onClose(); // 모달 닫기
    onConfirm(); // 사용자 정의 동작 실행
  };

  return ReactDOM.createPortal(
    <Backdrop>
      <Container>
        <Message>{message}</Message>
        <ButtonGroup>
          <Button className="left-button" onClick={onClose}>
            {leftButtonText}
          </Button>
          <Button className="right-button" onClick={handleConfirm}>
            {rightButtonText}
          </Button>
        </ButtonGroup>
      </Container>
    </Backdrop>,
    modalRoot
  );
};

export default Modal;

✅ 이 코드는 다음과 같은 구조로 동작한다:

  • isOpentrue일 때만 렌더링된다.
  • ReactDOM.createPortal(...)을 통해 실제 렌더링은 #modal-root에 일어난다.
  • 버튼을 누르면 onCloseonConfirm 이벤트가 순서대로 실행된다.

📌 이렇게 적용하면서 배운 점

처음엔 “Portal이 어렵고 낯선 개념”처럼 느껴졌지만,
실제로는 단순히 렌더링 위치를 분리해주는 도구였다.

구조적으로 Modal 컴포넌트는 부모 DOM과 분리되어 렌더링되지만,
여전히 React 트리 안에서 상태와 이벤트를 그대로 공유할 수 있었다.

앞으로는 모달, 드롭다운, 툴팁 등은 Portal을 활용하는 게 더 안정적이라는 걸 깨달았다.


🔗 참고 자료


✅ 마무리 정리

  • Portal은 컴포넌트를 다른 DOM 위치에 렌더링할 수 있게 해주는 기능이다.
  • 일반적으로 모달, 알림, 툴팁, 드롭다운 UI에 자주 사용된다.
  • DOM 구조를 분리하면서도, React 트리 내에서 상태와 이벤트 관리는 그대로 유지된다.
  • 사용법은 매우 간단하다: ReactDOM.createPortal(element, container)
profile
아는만큼 보이는🌱👀

0개의 댓글