createPortal

김동현·2026년 3월 17일

createPortal

소개

createPortal을 사용하면 일부 children을 DOM의 다른 부분에 렌더링할 수 있어요.

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

Portal은 React 컴포넌트를 DOM 트리의 다른 위치로 "순간이동"시키는 기능이에요. 모달, 툴팁, 드롭다운 같은 UI를 만들 때 특히 유용해요!


목차


레퍼런스

createPortal(children, domNode, key?)

portal을 만들려면, createPortal을 호출하면서 JSX와 렌더링되어야 할 DOM 노드를 전달하세요:

import { createPortal } from 'react-dom';

// ...

<div>
  <p>This child is placed in the parent div.</p>
  {createPortal(
    <p>This child is placed in the document body.</p>,
    document.body
  )}
</div>

아래에서 더 많은 예시를 확인해보세요.

portal은 DOM 노드의 물리적 배치만 변경해요. 다른 모든 면에서, portal로 렌더링하는 JSX는 그것을 렌더링하는 React 컴포넌트의 자식 노드처럼 동작해요. 예를 들어, 자식은 부모 트리가 제공하는 context에 접근할 수 있고, 이벤트는 React 트리에 따라 자식에서 부모로 버블링돼요.

매개변수

  • children: React로 렌더링할 수 있는 모든 것이에요. JSX 조각(예: <div /> 또는 <SomeComponent />), Fragment(<>...</>), 문자열이나 숫자, 또는 이들의 배열이 될 수 있어요.

  • domNode: 어떤 DOM 노드예요. document.getElementById()가 반환하는 것 같은 거요. 노드가 이미 존재해야 해요. 업데이트 중에 다른 DOM 노드를 전달하면 portal 콘텐츠가 다시 생성될 거예요.

  • 선택적 key: portal의 key로 사용될 고유한 문자열이나 숫자예요.

반환값

createPortal은 JSX에 포함하거나 React 컴포넌트에서 반환할 수 있는 React 노드를 반환해요. React가 렌더링 출력에서 이것을 만나면, 제공된 children을 제공된 domNode 안에 배치할 거예요.

주의사항

  • portal의 이벤트는 DOM 트리가 아니라 React 트리에 따라 전파돼요. 예를 들어, portal 내부를 클릭하고 portal이 <div onClick>으로 감싸져 있으면, 그 onClick 핸들러가 실행될 거예요. 이게 문제를 일으킨다면, portal 내부에서 이벤트 전파를 중지하거나, portal 자체를 React 트리에서 위로 이동시키세요.

사용법

DOM의 다른 부분에 렌더링하기

Portal을 사용하면 컴포넌트가 일부 children을 DOM의 다른 위치에 렌더링할 수 있어요. 이렇게 하면 컴포넌트의 일부가 어떤 컨테이너 안에 있든 "탈출"할 수 있어요. 예를 들어, 컴포넌트가 페이지의 나머지 부분 위와 바깥에 나타나는 모달 다이얼로그나 툴팁을 표시할 수 있어요.

portal을 만들려면, createPortal의 결과를 JSX와 그것이 가야 할 DOM 노드와 함께 렌더링하세요:

import { createPortal } from 'react-dom';

function MyComponent() {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>This child is placed in the parent div.</p>
      {createPortal(
        <p>This child is placed in the document body.</p>,
        document.body
      )}
    </div>
  );
}

React는 전달한 JSX의 DOM 노드를 제공한 DOM 노드 안에 넣을 거예요.

portal이 없으면, 두 번째 <p>는 부모 <div> 안에 배치될 거예요. 하지만 portal이 그것을 document.body로 "순간이동"시켰어요:

import { createPortal } from 'react-dom';

export default function MyComponent() {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>This child is placed in the parent div.</p>
      {createPortal(
        <p>This child is placed in the document body.</p>,
        document.body
      )}
    </div>
  );
}

두 번째 단락이 시각적으로 테두리가 있는 부모 <div> 바깥에 나타나는 걸 주목하세요. 개발자 도구로 DOM 구조를 검사하면, 두 번째 <p><body>에 직접 배치된 걸 볼 수 있어요:

<body>
  <div id="root">
    ...
      <div style="border: 2px solid black">
        <p>This child is placed inside the parent div.</p>
      </div>
    ...
  </div>
  <p>This child is placed in the document body.</p>
</body>

portal은 DOM 노드의 물리적 배치만 변경해요. 다른 모든 면에서, portal로 렌더링하는 JSX는 그것을 렌더링하는 React 컴포넌트의 자식 노드처럼 동작해요. 예를 들어, 자식은 부모 트리가 제공하는 context에 접근할 수 있고, 이벤트는 여전히 React 트리에 따라 자식에서 부모로 버블링돼요.


portal로 모달 다이얼로그 렌더링하기

portal을 사용해서 페이지의 나머지 부분 위에 떠 있는 모달 다이얼로그를 만들 수 있어요. 다이얼로그를 호출하는 컴포넌트가 overflow: hidden이나 다이얼로그를 방해하는 다른 스타일이 있는 컨테이너 안에 있어도요.

이 예시에서, 두 컨테이너는 모달 다이얼로그를 방해하는 스타일이 있지만, portal로 렌더링된 것은 영향을 받지 않아요. DOM에서 모달이 부모 JSX 요소 안에 포함되어 있지 않기 때문이에요.

// src/App.js
import NoPortalExample from './NoPortalExample';
import PortalExample from './PortalExample';

export default function App() {
  return (
    <>
      <div className="clipping-container">
        <NoPortalExample  />
      </div>
      <div className="clipping-container">
        <PortalExample />
      </div>
    </>
  );
}
// src/NoPortalExample.js
import { useState } from 'react';
import ModalContent from './ModalContent.js';

export default function NoPortalExample() {
  const [showModal, setShowModal] = useState(false);
  return (
    <>
      <button onClick={() => setShowModal(true)}>
        Show modal without a portal
      </button>
      {showModal && (
        <ModalContent onClose={() => setShowModal(false)} />
      )}
    </>
  );
}
// src/PortalExample.js
import { useState } from 'react';
import { createPortal } from 'react-dom';
import ModalContent from './ModalContent.js';

export default function PortalExample() {
  const [showModal, setShowModal] = useState(false);
  return (
    <>
      <button onClick={() => setShowModal(true)}>
        Show modal using a portal
      </button>
      {showModal && createPortal(
        <ModalContent onClose={() => setShowModal(false)} />,
        document.body
      )}
    </>
  );
}
// src/ModalContent.js
export default function ModalContent({ onClose }) {
  return (
    <div className="modal">
      <div>I'm a modal dialog</div>
      <button onClick={onClose}>Close</button>
    </div>
  );
}
/* src/styles.css */
.clipping-container {
  position: relative;
  border: 1px solid #aaa;
  margin-bottom: 12px;
  padding: 12px;
  width: 250px;
  height: 80px;
  overflow: hidden;
}

.modal {
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  box-shadow: rgba(100, 100, 111, 0.3) 0px 7px 29px 0px;
  background-color: white;
  border: 2px solid rgb(240, 240, 240);
  border-radius: 12px;
  position:  absolute;
  width: 250px;
  top: 70px;
  left: calc(50% - 125px);
  bottom: 70px;
}

위 예시에서:

  • 첫 번째 버튼은 portal 없이 모달을 보여줘요. overflow: hidden이 있는 컨테이너 안에 있어서 모달이 잘려 보여요.
  • 두 번째 버튼은 portal을 사용해서 모달을 document.body에 렌더링해요. 그래서 컨테이너의 스타일 제약을 벗어날 수 있어요!

⚠️ 주의

portal을 사용할 때 앱이 접근 가능한지 확인하는 것이 중요해요. 예를 들어, 사용자가 자연스러운 방식으로 portal 안팎으로 포커스를 이동할 수 있도록 키보드 포커스를 관리해야 할 수 있어요.

모달을 만들 때 WAI-ARIA 모달 작성 관행을 따르세요. 커뮤니티 패키지를 사용한다면, 접근 가능하고 이 가이드라인을 따르는지 확인하세요.


React 컴포넌트를 비React 서버 마크업에 렌더링하기

Portal은 React root가 React로 빌드되지 않은 정적 또는 서버 렌더링 페이지의 일부일 때 유용할 수 있어요. 예를 들어, 페이지가 Rails 같은 서버 프레임워크로 빌드되었다면, 사이드바 같은 정적 영역 내에 인터랙티브한 영역을 만들 수 있어요. 여러 개의 별도 React root를 갖는 것과 비교하면, portal을 사용하면 부분들이 DOM의 다른 부분에 렌더링되더라도 앱을 공유 state를 가진 단일 React 트리로 취급할 수 있어요.

<!-- public/index.html -->
<!DOCTYPE html>
<html>
  <head><title>My app</title></head>
  <body>
    <h1>Welcome to my hybrid app</h1>
    <div class="parent">
      <div class="sidebar">
        This is server non-React markup
        <div id="sidebar-content"></div>
      </div>
      <div id="root"></div>
    </div>
  </body>
</html>
// src/index.js
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.js';
import './styles.css';

const root = createRoot(document.getElementById('root'));
root.render(
  <StrictMode>
    <App />
  </StrictMode>
);
// src/App.js
import { createPortal } from 'react-dom';

const sidebarContentEl = document.getElementById('sidebar-content');

export default function App() {
  return (
    <>
      <MainContent />
      {createPortal(
        <SidebarContent />,
        sidebarContentEl
      )}
    </>
  );
}

function MainContent() {
  return <p>This part is rendered by React</p>;
}

function SidebarContent() {
  return <p>This part is also rendered by React!</p>;
}
.parent {
  display: flex;
  flex-direction: row;
}

#root {
  margin-top: 12px;
}

.sidebar {
  padding:  12px;
  background-color: #eee;
  width: 200px;
  height: 200px;
  margin-right: 12px;
}

#sidebar-content {
  margin-top: 18px;
  display: block;
  background-color: white;
}

p {
  margin: 0;
}

위 예시에서:

  • HTML에 React root (#root)와 별도의 사이드바 영역 (#sidebar-content)이 있어요
  • App 컴포넌트는 메인 콘텐츠를 #root에 렌더링하고, portal을 사용해서 사이드바 콘텐츠를 #sidebar-content에 렌더링해요
  • 두 부분 모두 같은 React 트리에 속하므로 state와 context를 공유할 수 있어요!

React 컴포넌트를 비React DOM 노드에 렌더링하기

portal을 사용해서 React 외부에서 관리되는 DOM 노드의 콘텐츠를 관리할 수도 있어요. 예를 들어, 비React 지도 위젯과 통합하고 팝업 내부에 React 콘텐츠를 렌더링하고 싶다고 가정해보세요. 이렇게 하려면, 렌더링할 DOM 노드를 저장할 popupContainer state 변수를 선언하세요:

const [popupContainer, setPopupContainer] = useState(null);

서드파티 위젯을 만들 때, 위젯이 반환한 DOM 노드를 저장해서 그 안에 렌더링할 수 있어요:

useEffect(() => {
  if (mapRef.current === null) {
    const map = createMapWidget(containerRef.current);
    mapRef.current = map;
    const popupDiv = addPopupToMapWidget(map);
    setPopupContainer(popupDiv);
  }
}, []);

이렇게 하면 popupContainer가 사용 가능해지면 createPortal을 사용해서 React 콘텐츠를 렌더링할 수 있어요:

return (
  <div style={{ width: 250, height: 250 }} ref={containerRef}>
    {popupContainer !== null && createPortal(
      <p>Hello from React!</p>,
      popupContainer
    )}
  </div>
);

다음은 실험해볼 수 있는 완전한 예시예요:

// package.json
{
  "dependencies": {
    "leaflet": "1.9.1",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "remarkable": "2.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }
}
// src/App.js
import { useRef, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createMapWidget, addPopupToMapWidget } from './map-widget.js';

export default function Map() {
  const containerRef = useRef(null);
  const mapRef = useRef(null);
  const [popupContainer, setPopupContainer] = useState(null);

  useEffect(() => {
    if (mapRef.current === null) {
      const map = createMapWidget(containerRef.current);
      mapRef.current = map;
      const popupDiv = addPopupToMapWidget(map);
      setPopupContainer(popupDiv);
    }
  }, []);

  return (
    <div style={{ width: 250, height: 250 }} ref={containerRef}>
      {popupContainer !== null && createPortal(
        <p>Hello from React!</p>,
        popupContainer
      )}
    </div>
  );
}
// src/map-widget.js
import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

export function createMapWidget(containerDomNode) {
  const map = L.map(containerDomNode);
  map.setView([0, 0], 0);
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap'
  }).addTo(map);
  return map;
}

export function addPopupToMapWidget(map) {
  const popupDiv = document.createElement('div');
  L.popup()
    .setLatLng([0, 0])
    .setContent(popupDiv)
    .openOn(map);
  return popupDiv;
}
button { margin: 5px; }

위 예시에서:

  • Leaflet 지도 라이브러리를 사용해서 지도를 만들어요
  • 지도에 팝업을 추가하고, 그 팝업의 DOM 노드를 popupContainer에 저장해요
  • Portal을 사용해서 React 콘텐츠를 그 팝업 안에 렌더링해요
  • React와 비React 라이브러리를 함께 사용하는 완벽한 예시예요!

사이트맵

모든 문서 페이지 개요

profile
프론트에_가까운_풀스택_개발자

0개의 댓글