일부 자식을 DOM의 다른 부분으로 렌더링할 수 있다.
createPortal(children, domNode, key?)
portal을 만들기 위해서는 JSX와 렌더링할 DOM 노드를 전달한다.
포털을 사용하면 컴포넌트가 자식 중 일부를 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 노드 안에 배치한다.
포털이 없다면, 두 번째 <p>
태그는 부모 <div>
태그 안에 배치되지만 포털은 있으면 본문(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>
);
}
개발자 도구로 DOM 구조를 확인하면 두 번째 <p>
태그가 <body>
태그 안에 배치된 모습을 확인할 수 있다.
모달을 불러오는 구성요소가 overflow가 있는 컨테이너 안에 있는 경우에도 포털을 사용하여 페이지의 나머지 부분 위에 떠있는 모달을 만들 수 있다.
아래 예제는 2개의 컨테이너가 모달을 방해하는 스타일이 있지만, DOM에서 모달이 부모 JSX 요소에 포함되어 있지 않기 때문에 포털로 렌더링된 모달은 영향을 받지 않는다.
export default function App() {
return (
<>
<div className="clipping-container">
<NoPortalExample />
</div>
<div className="clipping-container">
<PortalExample />
</div>
</>
);
}
포탈이 없는 경우, 부모 요소 스타일 영향을 받은 모습이고
포탈이 있는 경우, 부모 요소 스타일 영향을 받지 않은 모습이다.
<div id="root"></div>
종속하여 구현한다.createPortal
을 사용하면 id가 root인 div에서 벗어날 수 있다. <html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
<div id="modal"></div>
를 추가했다. <body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="modal"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
createPortal
을 사용하면 id="modal"을 가진 DOM 노드 안에 배치한다. import { createPortal } from 'react-dom';
import { useModalStore } from 'store/modal';
const Modal = ({ children }: { children: React.ReactNode }) => {
const { isOpen } = useModalStore();
if (!isOpen) return null;
return createPortal(
<div className='w-100 h-100 fixed top-0 left-0 right-0 bottom-0 flex justify-center items-center'>
<div className='rounded-md min-w-80 absolute top-1/2 left-1/2 p-10 bg-white shadow-modal -translate-x-1/2 -translate-y-1/2 overflow-hidden'>
<div className='w-80 h-auto flex flex-col items-center'>{children}</div>
</div>
</div>,
document.getElementById('modal')!,
);
};
export default Modal;
장바구니 담기 또는 중복 옵션을 선택했을 때 모달창을 띄우기 위해
전역 상태 관리로 모드를 변경하고 열고 닫을 수 있도록 설정하기로 했다.
// src\store\modal.ts
import { create } from 'zustand';
type ModalStore = {
isOpen: boolean;
mode: 'add cart' | 'duplicate option';
setMode: (mode: 'add cart' | 'duplicate option') => void;
toggleModal: () => void;
};
export const useModalStore = create<ModalStore>((set) => ({
isOpen: false,
mode: 'add cart',
setMode: (mode) => set({ mode }),
toggleModal: () => set((state) => ({ isOpen: !state.isOpen })),
}));
// src\components\AddToCartModal.tsx
import ModalCloseSvg from 'assets/svg/ModalCloseSvg';
import { Link } from 'react-router-dom';
import { useModalStore } from 'store/modal';
const AddToCartModal = () => {
const { toggleModal } = useModalStore();
const handleClick = () => {
toggleModal();
};
return (
<div className='flex flex-col gap-5 text-lg text-center leading-6'>
<p>장바구니에 상품이 담겼습니다.</p>
<Link
className='border border-gray-400 p-3'
to={'/carts'}
onClick={handleClick}
>
장바구니 바로 가기
</Link>
<button className='w-10 absolute top-2 right-2 p-2' onClick={toggleModal}>
<ModalCloseSvg />
</button>
</div>
);
};
export default AddToCartModal;
장바구니 담기 확인 모달창이 나올 때 mode
를 확인 후 필요시
변경한 다음 모달창을 연다.
//src\pages\productDetail.tsx
const ProductDetail = () => {
// ...
const { isOpen, mode, setMode, toggleModal } = useModalStore();
const handleAdd = () => {
if (!selected) return;
if (mode !== 'add cart') {
setMode('add cart');
}
toggleModal();
// ...
};
return (
<div className='w-full flex flex-col md:flex-row content-between gap-10 p-10'>
{isOpen && mode === 'add cart' && (
<Modal>
<AddToCartModal />
</Modal>
)}
{isOpen && mode === 'duplicate option' && (
<Modal>
<SelectedOptionModal />
</Modal>
)}
<img
className='w-full basis-1/2 md:w-140 md:h-140'
src={image}
alt={'상품 이미지'}
/>
<div className='w-full basis-1/2 flex flex-col gap-2 pl-10'>
<div className='flex justify-between'>
<h3 className='text-xl font-semibold'>{name}</h3>
<button>
<IoMdHeartEmpty size={26} />
</button>
</div>
<div className='border-b-2 pb-2'>
<span className='text-xl font-semibold'>
{price.toLocaleString()}
</span>
<span className='font-bold'>원</span>
</div>
<p>{description}</p>
<select
className='h-8 border border-gray-400 outline-none cursor-pointer'
onChange={handleSelect}
value={''}
>
<option disabled value={''}>
[사이즈]를 선택하세요.
</option>
{size?.map((s, i) => (
<option key={i} value={s}>
{s}
</option>
))}
</select>
{selected && <SelectedProduct option={option} />}
<button
className='h-12 mt-4 bg-primary text-white hover:bg-price-stress shadow-md'
onClick={handleAdd}
>
장바구니 담기
</button>
</div>
</div>
);
};
export default ProductDetail;
➡️ 상황에 따라 모달 창이 다르게 나오는 것을 확인할 수 있다.