Skip to main content

Command Palette

Search for a command to run...

Modal ๋งŒ๋“ค๊ธฐ

๋ฆฌ์•กํŠธ ํฌํƒˆ๊ณผ ์ปดํŒŒ์šด๋“œ ์ปดํฌ๋„ŒํŠธ ์ ์šฉํ•ด์„œ Modal ๋งŒ๋“ค์–ด๋ณด๊ธฐ

Published
โ€ข10 min readโ€ขView as Markdown
Modal ๋งŒ๋“ค๊ธฐ
L

์ง๋ฉดํ•˜๋Š” ๋ชจ๋“  ๋ฌธ์ œ์— ์œ ์น˜ํ•œ ๊ฒƒ์€ ์—†์œผ๋ฉฐ, ์˜๋ฏธ ์—†๋Š” ์‚ฝ์งˆ ๋˜ํ•œ ์—†๋‹ค๊ณ  ๋ฏฟ์Šต๋‹ˆ๋‹ค.

โœ‹๐Ÿป ๋“ค์–ด๊ฐ€๋ฉฐ

์ด๋ฒˆ์— ์žฌ๋‚จ๋‹˜๊ณผ ํ•จ๊ป˜ํ•˜๋Š” UI ์š”์†Œ ๋งŒ๋“ค๊ธฐ ์Šคํ„ฐ๋””์— ํ•ฉ๋ฅ˜ํ–ˆ๋‹ค. ํ‰์†Œ ๊ด€์‹ฌ์ด ๋งŽ์•˜๋˜ ๋ถ€๋ถ„์ด๋‹ค๋ณด๋‹ˆ ๋ญ”๊ฐ€ ๋งˆ์Œ์ด ์„ค๋ ˆ๊ณ .. ๊ทธ๋Ÿฐ๋ฐ ๋˜ ๋งˆ์Œ๋งŒ ์„ค๋  ์ˆ˜๋Š” ์—†์œผ๋‹ˆ ์…€ํ”„ ์Šคํ„ฐ๋””๋ฅผ ์œ„ํ•ด ๋ ˆํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด๋ดค๋‹ค. ์Šคํ„ฐ๋””์—์„œ ๋‹ค๋ฃฐ ๋‚ด์šฉ๋“ค์ด ๋‚˜๋ฆ„ ์ˆœ์„œ๊ฐ€ ์žˆ์–ด์„œ ํ˜ผ์ž์„œ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์–ด๋ณด๊ณ  ๋‹ค๋ฅธ ๋ถ„๋“ค์ด ๋งŒ๋“  ๊ฒƒ๋“ค๋„ ๋น„๊ตํ•ด๊ฐ€๋ฉด์„œ ๋งŽ์ด ๋ฐฐ์›Œ๋ด์•ผ๊ฒ ๋‹ค.

์Šคํ„ฐ๋””์—์„œ๋Š” ์•„์ง ๋ชจ๋‹ฌ์„ ๋‹ค๋ฃจ๋ ค๋ฉด ๋ฉ€์—ˆ์ง€๋งŒ, ์ด๋ฒˆ์— ReactDOM.createPortal ์„ ๊ณต๋ถ€๋ฅผ ํ–ˆ์—ˆ๋Š”๋ฐ ๋ง‰์ƒ ์จ๋ณธ ์ ์€ ์—†์—ˆ์–ด์„œ ์ด๊ฒƒ๊ณผ ํ•จ๊ป˜ ์ปดํŒŒ์šด๋“œ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด๋„ ์ž˜ ์ ์šฉํ•ด๋ณด๊ณ  ์‹ถ์–ด์„œ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ณจ๋ผ์„œ ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋ดค๋‹ค.


๐ŸŒ ReactDOM.createPortal()

๋ฆฌ์•กํŠธ ๊ณต์‹๋ฌธ์„œ์—์„œ๋Š” ํฌํƒˆ์„ ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค๋ช…ํ•˜๊ณ  ์žˆ๋‹ค.

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

Portal์€ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์˜ DOM ๊ณ„์ธต ๊ตฌ์กฐ ๋ฐ”๊นฅ์— ์žˆ๋Š” DOM ๋…ธ๋“œ๋กœ ์ž์‹์„ ๋ Œ๋”๋งํ•˜๋Š” ์ตœ๊ณ ์˜ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์Œ ๊ทผ๋ฐ ์ด๊ฑธ ์™œ ์“ฐ๋Š”๋ฐ ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ ๋ฒจ๋กœ๊ทธ์— ๋ชจ๋‹ฌ ๊ฒ€์ƒ‰ํ•˜๋ฉด ๊ผญ ํ•จ๊ป˜ ๋‚˜์˜ค๋Š” ์นœ๊ตฌ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์ด ํฌํƒˆ์ธ๋ฐ ๊ทธ๋ž˜์„œ ์™œ ์“ฐ๋Š”๋ฐ?ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹๋ฅผ ์•Œ์•„๋ดค๋‹ค.

์™œ ์“ฐ๋ƒ๊ณ ?

ํ•˜์ง€๋งŒ ๋ญ ์ด๋Ÿฐ๊ฒŒ ์žˆ๋‹ค๊ณ  ํ•˜๊ณ , ์ด๊ฑธ ์“ฐ์ง€ ์•Š๋”๋ผ๋„ css ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํžˆ ๋ชจ๋‹ฌ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‚ด๊ฐ€ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ•˜๋ƒ์— ๋”ฐ๋ผ์„œ ๋‹น์—ฐํžˆ! ํ•˜์ง€๋งŒ React ๋ผ๋Š” ๋…€์„์€ ์ƒ๊ฒจ๋จน์€๊ฒŒ ๋ญ”๊ฐ€ ๋ถˆํŽธํ•˜๋‹ค.

React ๋Š” ํŠธ๋ฆฌ ๊ตฌ์กฐ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋Š”๋ฐ, ๊ทธ๋Ÿฌ๋‹ค๋ณด๋‹ˆ ์›ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋„ ์Šคํƒ€์ผ์ด ์ƒ์†๋˜์–ด ๊ณค๋ž€ํ•˜๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์ƒ๊ธด๋‹ค. (์ด๊ฒŒ ๊ฐ€์žฅ ํฐ ์ด์œ ๊ฐ€ ์•„๋‹๊นŒ..? ๐Ÿคฆ๐Ÿปโ€โ™€๏ธ ๊ณค๋ž€ํ–ˆ๋˜๊ฒŒ ํ•œ ๋‘๋ฒˆ์ด ์•„๋‹ˆ์—ˆ๋‹ค.) ํ•˜์ง€๋งŒ Portal ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋…๋ฆฝ์ ์œผ๋กœ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋งŽ์ด ์‚ฌ์šฉ๋œ๋‹ค.

์–ด๋–ป๊ฒŒ ์“ฐ๋Š”๋ฐ!

๋ฆฌ์•กํŠธ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ƒ๊ธด html ํŒŒ์ผ์ด ํ•˜๋‚˜ ์ƒ์„ฑ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  body ํƒœ๊ทธ ๋‚ด๋ถ€์— "root" ๋ผ๋Š” ์•„์ด๋””๋ฅผ ๊ฐ€์ง„ div ๋‚ด๋ถ€์— ๋ Œ๋”๋ง๋  ์•„์ด๋“ค์ด ๋น„์ง‘๊ณ  ๋“ค์–ด๊ฐ€๊ฒŒ๋œ๋‹ค. ์ฆ‰ ๋ฆฌ์•กํŠธ ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€์—์„œ ๋ Œ๋”๋ง๋  ์•„์ด๋“ค์€ ๋ชจ๋‘ "root" ์˜ ํ•˜์œ„์— ์œ„์น˜ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๋‹ˆ ์›ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์˜ ์Šคํƒ€์ผ ์ƒ์†์ด ์ƒ๊ธธ ์ˆ˜๊ฐ€ ์žˆ๊ฒ ์ง€.

<!doctype html>
<html lang="en">
  <head>
    //...
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

ํฌํƒˆ์„ ์—ด์–ด์ค„ createPortal

ReactDOM.createPortal(children, container);
  • children : ๋ Œ๋”๋งํ•  ์ž์‹ ์ปดํฌ๋„ŒํŠธ

  • container : DOM element

์ด์ œ "root" ๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ๊ณณ์— ํฌํƒˆ์„ ์—ด์–ด์ค„ํ…๋ฐ ๊ทธ ๋•Œ ์‚ฌ์šฉํ•˜๊ฒŒ ๋  ๋…€์„์ด ๋ฐ”๋กœ ReactDOM.createPortal ์ด๋‹ค. ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” ์–ด๋–ค ๊ฒƒ๋“ค์„ ๋ Œ๋”๋ง ์‹œํ‚ฌ๊ฑด์ง€ ์ „๋‹ฌ์„ ํ•ด์ฃผ๋ฉด๋˜๊ณ , ๋‘๋ฒˆ์งธ ์ธ์ž๋กœ๋Š” "root" ์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•ด์ค„ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„ฃ์–ด์ค€๋‹ค.

์ ์šฉํ•ด๋ณผ๊นŒ?

๋จผ์ € "root" ์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•ด์ค„ ์ปจํ…Œ์ด๋„ˆ๋ฅผ html ํŒŒ์ผ์— ์ž‘์„ฑํ•œ๋‹ค. ๋‚˜๋Š” ๋ชจ๋‹ฌ์„ ๋งŒ๋“ค์–ด์ค„ ๊ฒƒ์ด๋‹ˆ "root-modal" ์ด๋ผ๋Š” ์•„์ด๋””๋กœ div ํƒœ๊ทธ๋ฅผ ๋„ฃ์–ด์คฌ๋‹ค. ์ด์ œ ๋‚ด๊ฐ€ ํฌํƒˆ์„ ์—ด์–ด์ฃผ๋ฉด "root-modal" ์˜ ํ•˜์œ„์— ๋ Œ๋”๋ง์ด ์ผ์–ด๋‚˜๋ฉฐ "root" ์™€๋Š” ๋ถ„๋ฆฌ๊ฐ€ ๋˜์–ด ํ™”๋ฉด์— ํ‘œ์‹œ๋  ๊ฒƒ์ด๋‹ค.

<!doctype html>
<html lang="en">
  <head>
    <!-- ... -->
  </head>
  <body>
    <!-- ๐Ÿ‘‰๐Ÿป add div tag (with id "root-modal") -->
    <div id="root-modal"></div>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

์ด์ œ ํฌํƒˆ์„ ์—ด์–ด์ค„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์ž. ๋ฐฉ๋ฒ•์€ ์•„์ฃผ ๊ฐ„๋‹จํ•˜๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑ๋งŒ ํ•ด์ฃผ๋ฉด ๋!

function ModalPortal(props) {
    return ReactDOM.createPortal(props.children, document.getElementById("rood-modal");
}

์ด๋ก ์„ ๋ฐฐ์› ์œผ๋‹ˆ ์ด์ œ ์ง์ ‘ ์ด๊ฑธ ์ ์šฉํ•ด์„œ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•  ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ Modal ๋งŒ๋“ค์–ด๋ณด๊ธฐ

์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์€ ๊ต‰์žฅํžˆ ๋‹ค์–‘ํ•˜๋‹ค.(๊ทธ๋ž˜์„œ ๋Š˜ ๊ณ ํ†ต์ด๋‹ค.) ์ด๋ฒˆ์— ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๋ฉด์„œ๋„ โ€˜์ด๊ฒŒ ๋งž๋Š” ๋ฐฉ๋ฒ•์ผ๊นŒ?โ€™๋ฅผ ๋งŽ์ด ๊ณ ๋ฏผํ–ˆ์—ˆ๋Š”๋ฐ ๊ทธ ๋ชจ๋“  ๊ณ ๋ฏผ์˜ ๊ฒฐ๊ณผ๋Š” ๋ฐฉ๋ฒ•์€ ๋งŽ์ง€๋Š” ์ •๋‹ต์€ ์—†๋‹ค๋Š” ๊ฒƒ์ด์—ˆ๋‹ค. ์›ํ”ผ์Šค๋ฅผ ์ฐพ๋Š” ๋А๋‚Œ์ด์—ˆ์„๊นŒ.

๊ทธ๋ž˜์„œ ์ด๋ฒˆ์—๋Š” Compound Component ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด์„œ Modal ์„ ๋งŒ๋“ค์–ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค. ๊ธฐ์กด์— Modal ์„ ๋งŒ๋“ค ๋•Œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๋งŒ๋“ค์–ด์„œ ๋‚ด๋ถ€์— ์—ฌ๋Ÿฌ ์กฐ๊ฑด๋“ค์„ ๊ฑธ์–ด์ค„ ๋•Œ๊ฐ€ ๋งŽ์•˜๋‹ค.

const Modal = ({title, content, onClose}) => {

    return (
        <Layout>
            { title && <Title>{title}</Title> }
            { content && <Content>{content}</Content> }
            <Footer>
                <Button onClick={onClose}>๋‹ซ๊ธฐ</Button>
            </Footer>
        </Layout>
    )
}

๋Œ€์ถฉ ์œ„์™€ ๊ฐ™์€ ๋ชจ์Šต์ด ๋ฒ ์ด์Šค๋กœ ๋‚˜์˜ค๊ณ , ํ•„์š”์— ๋”ฐ๋ผ์„œ ์—ฌ๋Ÿฌ๊ฐ€์ง€ props ๊ฐ€ ๋งŒ๋“ค์–ด์ง€๊ณ  ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€๊ฐ€ ๋ฌด๊ฑฐ์›Œ์งˆ ๊ฒƒ์ด๋‹ค. ์œ„์˜ ์ผ€์ด์Šค๋ผ๋ฉด ๋ฒ„ํŠผ์ด ํ•˜๋‚˜๋งŒ ์žˆ๋Š”๋ฐ, ๊ธฐํš์ด ๋‹ค์–‘ํ•ด์ง€๋ฉด ๋ฒ„ํŠผ์ด ๋‘ ๊ฐœ๋กœ ๋Š˜์–ด๋‚  ์ˆ˜๋„ ์žˆ๊ณ โ€ฆ ๊ทธ๋Ÿฐ ๊ฒƒ๋“ค์„ ๊ณ ๋ คํ•˜๋‹ค๋ณด๋ฉด ์–ด๋А์ƒŒ๊ฐ€ ๋‚˜๋„ ๋ชจ๋ฅด๊ฒŒ ๋น„๋Œ€ํ•ด์ง„ props์— ํ˜„๊ธฐ์ฆ์ด ๋‚˜๊ฒŒ ๋  ๊ฒƒ์ด๋‹ค.

์Œ ์œ„์™€ ๊ฐ™์€ ์ผ€์ด์Šค์—์„œ ๋น„๋Œ€ํ•ด์งˆ ์ˆ˜ ์žˆ๋Š” props์„ ์ •๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์ค€๋‹ค๊ฑฐ๋‚˜ ์ผ€์ด์Šค๋ฅผ ํƒ€์ž…์œผ๋กœ ๋ฐ›์•„์„œ ํƒ€์ž…์— ๋”ฐ๋ฅธ ๋ Œ๋”๋ง์„ ํ•ด์ฃผ๋Š” ๋“ฑ์˜ ์‹œ๋„๋ฅผ ํ•ด์ค„ ์ˆ˜ ์žˆ๊ฒ ๋‹ค. ์•„๋‹ˆ๋ฉด ์กฐ๊ธˆ ๋” ์ž์œ ๋„๋ฅผ ์ฃผ๊ธฐ ์œ„ํ•ด์„œ render props ๋ฅผ ๋ฐ›๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๊ฒ ๊ณ . ์˜ˆ๋ฅผ ๋“ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ž‘์„ฑํ•ด์ค„ ์ˆ˜๋„ ์žˆ์„ ๊ฒƒ.

const Page = () => {
    const [open, setOpen] = useState(false);

    return (
        <Layout>
            <Modal
                open={open}
                render={() => (<๋ชจ๋‹ฌ๋‚ด๋ถ€/>)}
                />
        </Layout>
    );
};

์œ„ ๋ฐฉ๋ฒ•์€ ์–ด๋А์ •๋„ ๋งŒ๋“ค์–ด์ง„ ํฐ ํ‹€์— ๋‚ด๋ถ€๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์ปค์Šคํ…€ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํŽธํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค. ๋‹ค๋งŒ ๋‚ด๋ถ€์— ๋‹ค์‹œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„ ์–ธํ•ด์„œ ๋ถˆ๋Ÿฌ์™€์ฃผ๊ฑฐ๋‚˜ ๋งค๋ฒˆ ์ƒˆ๋กญ๊ฒŒ ์ž‘์„ฑํ•˜๋Š” ๋ถˆํŽธํ•จ์ด ์žˆ๊ฒ ๋‹ค. ์•„๋‹ˆ๋ฉด ๊ธฐ๋ณธ ํผ์ด ์žˆ๋Š”๋ฐ ๊ทธ ํผ ์ž์ฒด๋ฅผ ์ƒˆ๋กญ๊ฒŒ ์ปค์Šคํ…€ํ•  ๋•Œ๋งŒ ์ € props ๋ฅผ ์ด์šฉํ•œ๋‹ค๊ฑฐ๋‚˜? (Antd์—์„œ ์ด๋Ÿฐ ๋ฐฉ๋ฒ•์„ ๋งŽ์ด ์“ฐ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.)

๐Ÿงฉ Compound Component Pattern

Compound Component ํŒจํ„ด์€ ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ์•”์‹œ์  ์ƒํƒœ ๊ณต์œ  ์ปดํฌ๋„ŒํŠธ API ์ง‘ํ•ฉ์„ ์ œ๊ณตํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค. ์ด ์นœ๊ตฌ๋Š” ์œ ์—ฐ์„ฑ์ด ์ข‹๊ณ  ํ™•์žฅ์„ฑ์ด ๋›ฐ์–ด๋‚œ๋ฐ, ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ โ€œAPI์˜ ๋ณต์žก์„ฑ ๊ฐ์†Œโ€๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค.

์ด ํŒจํ„ด์„ ์ ์šฉํ•˜๋ฉด ํ•˜๋‚˜์˜ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์— props ๋ฅผ ์ด์šฉํ•ด ํ•˜์œ„๋กœ ๋‚ด๋ ค๋ณด๋‚ด๋Š” ๋Œ€์‹ , ๊ฐ๊ฐ์˜ props๋Š” ๊ฐ€์žฅ ์ ํ•ฉํ•œ SubComponent ์— ์—ฐ๊ฒฐ๋˜๋„๋ก ๊ตฌ์„ฑ๋œ๋‹ค. ์ฆ‰ ๊ฐ๊ฐ์˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ณธ์ธ์ด ์ •๋ง ํ•„์š”๋กœํ•˜๋Š” ๊ฒƒ๋“ค๋งŒ ๋ฐ›์œผ๋ฉด ๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

์ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๊ฐ€์žฅ ์ข‹์•˜๋˜ ์ ์€ ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ๋˜๋Š” state์™€ handler๋ฅผ context ๋กœ ๊ด€๋ฆฌํ•˜๊ฒŒ ๋˜๋ฉด์„œ props ๋ฅผ ์ค„์ด๋ฉด์„œ๋„ ๊ฐ๊ฐ์˜ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋Š” ์ ์ด ์ข‹์•˜๋‹ค.

๋‹ค๋งŒ ๋‹จ์ ์œผ๋กœ๋Š” JSX ํ–‰ ์ˆ˜๊ฐ€ ๊ต‰์žฅํžˆ ์ฆ๊ฐ€ํ•˜๋Š” ํŽธ์ด๋ฉฐ ์–ด๋А ์ •๋„๋กœ ์œ ์—ฐํ•˜๊ฒŒ ๊ฐ€์ง€๊ณ  ๊ฐ€์•ผํ•˜๋Š”๊ฐ€์— ๋Œ€ํ•œ ์ค‘์‹ฌ์„ ์žก๋Š”๊ฒŒ ์‰ฝ์ง€๊ฐ€ ์•Š์€ ๊ฒƒ ๊ฐ™๋‹ค.

์ด๋ก ์ ์ธ๊ฑด ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ดค์œผ๋‹ˆ ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋ณด์ž. ์กฐ๊ธˆ ๋” ์ž์„ธํ•œ ๋‚ด์šฉ์ด๋‚˜ ๋‹ค์–‘ํ•œ ๋ฆฌ์•กํŠธ์˜ ๋””์ž์ธ ํŒจํ„ด๊ณผ ๊ด€๋ จํ•ด์„œ๋Š” ์•„๋ž˜ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•ด๋ณด์ž.

๐Ÿ—“๏ธ ์ปดํฌ๋„ŒํŠธ ๊ตฌ์กฐ ์žก์•„๋ณด๊ธฐ

๋Œ€๋ถ€๋ถ„์˜ ๋ชจ๋‹ฌ์€ ์œ„์˜ ๋ชจ์Šต์œผ๋กœ ์ƒ๊ฒผ๋‹ค. ๋งŒ๋“ค์–ด์•ผํ•˜๋Š” ๊ฒƒ๋“ค์„ ๋‚˜์—ดํ•ด๋ณผ๊นŒ?

  1. ๋ชจ๋‹ฌ ํƒ€์ดํ‹€

  2. ๋ชจ๋‹ฌ ๋‚ด์šฉ

  3. ๋ชจ๋‹ฌ ํ•˜๋‹จ๋ถ€

  4. ๋ชจ๋‹ฌ ๋ ˆ์ด์•„์›ƒ / ๋ชจ๋‹ฌ ์™ธ๋ถ€ ์˜์—ญ - ํด๋ฆญํ•˜๋ฉด ๋ชจ๋‹ฌ์ด ๋‹ซํ˜€์•ผํ•จ

  5. ๋ชจ๋‹ฌ ๋ฒ„ํŠผ

์—ฌ๊ธฐ์— ํฌํƒˆ์„ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ํ–ˆ์œผ๋‹ˆ ํฌํƒˆ๋„ ์ถ”๊ฐ€ํ•ด์ค˜์•ผํ•˜๊ณ  ๐Ÿค” ๊ฐ๊ฐ์˜ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•ด์•ผํ•˜๋‹ˆ Context ๋„ ๋งŒ๋“ค์–ด์ค˜์•ผํ•œ๋‹ค.

  1. React Portal

  2. Context Provider

๊ณ„ํš์ด ์„ธ์›Œ์กŒ์œผ๋‹ˆ ํ•˜๋‚˜์”ฉ ๋‹จ๊ณ„๋ณ„๋กœ ์ž‘์„ฑํ•ด๋ณด์ž. ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ข… ๊ตฌ์กฐ๋Š” ์ด๋Ÿฐ์‹์œผ๋กœ ๋งŒ๋“ค์–ด์งˆ ๊ฒƒ์ด๋‹ค.

<์ปจํ…์ŠคํŠธ>
    <๋ฒ„ํŠผ/>
    <ํฌํƒˆ>
        <๋ ˆ์ด์•„์›ƒ>
            ...๋‚ด๋ถ€ ์›ํ•˜๋Š” ๊ฒƒ๋“ค ์กฐํ•ฉ
        </๋ ˆ์ด์•„์›ƒ>
    </ํฌํƒˆ>
</์ปจํ…์ŠคํŠธ>

1๏ธโƒฃ Context Provider

๋ชจ๋‹ฌ์„ ์—ด๊ณ  ๋‹ซ์•„์ค„ ์ƒํƒœ์™€ handler ๋ฅผ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ณต์œ ๋ฅผ ํ•ด์•ผํ•˜๋‹ˆ ์š”๊ฒƒ๋“ค์˜ ํƒ€์ž…์„ ์„ ์–ธํ•ด์ค€๋‹ค. ๊ทธ๋ฆฌ๊ณ  createContext ๋ฅผ ์‚ฌ์šฉํ•ด์„œ context ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์ž.

export type ModalContextType = {
  open: boolean;
  onOpen: () => void;
  onClose: () => void;
};

const ModalContext = createContext<ModalContextType | null>(null);

๊ทธ๋ฆฌ๊ณ  ModalRoot ๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค๊ณ  ๋‚ด๋ถ€์— ContextProvider ๋ฅผ ๋„ฃ์–ด์คฌ๋‹ค.

function ModalRoot({children}: PropsWithChildren) {
    const [open, setOpen] = useState<boolean>(false);

  const onOpen = useCallback(() => {
    setOpen(true);
  }, []);

  const onClose = useCallback(() => {
    setOpen(false);
  }, []);

  return <ModalContext.Provider value={{open, onOpen, onClose}}>{props.children}</ModalContext.Provider>;
}

ํ  ๊ทธ๋Ÿฐ๋ฐ ์•„๋งˆ ๋ชจ๋‹ฌ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋‹ค๋ฅธ ๊ณณ์—์„œ๋„ ๊ฐ„๋‹จํ•˜๊ฒŒ ์—ด๊ณ  ๋‹ซ๋Š” ๋กœ์ง์ด ๋งŽ์ด ์“ฐ์ผํ…Œ๋‹ˆ ํ•ด๋‹น๋กœ์ง์€ ๋‹ค๋ฅธ useToggle ํ›…์œผ๋กœ ๋ถ„๋ฆฌํ•ด์คฌ๋‹ค. Toast ์ปดํฌ๋„ŒํŠธ๋‚˜ ๋‹ค๋ฅธ ๋น„์Šทํ•œ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ์œ ์šฉํ•˜๊ฒŒ ์“ฐ์ผ ๊ฒƒ ๊ฐ™๋‹ค. (ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ์ž‘์—…๋“ค์€ ์ด๋ ‡๊ฒŒ ๋ฏธ๋ฆฌ ํ•  ํ•„์š”๋Š” ์‚ฌ์‹ค ์—†๋‹ค. ใ…‹ใ…‹ใ…‹ใ…‹ ๋‚˜๋Š” ์ด๋ฏธ ์“ฐ์ผ๊ฑฐ๋ผ๋Š” ๊ฒƒ์ด ๋ช…ํ™•ํ–ˆ๊ธฐ์— ๋ถ„๋ฆฌํ–ˆ์„ ๋ฟ)

export const useToggle = (): ModalContextType => {
    const [open, setOpen] = useState<boolean>(false);

  const onOpen = useCallback(() => {
    setOpen(true);
  }, []);

  const onClose = useCallback(() => {
    setOpen(false);
  }, []);

    return {open, onOpen, onClose}
}
export type ModalContextType = {
  open: boolean;
  onOpen: () => void;
  onClose: () => void;
};

const ModalContext = createContext<ModalContextType | null>(null);

function ModalRoot({children}: PropsWithChildren) {
    const toggle = useToggle();

  return <ModalContext.Provider value={toggle}>{props.children}</ModalContext.Provider>;
}

์ •๋ฆฌํ•˜๋‹ˆ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์กŒ๋‹ค. ์Œ.. ๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ModalRoot ์˜ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” Modal ์„ ๊ตฌ์„ฑํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์ด๋ฅผ ์—ด์–ด์ค„ ๋ฐฉ๋ฒ•์ด ์—†๋‹ค. ์˜ˆ๋ฅผ ๋“ค๋ฉด, ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ์„œ ๋ชจ๋‹ฌ์„ ์—ฌ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์„œ๋ฒ„์™€์˜ ๋„คํŠธ์›Œํฌ ํ†ต์‹  ์ดํ›„์— ์ž๋™์œผ๋กœ ๋ชจ๋‹ฌ์ด ์—ด๋ฆฌ๋Š” ๋“ฑ์˜ ์ผ€์ด์Šค์—์„œ๋Š” ์‚ฌ์šฉํ•˜๊ธฐ๊ฐ€ ํž˜๋“ค์–ด์ง„๋‹ค.

์ด๊ฒƒ๊ณผ ๊ด€๋ จํ•ด์„œ ๋‹ค๋ฅธ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ useImperativeHandle ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํƒœ๋ฅผ ์„ ์–ธํ•˜๊ณ  Modal ๋กœ ๋„˜๊ฒจ์ค„ ํ•„์š”๊ฐ€ ์—†์ง€ ์•Š์„๊นŒ? ๋ผ๋Š” ์—ฌ๋Ÿฌ ๊ณ ๋ฏผ๋“ค์„ ํ•ด๋ดค๊ณ โ€ฆ ๊ทธ๊ฒƒ์— ๋Œ€ํ•œ ๊ฒฐ๋ก ์€ ์ง€๊ธˆ์œผ๋กœ์„œ๋Š” ์˜ค๋ฒ„์ŠคํŽ™์ด๋ผ๋Š” ๋‚˜๋ฆ„์˜ ๊ฒฐ๋ก ์„ ๋‚ด๋ ธ๋‹ค. ๊ทธ๋ž˜์„œ ๊ฒฐ๊ตญ์€ ModalRoot ์ปดํฌ๋„ŒํŠธ์—์„œ ์‚ฌ์šฉ๋  ์ƒํƒœ์™€ ํ•ธ๋“ค๋Ÿฌ๋Š” ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ€ํ„ฐ ์ „๋‹ฌ ๋ฐ›๋Š” ๊ฒƒ์œผ๋กœ ์ˆ˜์ •ํ•ด์คฌ๋‹ค.

const ParentComponent = () => {
    const context = useToggle();

    return <ModalRoot context={context}></ModalRoot>;
}
export type ModalContextType = {
  open: boolean;
  onOpen: () => void;
  onClose: () => void;
};

const ModalContext = createContext<ModalContextType | null>(null);

interface ModalRootProps extends PropsWithChildren {
    context: ModalContextType;
}
function ModalRoot({context, children}: PropsWithChildren) {

  return <ModalContext.Provider value={context}>{props.children}</ModalContext.Provider>;
}

์•„๋งˆ ์ด ๋ถ€๋ถ„์ด ์ด๋ฒˆ ๋ชจ๋‹ฌ ๋งŒ๋“ค๊ธฐ์—์„œ ๊ฐ€์žฅ ํฐ ๊ณ ๋ฏผ๊ณผ ์‹œ๊ฐ„์„ ๋“ค์˜€๋˜ ๋‚ด์šฉ์ธ๋ฐ.. ๐Ÿคฆ๐Ÿปโ€โ™€๏ธ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ๋Š” ์ด๋ฒˆ UI์š”์†Œ ๋งŒ๋“ค๊ธฐ ์Šคํ„ฐ๋””์—์„œ ์ด์•ผ๊ธฐ๋ฅผ ํ•ด๋ณด๊ฑฐ๋‚˜ ์ฃผ๋ณ€ ๋ถ„๋“ค๊ณผ ๋งŽ์ด ํ† ๋ก ํ•ด๋ด์•ผ๊ฒ ๋‹ค. ๋ญ ๊ทธ๋ž˜๋„ ์ด๊ฒŒ ๋ช…ํ™•ํ•œ ๋‹ต์ด ์žˆ๋Š” ๋ถ€๋ถ„์€ ์•„๋‹ˆ๋‹ˆ, ์ง€๊ธˆ์œผ๋กœ์„œ๋Š” ๊ฐ€์žฅ ์ตœ์„ ์˜ ๋ฐฉ๋ฒ• ๊ฐ™๋”๋ผ.

<์ปจํ…์ŠคํŠธ> โœ…
    <๋ฒ„ํŠผ/>
    <ํฌํƒˆ>
        <๋ ˆ์ด์•„์›ƒ>
            ...๋‚ด๋ถ€ ์›ํ•˜๋Š” ๊ฒƒ๋“ค ์กฐํ•ฉ
        </๋ ˆ์ด์•„์›ƒ>
    </ํฌํƒˆ>
</์ปจํ…์ŠคํŠธ>

2๏ธโƒฃ ๋ชจ๋‹ฌ์„ ์—ด๊ณ  ๋‹ซ์•„์ค„ ๋ฒ„ํŠผ ๋งŒ๋“ค๊ธฐ

์ด์ œ๋Š” ๋ชจ๋‹ฌ์„ ์—ด๊ณ  ๋‹ซ์„ ๋ฒ„ํŠผ์„ ๋งŒ๋“คํ…๋ฐ, ๋ฒ„ํŠผ์„ ๋งŒ๋“ค๊ธฐ ์ „์— ๋ฒ„ํŠผ ๋‚ด๋ถ€์—์„œ context ๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ปค์Šคํ…€ ํ›…์„ ํ•˜๋‚˜ ๋ฏธ๋ฆฌ ๋งŒ๋“ค์—ˆ๋‹ค.

const useModalContext = () => {
  const context = useContext(ModalContext); // ModalContextType | null
  if (!context) {
    throw "value not provided";
  }
  return context;
};

context ๊ด€๋ จ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ„ํŽธํ•˜๊ฒŒ ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด ํ›…์„ ๋งŒ๋“ค์—ˆ๋‹ค. ์ด์ œ ์ด๊ฒƒ์„ ์‚ฌ์šฉํ•ด์„œ Trigger ๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์ž. ๋ฒ„ํŠผ์˜ ๊ฒฝ์šฐ์—๋Š” ๋‹จ์ˆœํžˆ ์—ด๊ณ  ๋‹ซ๊ธฐ๋งŒ ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ํŠน์ • ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— props ๋กœ ์ „๋‹ฌ๋ฐ›์€ onClick ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ž‘์„ฑํ–ˆ๋‹ค.

import Button, { ButtonProps } from "components/@elements/Button";

function ModalTrigger(props: ButtonProps) {
  const { children, ...rest } = props;
  const context = useModalContext();

  const onToggle = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    const callback = context.open ? context.onClose : context.onOpen;
    if (props.onClick) {
      props.onClick(event);
    }
    callback();
  };
  return (
    <Button onClick={onToggle} {...rest}>
      {children}
    </Button>
  );
}

์‚ฌ์‹ค ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค๊ณ  ๋ณด๋‹ˆ ๊ทธ๋ƒฅ ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฒ„ํŠผ๊ณผ ๋‹จ์ˆœํžˆ ์—ด๊ณ  ๋‹ซ๊ธฐ๋งŒ ํ•˜๋Š” ๋ฒ„ํŠผ์„ ๋ถ„๋ฆฌํ•ด์ฃผ๋Š”๊ฒŒ ๋งž์„์ง€ ๊ณ ๋ฏผ์ด ๋“œ๋Š”๋ฐ, ์•„์ง์€ ํฌ๊ฒŒ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์€ ์•„๋‹Œ ๊ฒƒ ๊ฐ™์•„์„œ ํ•˜๋‚˜๋กœ ํ‰์ณค๋‹ค. (๊ท€์ฐฎ..)

3๏ธโƒฃ Portal ๋งจ๋“ค๊ธฐ.

์ด์ œ ์ฒ˜์Œ ๊ณต๋ถ€ํ–ˆ๋˜ Portal ์„ ๋งŒ๋“ค์–ด์ค„ ๊ฒƒ์ด๋‹ค. ์šฐ์„  .html ํŒŒ์ผ์„ ์ฐพ์•„์„œ ์•„๋ž˜์™€ ๊ฐ™์ด "root-modal" ์„ ๋งŒ๋“ค์–ด์คฌ๋‹ค. ๋‚ด๊ฐ€ ํฌํƒˆ์„ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ์ด์ œ ๊ทธ ์ปดํฌ๋„ŒํŠธ๋Š” "root" ๋‚ด๋ถ€๊ฐ€ ์•„๋‹Œ "root-modal" ๋‚ด๋ถ€์—์„œ ๋ Œ๋”๋ง ๋  ๊ฒƒ์ด๋‹ค.

<!doctype html>
<html lang="en">
  <head>
    <!-- ... -->
  </head>
  <body>
    <div id="root-modal"></div>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

๋ชจ๋‹ฌ์„ ๋„์›Œ์ค„ ํฌํƒˆ์„ ๋งŒ๋“ค์–ด๋ณผ๊นŒ? ํ•ต์‹ฌ์€ ๋‘ ๊ฐ€์ง€๋‹ค.

  • children : ๋ Œ๋”๋งํ•  ์ž์‹ ์ปดํฌ๋„ŒํŠธ

  • container : DOM element

function ModalPortal({ children }: PropsWithChildren) {
  const container = document.getElementById("root-modal");

  if (container === null) {
    return null;
  }
  return ReactDom.createPortal(children, container);
}

์ด์ œ ํฌํƒˆ์ด ๋งŒ๋“ค์–ด์กŒ์œผ๋‹ˆ, ์•„๋ž˜์ฒ˜๋Ÿผ ์ž‘์„ฑํ•˜๋ฉด ํ™”๋ฉด์— ๋ Œ๋”๋ง์ด ๋  ๊ฒƒ์ด๋‹ค.

<ModalPortal>
    // ...๋ Œ๋”๋งํ•  ์ปดํฌ๋„ŒํŠธ ์•ผ๋ฌด์ง€๊ฒŒ ์ž‘์„ฑํ•ด์ฃผ๊ธฐ
</ModalPortal>
<์ปจํ…์ŠคํŠธ> โœ…
    <๋ฒ„ํŠผ/> โœ…
    <ํฌํƒˆ> โœ…
        <๋ ˆ์ด์•„์›ƒ>
            ...๋‚ด๋ถ€ ์›ํ•˜๋Š” ๊ฒƒ๋“ค ์กฐํ•ฉ
        </๋ ˆ์ด์•„์›ƒ>
    </ํฌํƒˆ>
</์ปจํ…์ŠคํŠธ>

4๏ธโƒฃ Layout ์žก๊ธฐ

์šฐ์„  ๋ ˆ์ด์•„์›ƒ ๋‚ด์— ํฌ๊ฒŒ ๋‘ ๊ฐœ์˜ div ๋ฅผ ๋„ฃ์–ด์คฌ๋‹ค. ์ „์ฒด ํ™”๋ฉด์„ ๋ฎ์„ ๋ฐฐ๊ฒฝ์˜์—ญ์ด ๋  ๋ถ€๋ถ„๊ณผ ํŒ์—…์˜ ๋‚ด์šฉ์„ ๋‹ด์•„์ค„ ์ปจํ…Œ์ด๋„ˆ ์˜์—ญ์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ณต์œ ๋ฐ›๊ณ  ์žˆ๋Š” context ๋ฅผ ๋ถˆ๋Ÿฌ์™€์„œ ์ƒํƒœ์— ๋”ฐ๋ผ ๋ Œ๋”๋ง์ด ๋  ์ˆ˜ ์žˆ๊ฒŒ ์กฐ๊ฑด๋ฌธ์„ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค.

const ModalLayout = (props: ModalProps) => {
  const { children } = props;
  const context = useModalContext();

    if (!context.open) return null;
  return (
        {/* ๊ฐ€์žฅ ๋ฐ”๊นฅ ์˜์—ญ - ๋ฐฐ๊ฒฝ */}
    <div css={dimmed}>
            {/* ํŒ์—… ์ปจํ…Œ์ด๋„ˆ */}
            <div css={layout}>
                // ...์ปดํฌ๋„ŒํŠธ
      </div>
    </div>
  );
};

๊ทธ๋Ÿฐ๋ฐ ์ด์ œ ์กฐ๊ธˆ์˜ ๋””ํ…Œ์ผ์„ ์ถ”๊ฐ€ํ•ด์ค˜๋ดค๋‹ค. ๋งˆ์šฐ์Šค๋กœ ๊ตณ์ด ๋‹ซ๊ธฐ ๋ฒ„ํŠผ๊นŒ์ง€ ์ฐพ์•„๊ฐ€๋Š” ์ˆ˜๊ณ ๋ฅผ ๋œ์–ด์ฃผ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์„œ ๋ฐฐ๊ฒฝ์„ ํด๋ฆญํ•˜๋ฉด ๋ชจ๋‹ฌ์ด ๋‹ซํžˆ๊ฒŒ๋” ๊ตฌํ˜„ํ•ด์ฃผ๋ฉด ํŽธํ•  ๊ฒƒ ๊ฐ™์•„์„œ!

์ด ๊ฒฝ์šฐ์—๋Š” ์™ธ๋ถ€ ์˜์—ญ ์ž์ฒด์— ํด๋ฆญ์ด๋ฒคํŠธ๋ฅผ ๊ฑธ์–ด์ค„ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™์€๋ฐ, ์ด๋ฒˆ์—๋Š” ref ๋ฅผ ์‚ฌ์šฉํ•ด๋ดค๋‹ค. ref ๋ฅผ ๋งŒ๋“ค์–ด์„œ ํด๋ฆญ ์ด๋ฒคํŠธ๊ฐ€ ์ผ์–ด๋‚˜๋ฉด โ€˜์•ˆ๋˜๋Š”โ€™ ๊ณณ์— ์‹ฌ์–ด์คฌ๋‹ค. ์ด์ œ ์ด ๋ถ€๋ถ„์˜ ์™ธ๋ถ€์˜์—ญ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ onClose ํ•จ์ˆ˜๊ฐ€ ๋ถˆ๋ฆฌ๊ฒŒ๋” ๋กœ์ง์„ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋˜๋Š”๋ฐ ์ด๋ฒคํŠธ๋ฅผ ๋‹ค๋ฃจ๋Š” ๊ฒƒ์ด๋‹ˆ addEventListener ๋ฅผ ๊ฑธ์–ด์ค„ ๊ฒƒ์ด๋‹ค.

const ModalLayout = (props: ModalProps) => {
  // ...
  const context = useModalContext();
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // โœ… ์ด๋ฒคํŠธ๊ฐ€ ์ƒ๊ธธ ๋•Œ ๋ถˆ๋Ÿฌ์ค„ ์ฝœ๋ฐฑ ํ•จ์ˆ˜
    const closeModal = (event: MouseEvent) => {
                         // โœ… ๋ชจ๋‹ฌ ์˜์—ญ์ด ํฌํ•จ๋˜์ง€ ์•Š์€ ๊ณณ์ผ ๋•Œ!
      if (ref.current && !ref.current.contains(event.target as Node | null)) {
        context.onClose();
      }
    };

    // โœ… ๋งˆ์šฐ์Šค๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ closeModal ํ•จ์ˆ˜๊ฐ€ ๋ถˆ๋ฆผ
    window.addEventListener("mousedown", closeModal);
    return () => {
      // โœ… ์•Œ์ž˜๋”ฑ์œผ๋กœ ํ•ด์ œ์‹œ์ผœ์ฃผ๊ธฐ 
      window.addEventListener("mousedown", closeModal);
    };
  }, [context]);

  // ... 
  return (
    <div css={dimmed}>
      <div css={layout} ref={ref}>
        {children}
      </div>
    </div>
  );
};

์•„์ฃผ ๋กธ์Šค๊ตฟํ•˜๊ฒŒ ์ž˜ ์ž‘๋™์ด ๋œ๋‹ค. ์ด๊ฒƒ๋„ ์ž์ฃผ ์‚ฌ์šฉํ•˜๊ฒŒ๋˜๋ฉด ๋”ฐ๋กœ ๋ถ„๋ฆฌํ•ด์ค˜๋„ ์ข‹์„ ๊ฒƒ ๊ฐ™๊ธดํ•˜๋‹ค.

5๏ธโƒฃ etc (๋‚ด๋ถ€ ๋ ˆ๊ณ  ์กฐ๊ฐ๋“ค)

<์ปจํ…์ŠคํŠธ> โœ…
    <๋ฒ„ํŠผ/> โœ…
    <ํฌํƒˆ> โœ…
        <๋ ˆ์ด์•„์›ƒ> โœ…
            ...๋‚ด๋ถ€ ์›ํ•˜๋Š” ๊ฒƒ๋“ค ์กฐํ•ฉ
        </๋ ˆ์ด์•„์›ƒ>
    </ํฌํƒˆ>
</์ปจํ…์ŠคํŠธ>

์ด์ œ ๋ญ ํ•ต์‹ฌ์€ ๋‹ค ๋‚˜์™”์œผ๋‹ˆ ์•Œ์ž˜๋”ฑ์œผ๋กœ ํ•„์š”ํ•œ ๋ ˆ๊ณ ๋“ค์„ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ๋œ๋‹ค. ๋‚˜๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ Title, Body, Footer ๋กœ ๋‚˜๋ˆ ์„œ ์ž‘์„ฑํ•ด์คฌ๋‹ค.

function ModalHeader({ children }: PropsWithChildren) {
  return (
    <div css={headerStyle}>
      {children}
    </div>
  );
}

function ModalBody({ children }: PropsWithChildren) {
  return (
    <div css={bodyStyle}>
      {children}
    </div>
  );
}

function ModalFooter({ children }: PropsWithChildren) {
  return (
    <div css={footerStyle}>
      {children}
    </div>
  );
}

์ด์ œ ํ•„์š”์— ๋”ฐ๋ผ์„œ ํŒŒ์ธ (?ใ…‹ใ…‹)๋“ค์„ ๋งŒ๋“ค์–ด์„œ ์›ํ•˜๋Š”๋Œ€๋กœ ์กฐํ•ฉํ•ด์„œ ์‚ฌ์šฉํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ์ด๋ ‡๊ฒŒ!

export function Page() {
  const toggle = useToggle();

  return (
    <div css={layout}>
            <Button onClick={toggle.onOpen}>์ด๋ ‡๊ฒŒ๋„ ์—ด๋ฆฌ์ง€</Button>

      <Modal.Root context={modal}>
        <Modal.Portal>
          <Modal.Layout modalType="ALERT">
            <Modal.Header>์ œ๋ชฉ</Modal.Header>
            <Modal.Body>
                            <ul>
                                <li>์ฃผ์˜์‚ฌํ•ญ์€ ์ด๋ ‡๊ณ  ์ €๋ ‡๊ณ </li>
                                <li>๊ทธ๋ ‡๊ณ  ์ €๋ ‡๊ณ  ์ด๋ ‡๊ณ </li>
                            <ul>
                        </Modal.Body>
            <Modal.Footer>
              <Modal.Trigger btnType="default">๋‹ซ๊ธฐ</Modal.Trigger>
            </Modal.Footer>
          </Modal.Layout>
        </Modal.Portal>
      </Modal.Root>
    </div>
  );
}

ํฌํƒˆ์ด ์ œ๋Œ€๋กœ ๋จนํžˆ๊ณ  ์žˆ๋Š”์ง€๋„ ํ™•์ธํ•ด๋ณผ๊นŒ?

์˜ค์šฐ ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์˜๋„ํ•œ๋Œ€๋กœ "root-modal" ๋‚ด๋ถ€์—์„œ ๋ Œ๋”๋ง๋˜๊ณ  ์žˆ๋‹ค. ๋‹ฅํ„ฐ ํŠธ๋ฃจ๋ ˆ์ธ์ง€ ์„ฑ๊ณต?ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹

์ด๋ ‡๊ฒŒ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ปดํŒŒ์šด๋“œ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด๊ณผ ํฌํƒˆ์„ ์‚ฌ์šฉํ•ด์„œ ๋งŒ๋“ค์–ด๋ดค๋Š”๋ฐ, ํ™•์‹คํžˆ return ๋ฌธ ๋‚ด๋ถ€์˜ ๋ชจ์‹œ๊ปญ์ด๊ฐ€ ๊ธธ์–ด์ ธ์„œ ๋ฌด๊ฑฐ์›Œ๋ณด์ธ๋‹ค. ํ•˜์ง€๋งŒ ํ•œ ํŽธ์œผ๋กœ๋Š” ๊ฐ๊ฐ์ด ์–ด๋–ค ์˜๋ฏธ๋ฅผ ๊ฐ€์ง€๊ณ  ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š”์ง€๊ฐ€ ๋ช…ํ™•ํ•˜๊ฒŒ ํŒŒ์•…์ด ๊ฐ€๋Šฅํ•ด์„œ ํŽธ๋ฆฌํ•˜๊ธฐ๋„ ํ•˜๋‹ค. ํ•ด๋‹น ํŽ˜์ด์ง€์˜ ์ถ”์ƒํ™” ๋‹จ๊ณ„์— ๋”ฐ๋ผ์„œ ๋”ฐ๋กœ ํฐ ๋ญ‰ํ‹ฐ๊ธฐ๋กœ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•๋„ ๊ดœ์ฐฎ์„ ๊ฒƒ ๊ฐ™๋‹ค. ๊ทธ๊ฑด ๊ทธ ํŽ˜์ด์ง€ ๋‚ด๋ถ€์—์„œ์˜ ์ถ”์ƒํ™” ๋‹จ๊ณ„๋ฅผ ์ž˜ ๊ณ ๋ คํ•ด์„œ ํŒ๋‹จํ•˜๋ฉด ๋  ๋ชซ์ธ ๊ฒƒ ๊ฐ™๊ตฐ.. ๐Ÿค”


๐Ÿ‘ฉ๐Ÿปโ€๐ŸŒพ ๋งˆ๋ฌด๋ฆฌ

์ปดํŒŒ์šด๋“œ ์ปดํฌ๋„ŒํŠธ ํŒจํ„ด์˜ ๊ฒฝ์šฐ๋Š” ์ง€๊ธˆ๊นŒ์ง€ ๊ฐœ๋ฐœ์„ ํ•˜๋ฉด์„œ ๊ฐ€์žฅ ์„ ํ˜ธํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์–ด์„œ ์ด๋ฒˆ์—๋„ ์š” ํŒจํ„ด์œผ๋กœ ๋ชจ๋“  UI ์ปดํฌ๋„ŒํŠธ๋“ค์„ ๋งŒ๋“ค์–ด๋ณด๋ ค ํ•˜๋Š”๋ฐ, ๋” ๋งŽ์ด ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ์žฅ๋‹จ์ ์— ๋Œ€ํ•ด ๋งŽ์ด ๋А๊ปด๋ด์•ผ๊ฒ ๋‹ค. ์•„์ง์€ ๊ทธ ๊ฒฝํ—˜์ด ๋งŽ๋‹ค๊ณ ๋Š” ํ•  ์ˆ˜๊ฐ€ ์—†์–ด์„œ ๋‚ด๊ฐ€ ๋А๋ผ๊ณ  ์žˆ๋Š” ๊ฒƒ๋“ค์„ ์ž์‹ ์žˆ๊ฒŒ ๋งํ•˜๊ธฐ๋Š” ํž˜๋“ค ๊ฒƒ ๊ฐ™๋‹ค.

๊ทธ๋ž˜๋„ ๋ญ”๊ฐ€ props ๊ฒฐ๋ฒฝ์ฆ์ด ์–ธ์ œ๋ถ€ํ„ด๊ฐ€ ์ƒ๊ฒจ๋ฒ„๋ ค์„œ ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ใ…‹ ์ง„์งœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ๋„˜๊ฒจ์ฃผ๊ณ  ์‹ถ์€ ์š•์‹ฌ์ด ๊ทธ๋“ํ•œ ์ธ๊ฐ„์ด๋ผ ์•„์ง์€ ์š” ํŒจํ„ด์„ ๊ฐ€์žฅ ์„ ํ˜ธํ•˜๋Š” ํŽธ์ด๋‹ค. ํ•„์š”ํ•œ ๊ฒƒ๋“ค๋งŒ ์กฐ๋ฆฝํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ๋„ ํŽธ๋ฆฌํ•˜๊ณ !

์–ด์จŒ๋“  ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด๋ดค๊ณ .. ๋‹ค์Œ์€ ์•„์ฝ”๋””์–ธ์„ ํ•œ ๋ฒˆ ๋งŒ๋“ค์–ด๋ด์•ผ๊ฒ ๋‹ค. ๊ฐ„๋ฐ”๋ ˆ ํŠธ๋ฃจ์Ÿ

์ฐธ๊ณ