createPortal์„ ์‚ฌ์šฉํ•˜๋ฉด ์ผ๋ถ€ ์ž์‹์„ DOM์˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์œผ๋กœ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

๋ ˆํผ๋Ÿฐ์Šค

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 : JSX์˜ ์ผ๋ถ€(์˜ˆ๋ฅผ ๋“ค์–ด <div /> ๋˜๋Š” <SomeComponent />), Fragment(<>...</>), ๋ฌธ์ž์—ด์ด๋‚˜ ์ˆซ์ž ๋˜๋Š” ์ด๋“ค์˜ ๋ฐฐ์—ด๊ณผ ๊ฐ™์ด React๋กœ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • domNode : document.getElementById()๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์ผ๋ถ€ DOM ๋…ธ๋“œ. ๋…ธ๋“œ๊ฐ€ ๋ฏธ๋ฆฌ ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์—…๋ฐ์ดํŠธ ์ค‘์— ๋‹ค๋ฅธ DOM ๋…ธ๋“œ๋ฅผ ์ „๋‹ฌํ•˜๋ฉด portal ์ฝ˜ํ…์ธ ๊ฐ€ ๋‹ค์‹œ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

  • optional key: Portal์˜ key๋กœ ์‚ฌ์šฉํ•  ๊ณ ์œ ํ•œ ๋ฌธ์ž์—ด ๋˜๋Š” ์ˆซ์ž์ž…๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜ ๊ฐ’

createPortal์€ JSX์— ํฌํ•จํ•˜๊ฑฐ๋‚˜ React ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋Š” React ๋…ธ๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. React๊ฐ€ ๋ Œ๋”๋ง ์ถœ๋ ฅ์—์„œ ์ด๋ฅผ ๋ฐœ๊ฒฌํ•˜๋ฉด, ์ œ๊ณต๋œ children์„ ์ œ๊ณต๋œ domNode ์•ˆ์— ๋ฐฐ์น˜ํ•ฉ๋‹ˆ๋‹ค.

์ฃผ์˜ ์‚ฌํ•ญ

  • Portal์˜ ์ด๋ฒคํŠธ๋Š” DOM ํŠธ๋ฆฌ๊ฐ€ ์•„๋‹Œ React ํŠธ๋ฆฌ์— ๋”ฐ๋ผ ์ „ํŒŒ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, portal ๋‚ด๋ถ€๋ฅผ ํด๋ฆญํ–ˆ์„ ๋•Œ ํฌํ„ธ์ด <div onClick>์œผ๋กœ ๊ฐ์‹ธ์ ธ ์žˆ์œผ๋ฉด ํ•ด๋‹น onClick ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด portal ๋‚ด๋ถ€์—์„œ ์ด๋ฒคํŠธ ์ „ํŒŒ๋ฅผ ์ค‘์ง€ํ•˜๊ฑฐ๋‚˜ portal ์ž์ฒด๋ฅผ React ํŠธ๋ฆฌ์—์„œ ์œ„๋กœ ์ด๋™ํ•˜์„ธ์š”.

์‚ฌ์šฉ ๋ฐฉ๋ฒ•

DOM์˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์œผ๋กœ ๋ Œ๋”๋งํ•˜๊ธฐ

Portal์„ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ผ๋ถ€ ์ž์‹์„ DOM์˜ ๋‹ค๋ฅธ ์œ„์น˜๋กœ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์ปดํฌ๋„ŒํŠธ์˜ ์ผ๋ถ€๊ฐ€ ์–ด๋–ค ์ปจํ…Œ์ด๋„ˆ์— ์žˆ๋“  ๊ทธ ์ปจํ…Œ์ด๋„ˆ์—์„œ โ€œํƒˆ์ถœโ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ปดํฌ๋„ŒํŠธ๋Š” ๋ชจ๋‹ฌ ๋Œ€ํ™”์ƒ์ž๋‚˜ ํˆดํŒ์„ ํŽ˜์ด์ง€์˜ ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„ ์œ„์™€ ์™ธ๋ถ€์— ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Portal์„ ์ƒ์„ฑํ•˜๋ ค๋ฉด ์ผ๋ถ€ JSX์™€ ํ•จ๊ป˜ createPortal์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  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์ด ์žˆ๋Š” ๋ชจ๋‹ฌ ๋Œ€ํ™” ์ƒ์ž ๋ Œ๋”๋งํ•˜๊ธฐ

๋Œ€ํ™” ์ƒ์ž๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ overflow: hidden ๋˜๋Š” ๋Œ€ํ™” ์ƒ์ž์— ์˜ํ–ฅ์„ ์ฃผ๋Š” ๋‹ค๋ฅธ ์Šคํƒ€์ผ์ด ์žˆ๋Š” ์ปจํ…Œ์ด๋„ˆ ์•ˆ์— ์žˆ๋”๋ผ๋„ portal์„ ์‚ฌ์šฉํ•˜์—ฌ ํŽ˜์ด์ง€์˜ ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„ ์œ„์— ๋–  ์žˆ๋Š” ๋ชจ๋‹ฌ ๋Œ€ํ™” ์ƒ์ž๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ์˜ˆ์‹œ์—์„œ ๋‘ ์ปจํ…Œ์ด๋„ˆ์—๋Š” ๋ชจ๋‹ฌ ๋Œ€ํ™” ์ƒ์ž์— ์˜ํ–ฅ์„ ์ฃผ๋Š” ์Šคํƒ€์ผ์ด ์žˆ์ง€๋งŒ, portal์— ๋ Œ๋”๋ง๋œ ์Šคํƒ€์ผ์€ ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๋Š”๋ฐ, ๊ทธ ์ด์œ ๋Š” DOM์—์„œ ๋ชจ๋‹ฌ์ด ์ƒ์œ„ JSX ์š”์†Œ์— ํฌํ•จ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

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>
    </>
  );
}

์ฃผ์˜ํ•˜์„ธ์š”!

Portal์„ ์‚ฌ์šฉํ•  ๋•Œ ์•ฑ์˜ ์ ‘๊ทผ์„ฑ์ด ์ค€์ˆ˜๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์‚ฌ์šฉ์ž๊ฐ€ portal ์•ˆํŒŽ์œผ๋กœ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ดˆ์ ์„ ์ด๋™ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ‚ค๋ณด๋“œ ํฌ์ปค์Šค๋ฅผ ๊ด€๋ฆฌํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋‹ฌ์„ ๋งŒ๋“ค ๋•Œ๋Š” WAI-ARIA ๋ชจ๋‹ฌ ์ œ์ž‘ ๊ด€ํ–‰์„ ๋”ฐ๋ฅด์„ธ์š”. ์ปค๋ฎค๋‹ˆํ‹ฐ ํŒจํ‚ค์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ํŒจํ‚ค์ง€๊ฐ€ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ์ง€, ์ด ๊ฐ€์ด๋“œ๋ผ์ธ์„ ๋”ฐ๋ฅด๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.


React ์ปดํฌ๋„ŒํŠธ๋ฅผ React๊ฐ€ ์•„๋‹Œ ์„œ๋ฒ„ ๋งˆํฌ์—…์œผ๋กœ ๋ Œ๋”๋งํ•˜๊ธฐ

Portal์€ React ๋ฃจํŠธ๊ฐ€ React๋กœ ๋นŒ๋“œ๋˜์ง€ ์•Š์€ ์ •์  ๋˜๋Š” ์„œ๋ฒ„ ๋ Œ๋”๋ง ํŽ˜์ด์ง€์˜ ์ผ๋ถ€์ผ ๋•Œ ์œ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํŽ˜์ด์ง€๊ฐ€ Rails์™€ ๊ฐ™์€ ์„œ๋ฒ„ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ๋นŒ๋“œ๋œ ๊ฒฝ์šฐ ์‚ฌ์ด๋“œ๋ฐ”์™€ ๊ฐ™์€ ์ •์  ์˜์—ญ ๋‚ด์— ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์˜์—ญ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฐœ๋ณ„ React ๋ฃจํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋น„๊ตํ•˜์—ฌ, portal์„ ์‚ฌ์šฉํ•˜๋ฉด ์•ฑ์˜ ์ผ๋ถ€๊ฐ€ DOM์˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ๋ Œ๋”๋ง ๋˜๋”๋ผ๋„ ๊ณต์œ  ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ๋‹จ์ผ React ํŠธ๋ฆฌ๋กœ ์ทจ๊ธ‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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>;
}


React ์ปดํฌ๋„ŒํŠธ๋ฅผ React๊ฐ€ ์•„๋‹Œ DOM ๋…ธ๋“œ๋กœ ๋ Œ๋”๋งํ•˜๊ธฐ

Portal์„ ์‚ฌ์šฉํ•ด React ์™ธ๋ถ€์—์„œ ๊ด€๋ฆฌ๋˜๋Š” DOM ๋…ธ๋“œ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, React๊ฐ€ ์•„๋‹Œ ๋งต ์œ„์ ฏ๊ณผ ํ†ตํ•ฉํ•˜๊ณ  ํŒ์—… ์•ˆ์— React ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  ์‹ถ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด ๋ด…์‹œ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด ๋ Œ๋”๋งํ•  DOM ๋…ธ๋“œ๋ฅผ ์ €์žฅํ•  popupContainer ์ƒํƒœ ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•˜์„ธ์š”.

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);
}
}, []);

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด createPortal์„ ์‚ฌ์šฉํ•˜์—ฌ React ์ฝ˜ํ…์ธ ๊ฐ€ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ด์ง€๋ฉด popupContainer๋กœ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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

๋‹ค์Œ์€ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ „์ฒด ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

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>
  );
}