[React] React Portal
๐๏ธ React Potal ์ด๋?
- ๋ค๋ฅธ ๊ณณ์ ์๋ element๋ฅผ ๋ ๋๋งํ ์ ์๋ค.
- ์ปดํฌ๋ํธ๊ฐ ์ข ์๋์ด ์๋ ๋ ํธ๋ฆฌ๋ฅผ ๋ฒ์ด๋ ์ธ๋ถ์ ๋ค๋ฅธ ๋์ผ๋ก ๋ ๋ํ๋ค. (์ดํ๋ฆฌ์ผ์ด์ ์ ํ๋์ง๋ง ๋ถ๋ถ์ ์ผ๋ก ๋ค๋ฅธ ๋์ ๋ง์ดํธ)
๐๏ธ useModal hook
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { styled } from "styled-components";
const useModal = () => {
const [modalOpened, setModalOpened] = useState(false);
const openModal = () => {
setModalOpened(true);
};
const closeModal = () => {
setModalOpened(false);
};
interface IProps {
children: React.ReactNode;
}
const ModalPortal: React.FC<IProps> = ({ children }) => {
const ref = useRef<Element | null>();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
if (document) {
// dom์ root-modal์ด ์์ผ๋ฉด ๊ฐ์ ธ์ค๊ธฐ
const dom = document.querySelector("#root-modal");
ref.current = dom;
}
}, []); // mount
if (ref.current && mounted && modalOpened) {
return createPortal(
<Container>
<div
className="modal-background"
role="presentation"
onClick={closeModal}
/>
{children}
</Container>,
ref.current
);
}
return null;
};
return {
openModal,
closeModal,
ModalPortal,
};
};
export default useModal;
const Container = styled.div`
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
z-index: 11;
.modal-background {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.75);
}
`;
๐๏ธ useModal hook ์ฌ์ฉํ๊ธฐ
import { styled } from "styled-components";
import AirbnbLogoIcon from "../public/static/svg/logo/logo.svg";
import AirbnbLogoTextIcon from "../public/static/svg/logo/logo_text.svg";
import Link from "next/link";
import palette from "../styles/palette";
import { useState } from "react";
import useModal from "../hooks/useModal";
import SignUpModal from "./auth/SignUpModal";
const Header: React.FC = () => {
const { openModal, ModalPortal } = useModal();
return (
<Container>
<Link href={"/"}>
<div className="header-logo-wrapper">
<AirbnbLogoIcon className="header-logo" />
<AirbnbLogoTextIcon />
</div>
</Link>
<div className="header-auth-buttons">
<button
type="button"
className="header-sign-up-button"
onClick={openModal}
>
ํ์๊ฐ์
</button>
<button type="button" className="header-login-button">
๋ก๊ทธ์ธ
</button>
</div>
<ModalPortal>
<SignUpModal />
</ModalPortal>
</Container>
);
};
export default Header;
const Container = styled.div`
position: sticky;
top: 0;
width: 100%;
height: 80px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 80px;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.08) 0px 1px 12px;
z-index: 10;
.header-logo-wrapper {
display: flex;
align-items: center;
.header-logo {
margin-right: 6px;
}
}
.header-auth-buttons {
.header-sign-up-button {
height: 42px;
margin-right: 8px;
padding: 0 16px;
border: 0;
border-radius: 21px;
background-color: white;
cursor: pointer;
outline: none;
&:hover {
background-color: ${palette.gray_f7};
}
}
.header-login-button {
height: 42px;
padding: 0 16px;
border: 0;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.18);
border-radius: 21px;
background-color: white;
cursor: pointer;
outline: none;
:hover {
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.12);
}
}
}
`;
์ฐธ๊ณ
- ํด๋ก ์ฝ๋ฉ์ผ๋ก ์์ํ๋ Next.js
๐ฌ ์ต์ ๋๊ธ