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