본문 바로가기

React

React portal

https://ko.legacy.reactjs.org/docs/portals.html

 

React portal이란?

리액트에서 일반적으로 컴포넌트는 부모 요소 안에 렌더링된다. 하지만 Portal을 사용하면 특정 DOM 요소에 렌더링할 수 있다. 즉, JSX 구조상으로는 부모 안에 있지만, 실제 DOM에서는 완전히 다른 곳에 렌더링 되도록 하는 것이다.

 

왜 필요할까?

예를 들어보자. 기본적으로 modal, tooltip 같은 UI 요소는 화면 최상단 레이어에 떠야 한다. 그런데 부모 컨테이너가 overflow:hidden과 같은 스타일이 적용되면 잘려서 보이지 않는 문제가 발생할 수 있다. 이런 문제를 portal로 해결할 수 있다.

 

코드로 살펴보자

먼저, 두가지 상황을 코드로 구현한 것이다. 한개의 modal은 portal을 적용하지 않아 modal의 일부분이 hidden 처리가 될 것이고, 한개의 modal은 portal이 적용되어 최상단에 전체가 다 보일 것이다.

개발자 도구도 함께 살펴 보았는데, portal을 적용하지 않은 modal은 부모 밑에 렌더링 되었지만, 적용한 modal은 body 안에 렌더링 된 것을 확인할 수 있었다.

// App.jsx
import React, { useState } from "react";
import Modal from "./Modal"; // ✅ Portal 사용 X (부모 안에서 렌더링됨)
import PortalModal from "./PortalModal"; // ✅ Portal 사용 O (body에 렌더링됨)

export default function App() {
  const [isOpen, setIsOpen] = useState(false);
  const [isPortalOpen, setIsPortalOpen] = useState(false);

  return (
    <div
      style={{
        overflow: "hidden", // ✅ 부모가 overflow: hidden을 가짐!
        position: "relative",
        height: "200px",
        width: "200px",
        backgroundColor: "orange",
      }}
    >
      <h2 style={{ fontSize: "14px" }}>부모 요소 (overflow: hidden 적용됨)</h2>

      {/* 일반 모달 */}
      <button onClick={() => setIsOpen(true)}>기본 모달 열기</button>
      {isOpen && (
        <Modal onClose={() => setIsOpen(false)}>
          <span>일반 모달</span>
        </Modal>
      )}

      {/* Portal을 사용하는 모달 */}
      <button
        onClick={() => setIsPortalOpen(true)}
        style={{ marginLeft: "10px" }}
      >
        포탈 모달 열기
      </button>
      {isPortalOpen && (
        <PortalModal onClose={() => setIsPortalOpen(false)}>
          <span>포탈 모달</span>
        </PortalModal>
      )}
    </div>
  );
}
// Modal.jsx
import React from "react";

const Modal = ({ children, onClose }) => {
  return (
    <div
      style={{
        position: "absolute", // 부모 기준으로 위치 결정됨
        top: "50%",
        left: "50%",
        transform: "translate(-50%, -50%)",
        width: "250px",
        height: "120px",
        background: "rgba(0, 0, 0, 0.8)",
        color: "#fff",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        borderRadius: "8px",
        zIndex: 10, // 부모의 z-index에 영향을 받을 수 있음
      }}
    >
      {children}
      <button onClick={onClose} style={{ marginLeft: "10px" }}>
        닫기
      </button>
    </div>
  );
};

export default Modal;
// PortalModal.jsx
import React from "react";
import ReactDOM from "react-dom";

const PortalModal = ({ children, onClose }) => {
  return ReactDOM.createPortal(
    <div
      style={{
        position: "fixed", // ✅ 항상 화면 기준으로 배치됨
        top: "50%",
        left: "50%",
        transform: "translate(-50%, -50%)",
        width: "250px",
        height: "120px",
        background: "rgba(0, 0, 0, 0.8)",
        color: "#fff",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        borderRadius: "8px",
        zIndex: 1000, // ✅ 항상 최상위 UI로 유지됨
      }}
    >
      {children}
      <button onClick={onClose} style={{ marginLeft: "10px" }}>
        닫기
      </button>
    </div>,
    document.body // ✅ body 아래에 직접 렌더링되므로 부모 영향 없음!
  );
};

export default PortalModal;

portal 적용 x
portal 적용

 

결론

React portal을 사용하면, overflow나 z-index에 영향을 받지 않는 UI 구현이 가능하다.

 

Portal 없이 position: fixed만 사용하면 뭐가 다를까?

근데 portal 없이 position:fixed만 사용해도 되지 않을까? 라는 의문이 들었다. 물론 여전히 부모 요소 안에 위치하기는 하겠지만, UI가 가려지지는 않을 것이라고 생각했다. 하지만, 부모의 z-index가 높으면 modal이 여전히 가려질 수 있다. 또한, fixed는 기본적으로 viewport를 기준으로 동작하지만, 부모가 transform, perspective, filter 속성을 가지면 부모 기준으로 움직이는 예외적인 경우가 발생할 수 있다고 한다.

'React' 카테고리의 다른 글

react-intersection-observer를 활용한 이미지 최적화  (0) 2025.03.04
React query 적용 구조적 패턴  (0) 2025.02.24
합성 컴포넌트 적용하기  (0) 2025.02.11
Error Boundary란 무엇일까?  (0) 2025.02.06
useCallback() 적용하기  (0) 2025.02.05