๋ฆฌ์กํธ ํฌํ์ ๊ฐ๋ ฅํ ์ด๋ฒคํธ ํธ๋ค๋ง์ ๋ง์คํฐํ์ธ์. ์ด ์ข ํฉ ๊ฐ์ด๋๋ ์ด๋ฒคํธ ์์์ด ์ด๋ป๊ฒ DOM ํธ๋ฆฌ์ ์ฐจ์ด๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ฐ๊ฒฐํ์ฌ ๊ธ๋ก๋ฒ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ํํ ์ฌ์ฉ์ ์ํธ์์ฉ์ ๋ณด์ฅํ๋์ง ์์ธํ ์ค๋ช ํฉ๋๋ค.
๋ฆฌ์กํธ ํฌํ ์ด๋ฒคํธ ํธ๋ค๋ง ๋ง์คํฐํ๊ธฐ: ๊ธ๋ก๋ฒ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ DOM ํธ๋ฆฌ ๊ฐ ์ด๋ฒคํธ ์์
๊ด๋ฒ์ํ๊ณ ์ํธ ์ฐ๊ฒฐ๋ ์น ๊ฐ๋ฐ์ ์ธ๊ณ์์, ์ ์ธ๊ณ ์ฌ์ฉ์๋ฅผ ๋ง์กฑ์ํค๋ ์ง๊ด์ ์ด๊ณ ๋ฐ์์ด ๋น ๋ฅธ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌ์ถํ๋ ๊ฒ์ ๋งค์ฐ ์ค์ํฉ๋๋ค. ๋ฆฌ์กํธ๋ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์ํคํ ์ฒ๋ฅผ ํตํด ์ด๋ฅผ ๋ฌ์ฑํ ์ ์๋ ๊ฐ๋ ฅํ ๋๊ตฌ๋ฅผ ์ ๊ณตํฉ๋๋ค. ๊ทธ์ค์์๋ ๋ฆฌ์กํธ ํฌํ(React Portals)์ ๋ถ๋ชจ ์ปดํฌ๋ํธ์ ๊ณ์ธต ๊ตฌ์กฐ ์ธ๋ถ์ ์๋ DOM ๋ ธ๋์ ์์์ ๋ ๋๋งํ๋ ๋งค์ฐ ํจ๊ณผ์ ์ธ ๋ฉ์ปค๋์ฆ์ผ๋ก ๋๋ณด์ ๋๋ค. ์ด ๊ธฐ๋ฅ์ ๋ชจ๋ฌ, ํดํ, ๋๋กญ๋ค์ด, ์๋ฆผ๊ณผ ๊ฐ์ด ๋ถ๋ชจ์ ์คํ์ผ๋ง์ด๋ `z-index` ์คํํน ์ปจํ ์คํธ์ ์ ์ฝ์์ ๋ฒ์ด๋์ผ ํ๋ UI ์์๋ฅผ ๋ง๋๋ ๋ฐ ๋งค์ฐ ์ ์ฉํฉ๋๋ค.
ํฌํ์ ์์ฒญ๋ ์ ์ฐ์ฑ์ ์ ๊ณตํ์ง๋ง, ๊ณ ์ ํ ๊ณผ์ ๋ฅผ ์ ์ํฉ๋๋ค. ๋ฐ๋ก ๋ฌธ์ ๊ฐ์ฒด ๋ชจ๋ธ(DOM) ํธ๋ฆฌ์ ์ฌ๋ฌ ๋ถ๋ถ์ ๊ฑธ์น ์ํธ์์ฉ์ ๋ค๋ฃฐ ๋์ ์ด๋ฒคํธ ํธ๋ค๋ง์ ๋๋ค. ์ฌ์ฉ์๊ฐ ํฌํ์ ํตํด ๋ ๋๋ง๋ ์์์ ์ํธ์์ฉํ ๋, ์ด๋ฒคํธ๊ฐ DOM์ ํต๊ณผํ๋ ๊ฒฝ๋ก๋ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ํธ๋ฆฌ์ ๋ ผ๋ฆฌ์ ๊ตฌ์กฐ์ ์ผ์นํ์ง ์์ ์ ์์ต๋๋ค. ์ด๋ ์ ๋๋ก ์ฒ๋ฆฌ๋์ง ์์ผ๋ฉด ์์์น ๋ชปํ ๋์์ผ๋ก ์ด์ด์ง ์ ์์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ๊น์ด ํ๊ตฌํ ํด๊ฒฐ์ฑ ์ ์น ๊ฐ๋ฐ์ ๊ธฐ๋ณธ ๊ฐ๋ ์ธ ์ด๋ฒคํธ ์์(Event Delegation)์ ์์ต๋๋ค.
์ด ์ข ํฉ ๊ฐ์ด๋๋ ๋ฆฌ์กํธ ํฌํ์ ์ด๋ฒคํธ ํธ๋ค๋ง์ ๋ํ ๊ถ๊ธ์ฆ์ ํ์ด์ค ๊ฒ์ ๋๋ค. ์ฐ๋ฆฌ๋ ๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ ๋ณต์ก์ฑ์ ํ๊ณ ๋ค๊ณ , ์ด๋ฒคํธ ๋ฒ๋ธ๋ง๊ณผ ์บก์ฒ์ ๋ฉ์ปค๋์ฆ์ ์ดํดํ๋ฉฐ, ๊ฐ์ฅ ์ค์ํ๊ฒ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ธ๋ก๋ฒ ๋๋ฌ ๋ฒ์๋ UI์ ๋ณต์ก์ฑ์ ๊ด๊ณ์์ด ์ํํ๊ณ ์์ธก ๊ฐ๋ฅํ ์ฌ์ฉ์ ๊ฒฝํ์ ๋ณด์ฅํ๊ธฐ ์ํด ๊ฐ๋ ฅํ ์ด๋ฒคํธ ์์์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์์ฐํ ๊ฒ์ ๋๋ค.
๋ฆฌ์กํธ ํฌํ ์ดํดํ๊ธฐ: DOM ๊ณ์ธต์ ์๋ ๋ค๋ฆฌ
์ด๋ฒคํธ ํธ๋ค๋ง์ ๋ํด ์์๋ณด๊ธฐ ์ ์, ๋ฆฌ์กํธ ํฌํ์ด ๋ฌด์์ด๋ฉฐ ์ ํ๋ ์น ๊ฐ๋ฐ์์ ๊ทธ๋ ๊ฒ ์ค์ํ์ง์ ๋ํ ์ดํด๋ฅผ ํ๊ณ ํ ํด๋ด ์๋ค. ๋ฆฌ์กํธ ํฌํ์ `ReactDOM.createPortal(child, container)`๋ฅผ ์ฌ์ฉํ์ฌ ์์ฑ๋ฉ๋๋ค. ์ฌ๊ธฐ์ `child`๋ ๋ ๋๋ง ๊ฐ๋ฅํ ๋ชจ๋ ๋ฆฌ์กํธ ์์(์: ์์, ๋ฌธ์์ด, ํ๋๊ทธ๋จผํธ)์ด๊ณ , `container`๋ DOM ์์์ ๋๋ค.
๋ฆฌ์กํธ ํฌํ์ด ๊ธ๋ก๋ฒ UI/UX์ ํ์์ ์ธ ์ด์
๋ถ๋ชจ ์ปดํฌ๋ํธ์ `z-index`๋ `overflow` ์์ฑ์ ๊ด๊ณ์์ด ๋ค๋ฅธ ๋ชจ๋ ์ฝํ
์ธ ์์ ๋ํ๋์ผ ํ๋ ๋ชจ๋ฌ ๋ํ ์์๋ฅผ ์๊ฐํด๋ณด์ธ์. ๋ง์ฝ ์ด ๋ชจ๋ฌ์ด ์ผ๋ฐ์ ์ธ ์์์ผ๋ก ๋ ๋๋ง๋๋ค๋ฉด, `overflow: hidden`์ธ ๋ถ๋ชจ์ ์ํด ์๋ฆฌ๊ฑฐ๋ `z-index` ์ถฉ๋๋ก ์ธํด ํ์ ์์๋ค ์์ ๋ํ๋๊ธฐ ์ด๋ ค์ธ ์ ์์ต๋๋ค. ํฌํ์ ๋ชจ๋ฌ์ด ๋ฆฌ์กํธ ๋ถ๋ชจ ์ปดํฌ๋ํธ์ ์ํด ๋
ผ๋ฆฌ์ ์ผ๋ก๋ ๊ด๋ฆฌ๋์ง๋ง, ๋ฌผ๋ฆฌ์ ์ผ๋ก๋ ์ง์ ๋ DOM ๋
ธ๋(์ข
์ข
document.body์ ์์)์ ์ง์ ๋ ๋๋ง๋๋๋ก ํจ์ผ๋ก์จ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํฉ๋๋ค.
- ์ปจํ ์ด๋ ์ ์ฝ ํ์ถ: ํฌํ์ ์ปดํฌ๋ํธ๊ฐ ๋ถ๋ชจ ์ปจํ ์ด๋์ ์๊ฐ์ ๋ฐ ์คํ์ผ๋ง ์ ์ฝ์์ "ํ์ถ"ํ ์ ์๊ฒ ํด์ค๋๋ค. ์ด๋ ๋ทฐํฌํธ๋ฅผ ๊ธฐ์ค์ผ๋ก ์์น๋ฅผ ์ก๊ฑฐ๋ ์คํํน ์ปจํ ์คํธ์ ์ต์๋จ์ ์์นํด์ผ ํ๋ ์ค๋ฒ๋ ์ด, ๋๋กญ๋ค์ด, ํดํ, ๋ํ ์์์ ํนํ ์ ์ฉํฉ๋๋ค.
- ๋ฆฌ์กํธ ์ปจํ ์คํธ์ ์ํ ์ ์ง: ๋ค๋ฅธ DOM ์์น์ ๋ ๋๋ง๋จ์๋ ๋ถ๊ตฌํ๊ณ , ํฌํ์ ํตํด ๋ ๋๋ง๋ ์ปดํฌ๋ํธ๋ ๋ฆฌ์กํธ ํธ๋ฆฌ์์์ ์์น๋ฅผ ์ ์งํฉ๋๋ค. ์ด๋ ์ฌ์ ํ ์ปจํ ์คํธ์ ์ ๊ทผํ๊ณ , props๋ฅผ ๋ฐ์ผ๋ฉฐ, ์ผ๋ฐ์ ์ธ ์์์ธ ๊ฒ์ฒ๋ผ ๋์ผํ ์ํ ๊ด๋ฆฌ์ ์ฐธ์ฌํ ์ ์์์ ์๋ฏธํ์ฌ ๋ฐ์ดํฐ ํ๋ฆ์ ๋จ์ํํฉ๋๋ค.
- ํฅ์๋ ์ ๊ทผ์ฑ: ํฌํ์ ์ ๊ทผ์ฑ ์๋ UI๋ฅผ ๋ง๋๋ ๋ฐ ์ค์ํ ์ญํ ์ ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ๋ชจ๋ฌ์
document.body์ ์ง์ ๋ ๋๋งํ์ฌ ํฌ์ปค์ค ํธ๋ํ์ ๊ด๋ฆฌํ๊ณ ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ์ฝํ ์ธ ๋ฅผ ์ต์์ ๋ํ ์์๋ก ์ฌ๋ฐ๋ฅด๊ฒ ํด์ํ๋๋ก ๋ณด์ฅํ๊ธฐ๊ฐ ๋ ์ฌ์์ง๋๋ค. - ๊ธ๋ก๋ฒ ์ผ๊ด์ฑ: ์ ์ธ๊ณ ์ฌ์ฉ์๋ฅผ ๋์์ผ๋ก ํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ์ผ๊ด๋ UI ๋์์ด ํ์์ ์ ๋๋ค. ํฌํ์ ๊ฐ๋ฐ์๊ฐ ๊ณ๋จ์ CSS ๋ฌธ์ ๋ DOM ๊ณ์ธต ๊ตฌ์กฐ ์ถฉ๋ ์์ด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค์ํ ๋ถ๋ถ์ ๊ฑธ์ณ ํ์ค UI ํจํด(์ผ๊ด๋ ๋ชจ๋ฌ ๋์ ๋ฑ)์ ๊ตฌํํ ์ ์๊ฒ ํด์ค๋๋ค.
์ผ๋ฐ์ ์ธ ์ค์ ์ index.html์ ์ ์ฉ DOM ๋
ธ๋(์: <div id="modal-root"></div>)๋ฅผ ๋ง๋ค๊ณ , `ReactDOM.createPortal`์ ์ฌ์ฉํ์ฌ ๊ทธ ์์ ์ฝํ
์ธ ๋ฅผ ๋ ๋๋งํ๋ ๊ฒ์
๋๋ค. ์๋ฅผ ๋ค์ด:
// public/index.html
<body>
<div id="root"></div>
<div id="portal-root"></div>
</body>
// MyModal.js
import React from 'react';
import ReactDOM from 'react-dom';
const portalRoot = document.getElementById('portal-root');
const MyModal = ({ children, isOpen, onClose }) => {
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
portalRoot
);
};
export default MyModal;
์ด๋ฒคํธ ํธ๋ค๋ง์ ์์๊ป๋ผ: DOM๊ณผ ๋ฆฌ์กํธ ํธ๋ฆฌ๊ฐ ๊ฐ๋ผ์ง ๋
๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ ์ถ์ํ์ ๊ฒฝ์ด๋ก์์ ๋๋ค. ์ด๋ ๋ธ๋ผ์ฐ์ ์ด๋ฒคํธ๋ฅผ ์ ๊ทํํ์ฌ ๋ค๋ฅธ ํ๊ฒฝ์์๋ ์ผ๊ด๋ ์ด๋ฒคํธ ํธ๋ค๋ง์ ๊ฐ๋ฅํ๊ฒ ํ๊ณ , `document` ๋ ๋ฒจ์์์ ์์์ ํตํด ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. ๋ฆฌ์กํธ ์์์ `onClick` ํธ๋ค๋ฌ๋ฅผ ๋ถ์ด๋ฉด, ๋ฆฌ์กํธ๋ ํด๋น ํน์ DOM ๋ ธ๋์ ์ง์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ถ๊ฐํ์ง ์์ต๋๋ค. ๋์ , ํด๋น ์ด๋ฒคํธ ์ ํ(์: `click`)์ ๋ํ ๋จ์ผ ๋ฆฌ์ค๋๋ฅผ `document`๋ ๋ฆฌ์กํธ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ฃจํธ์ ๋ถ์ ๋๋ค.
์ค์ ๋ธ๋ผ์ฐ์ ์ด๋ฒคํธ(์: ํด๋ฆญ)๊ฐ ๋ฐ์ํ๋ฉด, ์ด๋ ๋ค์ดํฐ๋ธ DOM ํธ๋ฆฌ๋ฅผ ๋ฐ๋ผ `document`๊น์ง ๋ฒ๋ธ๋ง๋ฉ๋๋ค. ๋ฆฌ์กํธ๋ ์ด ์ด๋ฒคํธ๋ฅผ ๊ฐ๋ก์ฑ ํฉ์ฑ ์ด๋ฒคํธ ๊ฐ์ฒด๋ก ๊ฐ์ผ ๋ค์, ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ํตํ ๋ฒ๋ธ๋ง์ ์๋ฎฌ๋ ์ด์ ํ๋ฉฐ ์ ์ ํ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๋ก ๋ค์ ๋์คํจ์นํฉ๋๋ค. ์ด ์์คํ ์ ํ์ค DOM ๊ณ์ธต ๊ตฌ์กฐ ๋ด์ ๋ ๋๋ง๋ ์ปดํฌ๋ํธ์์๋ ๋งค์ฐ ์ ์๋ํฉ๋๋ค.
ํฌํ์ ํน์ด์ : DOM์์์ ์ฐํ
์ฌ๊ธฐ์ ํฌํ์ ์ด๋ ค์์ด ์์ต๋๋ค. ํฌํ์ ํตํด ๋ ๋๋ง๋ ์์๋ ๋
ผ๋ฆฌ์ ์ผ๋ก๋ ๋ฆฌ์กํธ ๋ถ๋ชจ์ ์์์ด์ง๋ง, DOM ํธ๋ฆฌ์์์ ๋ฌผ๋ฆฌ์ ์์น๋ ์์ ํ ๋ค๋ฅผ ์ ์์ต๋๋ค. ๋ง์ฝ ๋ฉ์ธ ์ ํ๋ฆฌ์ผ์ด์
์ด <div id="root"></div>์ ๋ง์ดํธ๋๊ณ ํฌํ ์ฝํ
์ธ ๊ฐ `root`์ ํ์ ์ธ <div id="portal-root"></div>์ ๋ ๋๋ง๋๋ค๋ฉด, ํฌํ ๋ด๋ถ์์ ๋ฐ์ํ ํด๋ฆญ ์ด๋ฒคํธ๋ *์์ ์* ๋ค์ดํฐ๋ธ DOM ๊ฒฝ๋ก๋ฅผ ๋ฐ๋ผ ๋ฒ๋ธ๋ง๋์ด ๊ฒฐ๊ตญ `document.body`๋ฅผ ๊ฑฐ์ณ `document`์ ๋๋ฌํ ๊ฒ์
๋๋ค. ์ด๋ `div#root`๋ฅผ ํตํด ์์ฐ์ค๋ฝ๊ฒ ๋ฒ๋ธ๋ง๋์ด `div#root` ๋ด ํฌํ์ *๋
ผ๋ฆฌ์ * ๋ถ๋ชจ์ ์กฐ์์ ์ฐ๊ฒฐ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋์ ๋๋ฌํ์ง *์์ต๋๋ค*.
์ด๋ฌํ ๋ถ์ผ์น๋ ๋ชจ๋ ์์์ผ๋ก๋ถํฐ ์ด๋ฒคํธ๋ฅผ ์ก์ ๊ฒ์ ๊ธฐ๋ํ๋ฉฐ ๋ถ๋ชจ ์์์ ํด๋ฆญ ํธ๋ค๋ฌ๋ฅผ ๋ฐฐ์นํ๋ ์ ํต์ ์ธ ์ด๋ฒคํธ ํธ๋ค๋ง ํจํด์ด ํฌํ์ ๋ ๋๋ง๋ ์์๋ค์ ๋ํด์๋ ์คํจํ๊ฑฐ๋ ์์์น ๋ชปํ๊ฒ ๋์ํ ์ ์์์ ์๋ฏธํฉ๋๋ค. ์๋ฅผ ๋ค์ด, ๋ฉ์ธ `App` ์ปดํฌ๋ํธ์ `div`์ `onClick` ๋ฆฌ์ค๋๊ฐ ์๊ณ , ๊ทธ `div`์ ๋ ผ๋ฆฌ์ ์์์ธ ํฌํ ๋ด๋ถ์ ๋ฒํผ์ ๋ ๋๋งํ๋ค๋ฉด, ๋ฒํผ์ ํด๋ฆญํด๋ ๋ค์ดํฐ๋ธ DOM ๋ฒ๋ธ๋ง์ ํตํด `div`์ `onClick` ํธ๋ค๋ฌ๊ฐ ํธ๋ฆฌ๊ฑฐ๋์ง *์์ต๋๋ค*.
ํ์ง๋ง, ์ฌ๊ธฐ์ ์ค์ํ ์ฐจ์ด์ ์ด ์์ต๋๋ค: ๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ ์ด ๊ฐ๊ทน์ ์ฐ๊ฒฐํฉ๋๋ค. ๋ค์ดํฐ๋ธ ์ด๋ฒคํธ๊ฐ ํฌํ์์ ๋ฐ์ํ๋ฉด, ๋ฆฌ์กํธ์ ๋ด๋ถ ๋ฉ์ปค๋์ฆ์ ํฉ์ฑ ์ด๋ฒคํธ๊ฐ ๋ ผ๋ฆฌ์ ๋ถ๋ชจ์๊ฒ๋ก *๋ฆฌ์กํธ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ํตํด ์ฌ์ ํ ๋ฒ๋ธ๋ง๋๋๋ก* ๋ณด์ฅํฉ๋๋ค. ์ด๋ ํฌํ์ ๋ ผ๋ฆฌ์ ์ผ๋ก ํฌํจํ๋ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ์ `onClick` ํธ๋ค๋ฌ๊ฐ ์๋ค๋ฉด, ํฌํ ๋ด๋ถ์์์ ํด๋ฆญ์ด ํด๋น ํธ๋ค๋ฌ๋ฅผ *ํธ๋ฆฌ๊ฑฐํ ๊ฒ*์์ ์๋ฏธํฉ๋๋ค. ์ด๊ฒ์ด ๋ฆฌ์กํธ ์ด๋ฒคํธ ์์คํ ์ ๊ทผ๋ณธ์ ์ธ ์ธก๋ฉด์ด๋ฉฐ, ํฌํ์์์ ์ด๋ฒคํธ ์์์ ๊ฐ๋ฅํ๊ฒ ํ ๋ฟ๋ง ์๋๋ผ ๊ถ์ฅ๋๋ ์ ๊ทผ ๋ฐฉ์์ผ๋ก ๋ง๋๋ ์ด์ ์ ๋๋ค.
ํด๊ฒฐ์ฑ : ์ด๋ฒคํธ ์์ ์์ธ ์ค๋ช
์ด๋ฒคํธ ์์์ ์ฌ๋ฌ ํ์ ์์์ ๊ฐ๋ณ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ด๋ ๋์ , ๊ณตํต ์กฐ์ ์์์ ๋จ์ผ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ฌ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ๋์์ธ ํจํด์ ๋๋ค. ํ์ ์์์์ ์ด๋ฒคํธ(์: ํด๋ฆญ)๊ฐ ๋ฐ์ํ๋ฉด, DOM ํธ๋ฆฌ๋ฅผ ๋ฐ๋ผ ๋ฒ๋ธ๋ง๋์ด ์์๋ ๋ฆฌ์ค๋๊ฐ ์๋ ์กฐ์์ ๋๋ฌํฉ๋๋ค. ๊ทธ๋ฌ๋ฉด ๋ฆฌ์ค๋๋ `event.target` ์์ฑ์ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ ํน์ ์์๋ฅผ ์๋ณํ๊ณ ๊ทธ์ ๋ฐ๋ผ ๋ฐ์ํฉ๋๋ค.
์ด๋ฒคํธ ์์์ ์ฃผ์ ์ฅ์
- ์ฑ๋ฅ ์ต์ ํ: ์๋ง์ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋์ ๋จ ํ๋๋ง ์ฌ์ฉํฉ๋๋ค. ์ด๋ ๋ฉ๋ชจ๋ฆฌ ์๋น์ ์ค์ ์๊ฐ์ ์ค์ฌ์ฃผ๋ฉฐ, ํนํ ๋ง์ ์ํธ์์ฉ ์์๊ฐ ์๋ ๋ณต์กํ UI๋ ๋ฆฌ์์ค ํจ์จ์ฑ์ด ์ค์ํ ๊ธ๋ก๋ฒ ๋ฐฐํฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ๋ฆฌํฉ๋๋ค.
- ๋์ ์ฝํ ์ธ ์ฒ๋ฆฌ: ์ด๊ธฐ ๋ ๋๋ง ์ดํ DOM์ ์ถ๊ฐ๋ ์์(์: AJAX ์์ฒญ์ด๋ ์ฌ์ฉ์ ์ํธ์์ฉ์ ํตํด)๋ ์๋ก์ด ๋ฆฌ์ค๋๋ฅผ ๋ถ์ผ ํ์ ์์ด ์๋์ผ๋ก ์์๋ ๋ฆฌ์ค๋์ ํํ์ ๋ฐ์ต๋๋ค. ์ด๋ ๋์ ์ผ๋ก ๋ ๋๋ง๋๋ ํฌํ ์ฝํ ์ธ ์ ์๋ฒฝํ๊ฒ ์ ํฉํฉ๋๋ค.
- ๋ ๊น๋ํ ์ฝ๋: ์ด๋ฒคํธ ๋ก์ง์ ์ค์ ์ง์คํํ๋ฉด ์ฝ๋๋ฒ ์ด์ค๊ฐ ๋ ์ฒด๊ณ์ ์ด๊ณ ์ ์ง ๊ด๋ฆฌ๊ฐ ์ฌ์์ง๋๋ค.
- DOM ๊ตฌ์กฐ ์ ๋ฐ์ ๊ฒฌ๊ณ ํจ: ์ฐ๋ฆฌ๊ฐ ๋ ผ์ํ๋ฏ์ด, ๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ ํฌํ ์ฝํ ์ธ ์์ ๋ฐ์ํ ์ด๋ฒคํธ๊ฐ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ํตํด ๋ ผ๋ฆฌ์ ์กฐ์์ผ๋ก *์ฌ์ ํ* ๋ฒ๋ธ๋ง๋๋๋ก ๋ณด์ฅํฉ๋๋ค. ์ด๊ฒ์ด ๋ฌผ๋ฆฌ์ DOM ์์น๊ฐ ๋ค๋ฅด๋๋ผ๋ ํฌํ์ ๋ํ ์ด๋ฒคํธ ์์์ ํจ๊ณผ์ ์ธ ์ ๋ต์ผ๋ก ๋ง๋๋ ์ด์์ ๋๋ค.
์ด๋ฒคํธ ๋ฒ๋ธ๋ง๊ณผ ์บก์ฒ ์ค๋ช
์ด๋ฒคํธ ์์์ ์์ ํ ์ดํดํ๋ ค๋ฉด, DOM์์์ ์ด๋ฒคํธ ์ ํ์ ๋ ๋จ๊ณ๋ฅผ ์ดํดํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค:
- ์บก์ฒ๋ง ๋จ๊ณ (๋ด๋ ค๊ฐ๊ธฐ): ์ด๋ฒคํธ๋ `document` ๋ฃจํธ์์ ์์ํ์ฌ DOM ํธ๋ฆฌ๋ฅผ ๋ฐ๋ผ ๋ด๋ ค๊ฐ๋ฉฐ, ๋์ ์์์ ๋๋ฌํ ๋๊น์ง ๊ฐ ์กฐ์ ์์๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค. `useCapture = true`๋ก ๋ฑ๋ก๋ ๋ฆฌ์ค๋(๋๋ ๋ฆฌ์กํธ์์๋ `Capture` ์ ๋ฏธ์ฌ๋ฅผ ์ถ๊ฐ, ์: `onClickCapture`)๋ ์ด ๋จ๊ณ์์ ์คํ๋ฉ๋๋ค.
- ๋ฒ๋ธ๋ง ๋จ๊ณ (์ฌ๋ผ๊ฐ๊ธฐ): ๋์ ์์์ ๋๋ฌํ ํ, ์ด๋ฒคํธ๋ ๋ค์ DOM ํธ๋ฆฌ๋ฅผ ๋ฐ๋ผ ๋์ ์์์์ `document` ๋ฃจํธ๊น์ง ์ฌ๋ผ๊ฐ๋ฉฐ ๊ฐ ์กฐ์ ์์๋ฅผ ๋ฐฉ๋ฌธํฉ๋๋ค. ๋ชจ๋ ํ์ค ๋ฆฌ์กํธ `onClick`, `onChange` ๋ฑ์ ํฌํจํ ๋๋ถ๋ถ์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ ์ด ๋จ๊ณ์์ ์คํ๋ฉ๋๋ค.
๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ ์ฃผ๋ก ๋ฒ๋ธ๋ง ๋จ๊ณ๋ฅผ ์ฌ์ฉํฉ๋๋ค. ํฌํ ๋ด ์์์์ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด, ๋ค์ดํฐ๋ธ ๋ธ๋ผ์ฐ์ ์ด๋ฒคํธ๋ ๋ฌผ๋ฆฌ์ DOM ๊ฒฝ๋ก๋ฅผ ๋ฐ๋ผ ๋ฒ๋ธ๋ง๋ฉ๋๋ค. ๋ฆฌ์กํธ์ ๋ฃจํธ ๋ฆฌ์ค๋(๋ณดํต `document`์ ์์)๊ฐ ์ด ๋ค์ดํฐ๋ธ ์ด๋ฒคํธ๋ฅผ ์บก์ฒํฉ๋๋ค. ๊ฒฐ์ ์ ์ผ๋ก, ๋ฆฌ์กํธ๋ ์ด๋ฒคํธ๋ฅผ ์ฌ๊ตฌ์ฑํ๊ณ *ํฉ์ฑ* ๋์๋ฌผ์ ๋์คํจ์นํ๋๋ฐ, ์ด๋ ํฌํ ๋ด ์ปดํฌ๋ํธ์์ ๋ ผ๋ฆฌ์ ๋ถ๋ชจ ์ปดํฌ๋ํธ๋ก *๋ฆฌ์กํธ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ๋ฐ๋ผ ๋ฒ๋ธ๋ง๋๋ ๊ฒ์ ์๋ฎฌ๋ ์ด์ *ํฉ๋๋ค. ์ด ์๋ฆฌํ ์ถ์ํ๋ ๋ถ๋ฆฌ๋ ๋ฌผ๋ฆฌ์ DOM ์กด์ฌ์๋ ๋ถ๊ตฌํ๊ณ ํฌํ์์ ์ด๋ฒคํธ ์์์ด ์ํํ๊ฒ ์๋ํ๋๋ก ๋ณด์ฅํฉ๋๋ค.
๋ฆฌ์กํธ ํฌํ๋ก ์ด๋ฒคํธ ์์ ๊ตฌํํ๊ธฐ
์ฌ์ฉ์๊ฐ ์ฝํ ์ธ ์์ญ ๋ฐ๊นฅ(๋ฐฑ๋๋กญ)์ ํด๋ฆญํ๊ฑฐ๋ `Escape` ํค๋ฅผ ๋๋ฅด๋ฉด ๋ซํ๋ ๋ชจ๋ฌ ๋ํ ์์๋ผ๋ ์ผ๋ฐ์ ์ธ ์๋๋ฆฌ์ค๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค. ์ด๋ ํฌํ์ ์ ํ์ ์ธ ์ฌ์ฉ ์ฌ๋ก์ด๋ฉฐ ์ด๋ฒคํธ ์์์ ํ๋ฅญํ ์์ฐ์ ๋๋ค.
์๋๋ฆฌ์ค: ์ธ๋ถ ํด๋ฆญ ์ ๋ซํ๋ ๋ชจ๋ฌ
๋ฆฌ์กํธ ํฌํ์ ์ฌ์ฉํ์ฌ ๋ชจ๋ฌ ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ๋ ค๊ณ ํฉ๋๋ค. ๋ชจ๋ฌ์ ๋ฒํผ์ ํด๋ฆญํ๋ฉด ๋ํ๋์ผ ํ๊ณ , ๋ค์๊ณผ ๊ฐ์ ๊ฒฝ์ฐ์ ๋ซํ์ผ ํฉ๋๋ค:
- ์ฌ์ฉ์๊ฐ ๋ชจ๋ฌ ์ฝํ ์ธ ๋ฅผ ๋๋ฌ์ผ ๋ฐํฌ๋ช ์ค๋ฒ๋ ์ด(๋ฐฑ๋๋กญ)๋ฅผ ํด๋ฆญํ ๋.
- ์ฌ์ฉ์๊ฐ `Escape` ํค๋ฅผ ๋๋ฅผ ๋.
- ์ฌ์ฉ์๊ฐ ๋ชจ๋ฌ ๋ด์ ๋ช ์์ ์ธ "๋ซ๊ธฐ" ๋ฒํผ์ ํด๋ฆญํ ๋.
๋จ๊ณ๋ณ ๊ตฌํ
1๋จ๊ณ: HTML ๋ฐ ํฌํ ์ปดํฌ๋ํธ ์ค๋น
`index.html`์ ํฌํ์ ์ํ ์ ์ฉ ๋ฃจํธ๊ฐ ์๋์ง ํ์ธํ์ธ์. ์ด ์์ ์์๋ `id="portal-root"`๋ฅผ ์ฌ์ฉํ๊ฒ ์ต๋๋ค.
// public/index.html (์ผ๋ถ)
<body>
<div id="root"></div>
<div id="portal-root"></div> <!-- ํฌํ์ ๋์ -->
</body>
๋ค์์ผ๋ก, `ReactDOM.createPortal` ๋ก์ง์ ์บก์ํํ ๊ฐ๋จํ `Portal` ์ปดํฌ๋ํธ๋ฅผ ๋ง๋ญ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ชจ๋ฌ ์ปดํฌ๋ํธ๊ฐ ๋ ๊น๋ํด์ง๋๋ค.
// components/Portal.js
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
interface PortalProps {
children: React.ReactNode;
wrapperId?: string;
}
// wrapperId์ ํด๋นํ๋ ํฌํ div๊ฐ ์์ผ๋ฉด ์๋ก ์์ฑํฉ๋๋ค
function createWrapperAndAppendToBody(wrapperId: string) {
const wrapperElement = document.createElement('div');
wrapperElement.setAttribute('id', wrapperId);
document.body.appendChild(wrapperElement);
return wrapperElement;
}
function Portal({ children, wrapperId = 'portal-wrapper' }: PortalProps) {
const [wrapperElement, setWrapperElement] = useState<HTMLElement | null>(null);
useEffect(() => {
let element = document.getElementById(wrapperId) as HTMLElement;
let created = false;
if (!element) {
created = true;
element = createWrapperAndAppendToBody(wrapperId);
}
setWrapperElement(element);
return () => {
// ์ฐ๋ฆฌ๊ฐ ์ง์ ์์ฑํ ์์๋ผ๋ฉด ์ ๋ฆฌํฉ๋๋ค
if (created && element.parentNode) {
element.parentNode.removeChild(element);
}
};
}, [wrapperId]);
// wrapperElement๋ ์ฒซ ๋ ๋๋ง ์ null์
๋๋ค. ์๋ฌด๊ฒ๋ ๋ ๋๋งํ์ง ์์ผ๋ฏ๋ก ๊ด์ฐฎ์ต๋๋ค.
if (!wrapperElement) return null;
return createPortal(children, wrapperElement);
}
export default Portal;
์ฐธ๊ณ : ๋จ์ํ๋ฅผ ์ํด ์ด์ ์์ ์์๋ `portal-root`๋ฅผ `index.html`์ ํ๋์ฝ๋ฉํ์ต๋๋ค. ์ด `Portal.js` ์ปดํฌ๋ํธ๋ ๋ํผ div๊ฐ ์์ผ๋ฉด ์์ฑํ๋ ๋ ๋์ ์ธ ์ ๊ทผ ๋ฐฉ์์ ์ ๊ณตํฉ๋๋ค. ํ๋ก์ ํธ์ ํ์์ ๊ฐ์ฅ ์ ํฉํ ๋ฐฉ๋ฒ์ ์ ํํ์ธ์. ๋ช ํ์ฑ์ ์ํด `Modal` ์ปดํฌ๋ํธ์์๋ `index.html`์ ์ง์ ๋ `portal-root`๋ฅผ ๊ณ์ ์ฌ์ฉํ๊ฒ ์ง๋ง, ์์ `Portal.js`๋ ๊ฐ๋ ฅํ ๋์์ ๋๋ค.
2๋จ๊ณ: ๋ชจ๋ฌ ์ปดํฌ๋ํธ ์์ฑ
์ฐ๋ฆฌ์ `Modal` ์ปดํฌ๋ํธ๋ `children`์ผ๋ก ์ฝํ ์ธ ๋ฅผ ๋ฐ๊ณ `onClose` ์ฝ๋ฐฑ์ ๋ฐ์ต๋๋ค.
// components/Modal.js
import React, { useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
const modalRoot = document.getElementById('portal-root') as HTMLElement;
const Modal = ({ isOpen, onClose, children }: ModalProps) => {
const modalContentRef = useRef<HTMLDivElement>(null);
if (!isOpen) return null;
// Escape ํค ๋๋ฆ ์ฒ๋ฆฌ
useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
// ์ด๋ฒคํธ ์์์ ํต์ฌ: ๋ฐฑ๋๋กญ์ ๋จ์ผ ํด๋ฆญ ํธ๋ค๋ฌ๋ฅผ ์ค์ ํฉ๋๋ค.
// ์ด๋ ๋ชจ๋ฌ ๋ด๋ถ์ ๋ซ๊ธฐ ๋ฒํผ์๋ ์๋ฌต์ ์ผ๋ก ์์๋ฉ๋๋ค.
const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
// ํด๋ฆญ ๋์์ด ๋ชจ๋ฌ ๋ด๋ถ ์ฝํ
์ธ ๊ฐ ์๋ ๋ฐฑ๋๋กญ ์์ฒด์ธ์ง ํ์ธํฉ๋๋ค.
// ์ฌ๊ธฐ์ `modalContentRef.current.contains(event.target)`๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
// event.target์ ํด๋ฆญ์ด ์์๋ ์์์
๋๋ค.
// event.currentTarget์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ์ฐ๊ฒฐ๋ ์์(modal-overlay)์
๋๋ค.
if (modalContentRef.current && !modalContentRef.current.contains(event.target as Node)) {
onClose();
}
};
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={handleBackdropClick}>
<div className="modal-content" ref={modalContentRef}>
{children}
<button onClick={onClose} aria-label="Close modal">X</button>
</div>
</div>,
modalRoot
);
};
export default Modal;
3๋จ๊ณ: ๋ฉ์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ปดํฌ๋ํธ์ ํตํฉ
๋ฉ์ธ `App` ์ปดํฌ๋ํธ๋ ๋ชจ๋ฌ์ ์ด๋ฆผ/๋ซํ ์ํ๋ฅผ ๊ด๋ฆฌํ๊ณ `Modal`์ ๋ ๋๋งํฉ๋๋ค.
// App.js
import React, { useState } from 'react';
import Modal from './components/Modal';
import './App.css'; // ๊ธฐ๋ณธ ์คํ์ผ๋ง์ฉ
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => setIsModalOpen(true);
const closeModal = () => setIsModalOpen(false);
return (
<div className="App">
<h1>React Portal Event Delegation Example</h1>
<p>Demonstrating event handling across different DOM trees.</p>
<button onClick={openModal}>Open Modal</button>
<Modal isOpen={isModalOpen} onClose={closeModal}>
<h2>Welcome to the Modal!</h2>
<p>This content is rendered in a React Portal, outside the main application's DOM hierarchy.</p>
<button onClick={closeModal}>Close from inside</button>
</Modal>
<p>Some other content behind the modal.</p>
<p>Another paragraph to show the background.</p>
</div>
);
}
export default App;
4๋จ๊ณ: ๊ธฐ๋ณธ ์คํ์ผ๋ง (App.css)
๋ชจ๋ฌ๊ณผ ๋ฐฑ๋๋กญ์ ์๊ฐํํ๊ธฐ ์ํจ์ ๋๋ค.
/* App.css */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 8px;
min-width: 300px;
max-width: 80%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
position: relative; /* Needed for internal button positioning if any */
}
.modal-content button {
margin-top: 15px;
padding: 8px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.modal-content button:hover {
background-color: #0056b3;
}
.modal-content > button:last-child { /* 'X' ๋ซ๊ธฐ ๋ฒํผ ์คํ์ผ */
position: absolute;
top: 10px;
right: 10px;
background: none;
color: #333;
font-size: 1.2rem;
padding: 0;
margin: 0;
border: none;
}
.App {
font-family: Arial, sans-serif;
padding: 20px;
text-align: center;
}
.App button {
padding: 10px 20px;
font-size: 1.1rem;
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.App button:hover {
background-color: #218838;
}
์์ ๋ก์ง ์ค๋ช
์ฐ๋ฆฌ์ `Modal` ์ปดํฌ๋ํธ์์, `onClick={handleBackdropClick}`๋ ์์ ๋ฆฌ์ค๋ ์ญํ ์ ํ๋ `.modal-overlay` div์ ์ฐ๊ฒฐ๋ฉ๋๋ค. ์ด ์ค๋ฒ๋ ์ด ๋ด์์ ํด๋ฆญ์ด ๋ฐ์ํ๋ฉด (`modal-content`์ ๊ทธ ์์ `X` ๋ซ๊ธฐ ๋ฒํผ, ๊ทธ๋ฆฌ๊ณ '๋ด๋ถ์์ ๋ซ๊ธฐ' ๋ฒํผ ํฌํจ), `handleBackdropClick` ํจ์๊ฐ ์คํ๋ฉ๋๋ค.
`handleBackdropClick` ๋ด๋ถ:
- `event.target`์ *์ค์ ๋ก ํด๋ฆญ๋* ํน์ DOM ์์๋ฅผ ๊ฐ๋ฆฌํต๋๋ค (์: `modal-content` ๋ด๋ถ์ `<h2>`, `<p>` ๋๋ `<button>` ๋๋ `modal-overlay` ์์ฒด).
- `event.currentTarget`์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ์ฐ๊ฒฐ๋ ์์, ์ด ๊ฒฝ์ฐ์๋ `.modal-overlay` div๋ฅผ ๊ฐ๋ฆฌํต๋๋ค.
- `!modalContentRef.current.contains(event.target as Node)` ์กฐ๊ฑด์ด ์ฐ๋ฆฌ ์์์ ํต์ฌ์ ๋๋ค. ์ด๋ ํด๋ฆญ๋ ์์(`event.target`)๊ฐ `modal-content` div์ ํ์ ์์๊ฐ *์๋์ง* ํ์ธํฉ๋๋ค. `event.target`์ด `.modal-overlay` ์์ฒด์ด๊ฑฐ๋ ์ค๋ฒ๋ ์ด์ ์ง๊ณ ์์์ด์ง๋ง `modal-content`์ ์ผ๋ถ๊ฐ ์๋ ๋ค๋ฅธ ์์์ธ ๊ฒฝ์ฐ, `contains`๋ `false`๋ฅผ ๋ฐํํ๊ณ ๋ชจ๋ฌ์ด ๋ซํ๋๋ค.
- ๊ฒฐ์ ์ ์ผ๋ก, ๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ `event.target`์ด `portal-root`์ ๋ฌผ๋ฆฌ์ ์ผ๋ก ๋ ๋๋ง๋ ์์์ผ์ง๋ผ๋, ๋ ผ๋ฆฌ์ ๋ถ๋ชจ(`Modal` ์ปดํฌ๋ํธ์ `.modal-overlay`)์ `onClick` ํธ๋ค๋ฌ๊ฐ ์ฌ์ ํ ํธ๋ฆฌ๊ฑฐ๋๊ณ `event.target`์ด ๊น์ด ์ค์ฒฉ๋ ์์๋ฅผ ์ ํํ๊ฒ ์๋ณํ๋๋ก ๋ณด์ฅํฉ๋๋ค.
๋ด๋ถ ๋ซ๊ธฐ ๋ฒํผ์ ๊ฒฝ์ฐ, ๋จ์ํ ๊ทธ๋ค์ `onClick` ํธ๋ค๋ฌ์์ `onClose()`๋ฅผ ์ง์ ํธ์ถํ๋ ๊ฒ์ด ์๋ํฉ๋๋ค. ์๋ํ๋ฉด ์ด ํธ๋ค๋ฌ๋ค์ ์ด๋ฒคํธ๊ฐ `modal-overlay`์ ์์๋ ๋ฆฌ์ค๋๋ก ๋ฒ๋ธ๋ง๋๊ธฐ ์ ์ ์คํ๋๊ฑฐ๋ ๋ช ์์ ์ผ๋ก ์ฒ๋ฆฌ๋๊ธฐ ๋๋ฌธ์ ๋๋ค. ๋ฒ๋ธ๋ง๋๋๋ผ๋, ์ฐ๋ฆฌ์ `contains()` ๊ฒ์ฌ๋ ํด๋ฆญ์ด ์ฝํ ์ธ ๋ด๋ถ์์ ์์๋์๋ค๋ฉด ๋ชจ๋ฌ์ด ๋ซํ๋ ๊ฒ์ ๋ฐฉ์งํ ๊ฒ์ ๋๋ค.
`Escape` ํค ๋ฆฌ์ค๋๋ฅผ ์ํ `useEffect`๋ `document`์ ์ง์ ์ฐ๊ฒฐ๋๋๋ฐ, ์ด๋ ๊ธ๋ก๋ฒ ํค๋ณด๋ ๋จ์ถํค์ ๋ํ ์ผ๋ฐ์ ์ด๊ณ ํจ๊ณผ์ ์ธ ํจํด์ ๋๋ค. ์๋ํ๋ฉด ์ปดํฌ๋ํธ ํฌ์ปค์ค์ ๊ด๊ณ์์ด ๋ฆฌ์ค๋๊ฐ ํ์ฑํ๋๋๋ก ๋ณด์ฅํ๊ณ , ํฌํ ๋ด๋ถ์์ ๋ฐ์ํ ์ด๋ฒคํธ๋ฅผ ํฌํจํ์ฌ DOM์ ์ด๋ ๊ณณ์์๋ ์ด๋ฒคํธ๋ฅผ ์ก์ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋๋ค.
์ผ๋ฐ์ ์ธ ์ด๋ฒคํธ ์์ ์๋๋ฆฌ์ค ํด๊ฒฐ
์์น ์๋ ์ด๋ฒคํธ ์ ํ ๋ฐฉ์ง: `event.stopPropagation()`
๋๋ก๋ ์์์ ์ฌ์ฉํ๋๋ผ๋, ์์๋ ์์ญ ๋ด ํน์ ์์์์ ์ด๋ฒคํธ๊ฐ ๋ ์ด์ ๋ฒ๋ธ๋ง๋๋ ๊ฒ์ ๋ช ์์ ์ผ๋ก ๋ง๊ณ ์ถ์ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ๋ชจ๋ฌ ์ฝํ ์ธ ๋ด์ ์ค์ฒฉ๋ ์ํธ์์ฉ ์์๊ฐ ์๋๋ฐ, ์ด๋ฅผ ํด๋ฆญํ์ ๋ `onClose` ๋ก์ง์ด ํธ๋ฆฌ๊ฑฐ๋์ด์๋ ์ ๋๋ ๊ฒฝ์ฐ(`contains` ๊ฒ์ฌ๊ฐ ์ด๋ฏธ ์ฒ๋ฆฌํ๋๋ผ๋), `event.stopPropagation()`์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
<div className="modal-content" ref={modalContentRef}>
<h2>Modal Content</h2>
<p>Clicking this area will not close the modal.</p>
<button onClick={(e) => {
e.stopPropagation(); // ์ด ํด๋ฆญ์ด ๋ฐฑ๋๋กญ์ผ๋ก ๋ฒ๋ธ๋ง๋๋ ๊ฒ์ ๋ง์ต๋๋ค
console.log('Inner button clicked!');
}}>Inner Action Button</button>
<button onClick={onClose}>Close</button>
</div>
`event.stopPropagation()`์ ์ ์ฉํ ์ ์์ง๋ง, ์ ์คํ๊ฒ ์ฌ์ฉํด์ผ ํฉ๋๋ค. ๊ณผ์ฉํ๋ฉด ์ด๋ฒคํธ ํ๋ฆ์ ์์ธกํ ์ ์๊ฒ ๋ง๋ค๊ณ ๋๋ฒ๊น ์ ์ด๋ ต๊ฒ ๋ง๋ค ์ ์์ผ๋ฉฐ, ํนํ ๋ค๋ฅธ ํ๋ค์ด UI์ ๊ธฐ์ฌํ ์ ์๋ ํฌ๊ณ ์ ์ธ๊ณ์ ์ผ๋ก ๋ฐฐํฌ๋ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๋์ฑ ๊ทธ๋ ์ต๋๋ค.
์์์ผ๋ก ํน์ ์์ ์์ ์ฒ๋ฆฌํ๊ธฐ
๋จ์ํ ํด๋ฆญ์ด ๋ด๋ถ์ธ์ง ์ธ๋ถ์ธ์ง ํ์ธํ๋ ๊ฒ ์ด์์ผ๋ก, ์ด๋ฒคํธ ์์์ ์์๋ ์์ญ ๋ด์์ ๋ค์ํ ์ ํ์ ํด๋ฆญ์ ๊ตฌ๋ณํ ์ ์๊ฒ ํด์ค๋๋ค. `event.target.tagName`, `event.target.id`, `event.target.className` ๋๋ `event.target.dataset` ์์ฑ๊ณผ ๊ฐ์ ์์ฑ์ ์ฌ์ฉํ์ฌ ๋ค๋ฅธ ์์ ์ ์ํํ ์ ์์ต๋๋ค.
const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (modalContentRef.current && modalContentRef.current.contains(event.target as Node)) {
// ํด๋ฆญ์ด ๋ชจ๋ฌ ์ฝํ
์ธ ๋ด๋ถ์์ ๋ฐ์
const clickedElement = event.target as HTMLElement;
if (clickedElement.tagName === 'BUTTON' && clickedElement.dataset.action === 'confirm') {
console.log('Confirm action triggered!');
onClose();
} else if (clickedElement.tagName === 'A') {
console.log('Link inside modal clicked:', clickedElement.href);
// ๊ธฐ๋ณธ ๋์์ ๋ง๊ฑฐ๋ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ํ์ํ ์ ์์
}
// ๋ชจ๋ฌ ๋ด๋ถ์ ๋ค๋ฅธ ํน์ ์์ ํธ๋ค๋ฌ
} else {
// ํด๋ฆญ์ด ๋ชจ๋ฌ ์ฝํ
์ธ ์ธ๋ถ(๋ฐฑ๋๋กญ)์์ ๋ฐ์
onClose();
}
};
์ด ํจํด์ ๋จ์ผ์ ํจ์จ์ ์ธ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ์ฌ์ฉํ์ฌ ํฌํ ์ฝํ ์ธ ๋ด์ ์ฌ๋ฌ ์ํธ์์ฉ ์์๋ฅผ ๊ด๋ฆฌํ๋ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค.
์ธ์ ์์ํ์ง ๋ง์์ผ ํ๋๊ฐ
์ด๋ฒคํธ ์์์ด ํฌํ์ ๋งค์ฐ ๊ถ์ฅ๋์ง๋ง, ์์ ์์ฒด์ ์ง์ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋๋ ๊ฒ์ด ๋ ์ ์ ํ ์๋๋ฆฌ์ค๋ ์์ต๋๋ค:
- ๋งค์ฐ ํน์ ํ ์ปดํฌ๋ํธ ๋์: ์ปดํฌ๋ํธ๊ฐ ์กฐ์์ ์์๋ ํธ๋ค๋ฌ์ ์ํธ์์ฉํ ํ์๊ฐ ์๋ ๋งค์ฐ ์ ๋ฌธํ๋๊ณ ๋ ๋ฆฝ์ ์ธ ์ด๋ฒคํธ ๋ก์ง์ ๊ฐ์ง๊ณ ์์ ๋.
- `onChange`๊ฐ ์๋ ์ ๋ ฅ ์์: ํ ์คํธ ์ ๋ ฅ๊ณผ ๊ฐ์ ์ ์ด๋ ์ปดํฌ๋ํธ์ ๊ฒฝ์ฐ, `onChange` ๋ฆฌ์ค๋๋ ์ฆ๊ฐ์ ์ธ ์ํ ์ ๋ฐ์ดํธ๋ฅผ ์ํด ์ผ๋ฐ์ ์ผ๋ก ์ ๋ ฅ ์์์ ์ง์ ๋ฐฐ์น๋ฉ๋๋ค. ์ด๋ฌํ ์ด๋ฒคํธ๋ ๋ฒ๋ธ๋ง๋์ง๋ง, ์ง์ ์ฒ๋ฆฌํ๋ ๊ฒ์ด ํ์ค ๊ดํ์ ๋๋ค.
- ์ฑ๋ฅ์ด ์ค์ํ๊ณ ๋น๋ฒํ ์ด๋ฒคํธ: `mousemove`๋ `scroll`๊ณผ ๊ฐ์ด ๋งค์ฐ ๋น๋ฒํ๊ฒ ๋ฐ์ํ๋ ์ด๋ฒคํธ์ ๊ฒฝ์ฐ, ๋ฉ๋ฆฌ ๋จ์ด์ง ์กฐ์์๊ฒ ์์ํ๋ฉด `event.target`์ ๋ฐ๋ณต์ ์ผ๋ก ํ์ธํ๋ ์ฝ๊ฐ์ ์ค๋ฒํค๋๊ฐ ๋ฐ์ํ ์ ์์ต๋๋ค. ๊ทธ๋ฌ๋ ๋๋ถ๋ถ์ UI ์ํธ์์ฉ(ํด๋ฆญ, ํค๋ค์ด)์์๋ ์์์ ์ด์ ์ด ์ด ๋ฏธ๋ฏธํ ๋น์ฉ์ ํจ์ฌ ๋ฅ๊ฐํฉ๋๋ค.
๊ณ ๊ธ ํจํด ๋ฐ ๊ณ ๋ ค ์ฌํญ
๋ ๋ณต์กํ ์ ํ๋ฆฌ์ผ์ด์ , ํนํ ๋ค์ํ ๊ธ๋ก๋ฒ ์ฌ์ฉ์ ๊ธฐ๋ฐ์ ๋์์ผ๋ก ํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฒฝ์ฐ, ํฌํ ๋ด ์ด๋ฒคํธ ํธ๋ค๋ง์ ๊ด๋ฆฌํ๊ธฐ ์ํ ๊ณ ๊ธ ํจํด์ ๊ณ ๋ คํ ์ ์์ต๋๋ค.
์ฌ์ฉ์ ์ ์ ์ด๋ฒคํธ ๋์คํจ์นญ
๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ด ์ฌ์ฉ์์ ์๊ตฌ์ ์๋ฒฝํ๊ฒ ์ผ์นํ์ง ์๋ ๋งค์ฐ ํน์ ํ ๊ทน๋จ์ ์ธ ๊ฒฝ์ฐ(๋๋ฌผ์ง๋ง), ์๋์ผ๋ก ์ฌ์ฉ์ ์ ์ ์ด๋ฒคํธ๋ฅผ ๋์คํจ์นํ ์ ์์ต๋๋ค. ์ด๋ `CustomEvent` ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ ๋์ ์์์์ ๋์คํจ์นํ๋ ๊ฒ์ ํฌํจํฉ๋๋ค. ๊ทธ๋ฌ๋ ์ด๋ ์ข ์ข ๋ฆฌ์กํธ์ ์ต์ ํ๋ ์ด๋ฒคํธ ์์คํ ์ ์ฐํํ๋ฉฐ, ์ ์ง ๊ด๋ฆฌ ๋ณต์ก์ฑ์ ์ด๋ํ ์ ์์ผ๋ฏ๋ก ์ ์คํ๊ฒ ๊ทธ๋ฆฌ๊ณ ๊ผญ ํ์ํ ๊ฒฝ์ฐ์๋ง ์ฌ์ฉํด์ผ ํฉ๋๋ค.
// ํฌํ ์ปดํฌ๋ํธ ๋ด๋ถ
const handleCustomAction = () => {
const event = new CustomEvent('my-custom-portal-event', { detail: { data: 'some info' }, bubbles: true });
document.dispatchEvent(event);
};
// ๋ฉ์ธ ์ฑ ์ด๋๊ฐ์์ (์: effect ํ
๋ด๋ถ)
useEffect(() => {
const handler = (event: Event) => {
if (event instanceof CustomEvent) {
console.log('Custom event received:', event.detail);
}
};
document.addEventListener('my-custom-portal-event', handler);
return () => document.removeEventListener('my-custom-portal-event', handler);
}, []);
์ด ์ ๊ทผ ๋ฐฉ์์ ์ธ๋ถํ๋ ์ ์ด๋ฅผ ์ ๊ณตํ์ง๋ง ์ด๋ฒคํธ ์ ํ๊ณผ ํ์ด๋ก๋์ ์ ์คํ ๊ด๋ฆฌ๊ฐ ํ์ํฉ๋๋ค.
์ด๋ฒคํธ ํธ๋ค๋ฌ๋ฅผ ์ํ ์ปจํ ์คํธ API
๊น์ด ์ค์ฒฉ๋ ํฌํ ์ฝํ ์ธ ๊ฐ ์๋ ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ฒฝ์ฐ, `onClose`๋ ๋ค๋ฅธ ํธ๋ค๋ฌ๋ฅผ props๋ฅผ ํตํด ์ ๋ฌํ๋ฉด prop ๋๋ฆด๋ง์ด ๋ฐ์ํ ์ ์์ต๋๋ค. ๋ฆฌ์กํธ์ ์ปจํ ์คํธ API๋ ์ฐ์ํ ํด๊ฒฐ์ฑ ์ ์ ๊ณตํฉ๋๋ค:
// context/ModalContext.js
import React, { createContext, useContext } from 'react';
interface ModalContextType {
onClose?: () => void;
// ํ์์ ๋ฐ๋ผ ๋ค๋ฅธ ๋ชจ๋ฌ ๊ด๋ จ ํธ๋ค๋ฌ ์ถ๊ฐ
}
const ModalContext = createContext<ModalContextType>({});
export const useModal = () => useContext(ModalContext);
export const ModalProvider = ({ children, onClose }: ModalContextType & React.PropsWithChildren) => (
<ModalContext.Provider value={{ onClose }}>
{children}
</ModalContext.Provider>
);
// components/Modal.js (์ปจํ
์คํธ๋ฅผ ์ฌ์ฉํ๋๋ก ์
๋ฐ์ดํธ๋จ)
// ... (์ํฌํธ ๋ฐ modalRoot ์ ์)
const Modal = ({ isOpen, onClose, children }: ModalProps) => {
const modalContentRef = useRef<HTMLDivElement>(null);
// ... (Escape ํค๋ฅผ ์ํ useEffect, handleBackdropClick์ ๊ฑฐ์ ๋์ผํ๊ฒ ์ ์ง)
if (!isOpen) return null;
return ReactDOM.createPortal(
<div className="modal-overlay" onClick={handleBackdropClick}>
<div className="modal-content" ref={modalContentRef}>
<ModalProvider onClose={onClose}>{children}</ModalProvider> <!-- ์ปจํ
์คํธ ์ ๊ณต -->
<button onClick={onClose} aria-label="Close modal">X</button>
</div>
</div>,
modalRoot
);
};
export default Modal;
// components/DeeplyNestedComponent.js (๋ชจ๋ฌ ์์ ์ด๋๊ฐ์ ์์น)
import React from 'react';
import { useModal } from '../context/ModalContext';
const DeeplyNestedComponent = () => {
const { onClose } = useModal();
return (
<div>
<p>This component is deep inside the modal.</p>
{onClose && <button onClick={onClose}>Close from Deep Nest</button>}
</div>
);
};
์ปจํ ์คํธ API๋ฅผ ์ฌ์ฉํ๋ฉด ํธ๋ค๋ฌ(๋๋ ๊ธฐํ ๊ด๋ จ ๋ฐ์ดํฐ)๋ฅผ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ฅผ ๋ฐ๋ผ ํฌํ ์ฝํ ์ธ ๋ก ๊น๋ํ๊ฒ ์ ๋ฌํ ์ ์์ด ์ปดํฌ๋ํธ ์ธํฐํ์ด์ค๋ฅผ ๋จ์ํํ๊ณ ํนํ ๋ณต์กํ UI ์์คํ ์์ ํ์ ํ๋ ๊ตญ์ ํ์ ์ ์ง ๋ณด์์ฑ์ ํฅ์์ํต๋๋ค.
์ฑ๋ฅ์ ๋ฏธ์น๋ ์ํฅ
์ด๋ฒคํธ ์์ ์์ฒด๋ ์ฑ๋ฅ์ ํฅ์์ํค์ง๋ง, `handleBackdropClick` ๋๋ ์์๋ ๋ก์ง์ ๋ณต์ก์ฑ์ ์ ์ํ์ธ์. ๋ชจ๋ ํด๋ฆญ์ ๋ํด ๋น์ฉ์ด ๋ง์ด ๋๋ DOM ์ํ๋ ๊ณ์ฐ์ ์ํํ๋ค๋ฉด ์ฑ๋ฅ์ ์ํฅ์ ๋ฏธ์น ์ ์์ต๋๋ค. ๊ฒ์ฌ(์: `event.target.closest()`, `element.contains()`)๋ฅผ ๊ฐ๋ฅํ ํ ํจ์จ์ ์ผ๋ก ์ต์ ํํ์ธ์. ๋งค์ฐ ๋น๋ฒํ ์ด๋ฒคํธ์ ๊ฒฝ์ฐ, ํ์ํ ๊ฒฝ์ฐ ๋๋ฐ์ด์ฑ์ด๋ ์ค๋กํ๋ง์ ๊ณ ๋ คํ ์ ์์ง๋ง, ์ด๋ ๋ชจ๋ฌ์ ๊ฐ๋จํ ํด๋ฆญ/ํค๋ค์ด ์ด๋ฒคํธ์์๋ ๋ ์ผ๋ฐ์ ์ ๋๋ค.
๊ธ๋ก๋ฒ ์ฌ์ฉ์๋ฅผ ์ํ ์ ๊ทผ์ฑ(A11y) ๊ณ ๋ ค ์ฌํญ
์ ๊ทผ์ฑ์ ๋ถ๊ฐ์ ์ธ ๊ฒ์ด ์๋๋ผ, ํนํ ๋ค์ํ ์๊ตฌ์ ๋ณด์กฐ ๊ธฐ์ ์ ๊ฐ์ง ๊ธ๋ก๋ฒ ์ฌ์ฉ์๋ฅผ ์ํด ๊ตฌ์ถํ ๋ ๊ธฐ๋ณธ์ ์ธ ์๊ตฌ ์ฌํญ์ ๋๋ค. ๋ชจ๋ฌ์ด๋ ์ ์ฌํ ์ค๋ฒ๋ ์ด์ ํฌํ์ ์ฌ์ฉํ ๋, ์ด๋ฒคํธ ํธ๋ค๋ง์ ์ ๊ทผ์ฑ์ ์ค์ํ ์ญํ ์ ํฉ๋๋ค:
- ํฌ์ปค์ค ๊ด๋ฆฌ: ๋ชจ๋ฌ์ด ์ด๋ฆฌ๋ฉด, ํฌ์ปค์ค๋ ๋ชจ๋ฌ ๋ด ์ฒซ ๋ฒ์งธ ์ํธ์์ฉ ์์๋ก ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ์ด๋ํด์ผ ํฉ๋๋ค. ๋ชจ๋ฌ์ด ๋ซํ๋ฉด, ํฌ์ปค์ค๋ ๋ชจ๋ฌ์ ์ฐ ์์๋ก ๋์๊ฐ์ผ ํฉ๋๋ค. ์ด๋ ์ข ์ข `useEffect`์ `useRef`๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.
- ํค๋ณด๋ ์ํธ์์ฉ: `Escape` ํค๋ก ๋ซ๋ ๊ธฐ๋ฅ(์์ฐ๋ ๋ฐ์ ๊ฐ์ด)์ ์ค์ํ ์ ๊ทผ์ฑ ํจํด์ ๋๋ค. ๋ชจ๋ฌ ๋ด์ ๋ชจ๋ ์ํธ์์ฉ ์์๊ฐ ํค๋ณด๋๋ก ํ์ ๊ฐ๋ฅ(`Tab` ํค)ํ์ง ํ์ธํ์ธ์.
- ARIA ์์ฑ: ์ ์ ํ ARIA ์ญํ ๊ณผ ์์ฑ์ ์ฌ์ฉํ์ธ์. ๋ชจ๋ฌ์ ๊ฒฝ์ฐ, `role="dialog"` ๋๋ `role="alertdialog"`, `aria-modal="true"`, ๊ทธ๋ฆฌ๊ณ `aria-labelledby` ๋๋ `aria-describedby`๊ฐ ํ์์ ์ ๋๋ค. ์ด๋ฌํ ์์ฑ๋ค์ ์คํฌ๋ฆฐ ๋ฆฌ๋๊ฐ ๋ชจ๋ฌ์ ์กด์ฌ๋ฅผ ์๋ฆฌ๊ณ ๊ทธ ๋ชฉ์ ์ ์ค๋ช ํ๋ ๋ฐ ๋์์ ์ค๋๋ค.
- ํฌ์ปค์ค ํธ๋ํ: ๋ชจ๋ฌ ๋ด์ ํฌ์ปค์ค ํธ๋ํ์ ๊ตฌํํ์ธ์. ์ด๋ ์ฌ์ฉ์๊ฐ `Tab`์ ๋๋ฅผ ๋ ํฌ์ปค์ค๊ฐ ๋ฐฐ๊ฒฝ ์ ํ๋ฆฌ์ผ์ด์ ์ ์์๊ฐ ์๋ ๋ชจ๋ฌ *๋ด๋ถ*์ ์์๋ค๋ง ์ํํ๋๋ก ๋ณด์ฅํฉ๋๋ค. ์ด๋ ์ผ๋ฐ์ ์ผ๋ก ๋ชจ๋ฌ ์์ฒด์ ์ถ๊ฐ `keydown` ํธ๋ค๋ฌ๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํ๋ฉ๋๋ค.
๊ฒฌ๊ณ ํ ์ ๊ทผ์ฑ์ ๋จ์ง ๊ท์ ์ค์์ ๊ดํ ๊ฒ์ด ์๋๋๋ค. ์ด๋ ์ฅ์ ๊ฐ ์๋ ๊ฐ์ธ์ ํฌํจํ์ฌ ๋ ๋์ ๊ธ๋ก๋ฒ ์ฌ์ฉ์ ๊ธฐ๋ฐ์ผ๋ก ์ ํ๋ฆฌ์ผ์ด์ ์ ๋๋ฌ ๋ฒ์๋ฅผ ํ์ฅํ๊ณ , ๋ชจ๋ ์ฌ๋์ด UI์ ํจ๊ณผ์ ์ผ๋ก ์ํธ์์ฉํ ์ ์๋๋ก ๋ณด์ฅํฉ๋๋ค.
๋ฆฌ์กํธ ํฌํ ์ด๋ฒคํธ ํธ๋ค๋ง์ ์ํ ๋ชจ๋ฒ ์ฌ๋ก
์์ฝํ์๋ฉด, ๋ฆฌ์กํธ ํฌํ๋ก ์ด๋ฒคํธ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํ ์ฃผ์ ๋ชจ๋ฒ ์ฌ๋ก๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
- ์ด๋ฒคํธ ์์ ์์ฉ: ํญ์ ๊ณตํต ์กฐ์(์: ๋ชจ๋ฌ์ ๋ฐฑ๋๋กญ)์ ๋จ์ผ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ด๊ณ , `event.target`์ `element.contains()` ๋๋ `event.target.closest()`์ ํจ๊ป ์ฌ์ฉํ์ฌ ํด๋ฆญ๋ ์์๋ฅผ ์๋ณํ๋ ๊ฒ์ ์ ํธํ์ธ์.
- ๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์ดํด: ๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ด ํฌํ์ ์ด๋ฒคํธ๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ฌ์กฐ์ ํ์ฌ ๋ ผ๋ฆฌ์ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ ํธ๋ฆฌ๋ก ๋ฒ๋ธ๋ง์์ผ ์์์ ์ ๋ขฐํ ์ ์๊ฒ ๋ง๋ ๋ค๋ ์ ์ ๊ธฐ์ตํ์ธ์.
- ๊ธ๋ก๋ฒ ๋ฆฌ์ค๋ ์ ์คํ๊ฒ ๊ด๋ฆฌ: `Escape` ํค ๋๋ฆ๊ณผ ๊ฐ์ ๊ธ๋ก๋ฒ ์ด๋ฒคํธ์ ๊ฒฝ์ฐ, `useEffect` ํ ๋ด์์ `document`์ ์ง์ ๋ฆฌ์ค๋๋ฅผ ๋ถ์ด๊ณ ์ ์ ํ ์ ๋ฆฌ๋ฅผ ๋ณด์ฅํ์ธ์.
- `stopPropagation()` ์ต์ํ: `event.stopPropagation()`์ ๋๋ฌผ๊ฒ ์ฌ์ฉํ์ธ์. ๋ณต์กํ ์ด๋ฒคํธ ํ๋ฆ์ ๋ง๋ค ์ ์์ต๋๋ค. ๋ค๋ฅธ ํด๋ฆญ ๋์์ ์์ฐ์ค๋ฝ๊ฒ ์ฒ๋ฆฌํ๋๋ก ์์ ๋ก์ง์ ์ค๊ณํ์ธ์.
- ์ ๊ทผ์ฑ ์ฐ์ ์์: ํฌ์ปค์ค ๊ด๋ฆฌ, ํค๋ณด๋ ํ์, ์ ์ ํ ARIA ์์ฑ์ ํฌํจํ์ฌ ์ฒ์๋ถํฐ ํฌ๊ด์ ์ธ ์ ๊ทผ์ฑ ๊ธฐ๋ฅ์ ๊ตฌํํ์ธ์.
- DOM ์ฐธ์กฐ๋ฅผ ์ํด `useRef` ํ์ฉ: `useRef`๋ฅผ ์ฌ์ฉํ์ฌ ํฌํ ๋ด DOM ์์์ ๋ํ ์ง์ ์ ์ธ ์ฐธ์กฐ๋ฅผ ์ป์ผ์ธ์. ์ด๋ `element.contains()` ๊ฒ์ฌ์ ์ค์ํฉ๋๋ค.
- ๋ณต์กํ Props๋ฅผ ์ํด ์ปจํ ์คํธ API ๊ณ ๋ ค: ํฌํ ๋ด ๊น์ ์ปดํฌ๋ํธ ํธ๋ฆฌ์ ๊ฒฝ์ฐ, ์ปจํ ์คํธ API๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ฒคํธ ํธ๋ค๋ฌ๋ ๋ค๋ฅธ ๊ณต์ ์ํ๋ฅผ ์ ๋ฌํ์ฌ prop ๋๋ฆด๋ง์ ์ค์ด์ธ์.
- ์ฒ ์ ํ ํ ์คํธ: ํฌํ์ ํฌ๋ก์ค-DOM ํน์ฑ์ ๊ณ ๋ คํ์ฌ, ๋ค์ํ ์ฌ์ฉ์ ์ํธ์์ฉ, ๋ธ๋ผ์ฐ์ ํ๊ฒฝ, ๋ณด์กฐ ๊ธฐ์ ์ ๋ฐ์ ๊ฑธ์ณ ์ด๋ฒคํธ ํธ๋ค๋ง์ ์๊ฒฉํ๊ฒ ํ ์คํธํ์ธ์.
๊ฒฐ๋ก
๋ฆฌ์กํธ ํฌํ์ ๊ณ ๊ธ์ค๋ฝ๊ณ ์๊ฐ์ ์ผ๋ก ๋งค๋ ฅ์ ์ธ ์ฌ์ฉ์ ์ธํฐํ์ด์ค๋ฅผ ๊ตฌ์ถํ๊ธฐ ์ํ ํ์์ ์ธ ๋๊ตฌ์ ๋๋ค. ๊ทธ๋ฌ๋ ๋ถ๋ชจ ์ปดํฌ๋ํธ์ DOM ๊ณ์ธต ๊ตฌ์กฐ ์ธ๋ถ์์ ์ฝํ ์ธ ๋ฅผ ๋ ๋๋งํ๋ ๋ฅ๋ ฅ์ ์ด๋ฒคํธ ํธ๋ค๋ง์ ๋ํ ๋ ํนํ ๊ณ ๋ ค ์ฌํญ์ ๋์ ํฉ๋๋ค. ๋ฆฌ์กํธ์ ํฉ์ฑ ์ด๋ฒคํธ ์์คํ ์ ์ดํดํ๊ณ ์ด๋ฒคํธ ์์ ๊ธฐ์ ์ ๋ง์คํฐํจ์ผ๋ก์จ ๊ฐ๋ฐ์๋ ์ด๋ฌํ ๊ณผ์ ๋ฅผ ๊ทน๋ณตํ๊ณ ๋งค์ฐ ์ํธ์์ฉ์ ์ด๊ณ ์ฑ๋ฅ์ด ๋ฐ์ด๋๋ฉฐ ์ ๊ทผ์ฑ ์๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ตฌ์ถํ ์ ์์ต๋๋ค.
์ด๋ฒคํธ ์์์ ๊ตฌํํ๋ฉด ๊ธฐ๋ณธ DOM ๊ตฌ์กฐ์ ๊ด๊ณ์์ด ๊ธ๋ก๋ฒ ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ผ๊ด๋๊ณ ๊ฒฌ๊ณ ํ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋๋ก ๋ณด์ฅํฉ๋๋ค. ์ด๋ ๋ ๊น๋ํ๊ณ ์ ์ง ๊ด๋ฆฌ๊ฐ ์ฉ์ดํ ์ฝ๋๋ก ์ด์ด์ง๋ฉฐ ํ์ฅ ๊ฐ๋ฅํ UI ๊ฐ๋ฐ์ ๊ธธ์ ์ด์ด์ค๋๋ค. ์ด๋ฌํ ํจํด์ ๋ฐ์๋ค์ด๋ฉด, ๋ค์ ํ๋ก์ ํธ์์ ๋ฆฌ์กํธ ํฌํ์ ๋ชจ๋ ํ์ ํ์ฉํ์ฌ ์ ์ธ๊ณ ์ฌ์ฉ์์๊ฒ ๋ฐ์ด๋ ๋์งํธ ๊ฒฝํ์ ์ ๊ณตํ ์ค๋น๊ฐ ๋ ๊ฒ์ ๋๋ค.