Modal ๋ง๋ค๊ธฐ
๋ฆฌ์กํธ ํฌํ๊ณผ ์ปดํ์ด๋ ์ปดํฌ๋ํธ ์ ์ฉํด์ Modal ๋ง๋ค์ด๋ณด๊ธฐ

โ๐ป ๋ค์ด๊ฐ๋ฉฐ
์ด๋ฒ์ ์ฌ๋จ๋๊ณผ ํจ๊ปํ๋ 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 ํ ์๊ฐ ๊ต์ฅํ ์ฆ๊ฐํ๋ ํธ์ด๋ฉฐ ์ด๋ ์ ๋๋ก ์ ์ฐํ๊ฒ ๊ฐ์ง๊ณ ๊ฐ์ผํ๋๊ฐ์ ๋ํ ์ค์ฌ์ ์ก๋๊ฒ ์ฝ์ง๊ฐ ์์ ๊ฒ ๊ฐ๋ค.
์ด๋ก ์ ์ธ๊ฑด ๊ฐ๋จํ๊ฒ ์์๋ดค์ผ๋ ํ ๋ฒ ๋ง๋ค์ด๋ณด์. ์กฐ๊ธ ๋ ์์ธํ ๋ด์ฉ์ด๋ ๋ค์ํ ๋ฆฌ์กํธ์ ๋์์ธ ํจํด๊ณผ ๊ด๋ จํด์๋ ์๋ ํฌ์คํ ์ ์ฐธ๊ณ ํด๋ณด์.
๐๏ธ ์ปดํฌ๋ํธ ๊ตฌ์กฐ ์ก์๋ณด๊ธฐ

๋๋ถ๋ถ์ ๋ชจ๋ฌ์ ์์ ๋ชจ์ต์ผ๋ก ์๊ฒผ๋ค. ๋ง๋ค์ด์ผํ๋ ๊ฒ๋ค์ ๋์ดํด๋ณผ๊น?
๋ชจ๋ฌ ํ์ดํ
๋ชจ๋ฌ ๋ด์ฉ
๋ชจ๋ฌ ํ๋จ๋ถ
๋ชจ๋ฌ ๋ ์ด์์ / ๋ชจ๋ฌ ์ธ๋ถ ์์ญ - ํด๋ฆญํ๋ฉด ๋ชจ๋ฌ์ด ๋ซํ์ผํจ
๋ชจ๋ฌ ๋ฒํผ
์ฌ๊ธฐ์ ํฌํ์ ์ฌ์ฉํ๊ธฐ๋ก ํ์ผ๋ ํฌํ๋ ์ถ๊ฐํด์ค์ผํ๊ณ ๐ค ๊ฐ๊ฐ์ ์ปดํฌ๋ํธ๋ค์ด ์ํ๋ฅผ ๊ณต์ ํด์ผํ๋ Context ๋ ๋ง๋ค์ด์ค์ผํ๋ค.
React Portal
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 ๊ฒฐ๋ฒฝ์ฆ์ด ์ธ์ ๋ถํด๊ฐ ์๊ฒจ๋ฒ๋ ค์ ใ ใ ใ ใ ใ ใ ใ ์ง์ง ํ์ํ ๊ฒฝ์ฐ์๋ง ๋๊ฒจ์ฃผ๊ณ ์ถ์ ์์ฌ์ด ๊ทธ๋ํ ์ธ๊ฐ์ด๋ผ ์์ง์ ์ ํจํด์ ๊ฐ์ฅ ์ ํธํ๋ ํธ์ด๋ค. ํ์ํ ๊ฒ๋ค๋ง ์กฐ๋ฆฝํด์ ์ฌ์ฉํ๋ ๊ฒ๋ ํธ๋ฆฌํ๊ณ !
์ด์จ๋ ์ด๋ ๊ฒ ๋ง๋ค์ด๋ดค๊ณ .. ๋ค์์ ์์ฝ๋์ธ์ ํ ๋ฒ ๋ง๋ค์ด๋ด์ผ๊ฒ ๋ค. ๊ฐ๋ฐ๋ ํธ๋ฃจ์
![[์๊ฐ ์ดํธ๋ฃจ] 3์์ ํ๊ณ .](https://cdn.hashnode.com/res/hashnode/image/upload/v1711895194426/f6a22b40-ffd3-4b11-a25b-e1b040b11853.png)
![[์๊ฐ ์ดํธ๋ฃจ] 2์์ ํ๊ณ .](https://cdn.hashnode.com/res/hashnode/image/upload/v1709129901980/5a2af3c5-8f6e-4f07-9c4f-1b43bef27ae9.png)


![[์๊ฐ ์ดํธ๋ฃจ] 1์์ ํ๊ณ .](https://cdn.hashnode.com/res/hashnode/image/upload/v1706708547437/51af577d-21f9-4256-8939-95c577a58abc.jpeg)