<html lang="en" className={nanumGothicFont.className}>
{/*
<head /> will contain the components returned by the nearest parent
head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
*/}
<head />
<body suppressHydrationWarning={true}>
<script
dangerouslySetInnerHTML={{
__html: themeInitializerScript,
}}
></script>
<Card>
<Header />
{children}
// 포탈 위치
<div id="portal"></div>
</Card>
</body>
</html>
"use client";
import classes from "./Login.module.css";
import { BiLogIn } from "react-icons/bi";
import { createPortal } from "react-dom";
import { useState, useEffect } from "react";
import LoginModal from "@/components/Modal/LoginModal";
export default function Login() {
let [loginModal, setLoginModal] = useState(false);
let [portalElement, setPortalElement] = useState<Element | null>(null);
useEffect(() => {
setPortalElement(document.getElementById("portal"));
}, [loginModal]);
const loginModalHandler = () => {
setLoginModal(!loginModal);
};
return (
<>
<div className={classes.login_div} onClick={loginModalHandler}>
<h2>로그인</h2>
<BiLogIn className={classes.login_icon} />
</div>
{loginModal && portalElement
? createPortal(<LoginModal />, portalElement)
: null}
</>
);
}
- useState를 통해서 초깃값을 false로 만들어 준후, 로그인 버튼을 누르면 loginModal 상태값이 true가 되면서 createPortal이 작동된다.
저 useEffect를 사용해서 portalElement에 document.getElementById("portal")를 넣어주는것이 중요하다.
그렇지 않으면 document is not defined 라는 에러가 발생한다.
document는 브라우저에 의존적인 요소이므로 SSR 에서는 렌더링 될수가 없다.
그렇기 때문에 useEffect 를 통해 렌더링 후에 portalElement를 업데이트 해주면 된다.
import classes from "./Backdrop.module.css";
type BackDropType = {
children: React.ReactNode;
loginModalHandler: () => void;
};
export default function Backdrop({
children,
loginModalHandler,
}: BackDropType) {
return (
<div className={classes.backdrop} onClick={loginModalHandler}>
{children}
</div>
);
}
import classes from "./LoginModal.module.css";
import Backdrop from "./Backdrop";
type LoginModalProps = {
loginModalHandler: () => void;
};
export default function LoginModal({ loginModalHandler }: LoginModalProps) {
return (
<>
<Backdrop loginModalHandler={loginModalHandler}>
<div className={classes.login_modal_container}></div>
</Backdrop>
</>
);
}
정상적으로 모달창이 등장했다.
로그인창과 환경설정 모달이 총 2개가 있어서, useModal 이라는 커스텀 훅을 만들어주었다.
"use client";
import { useState, useEffect } from "react";
type UseModalReturn = [boolean, () => void, Element | null];
const useModal = (): UseModalReturn => {
let [modal, setModal] = useState(false);
let [portalElement, setPortalElement] = useState<Element | null>(null);
useEffect(() => {
setPortalElement(document.getElementById("portal"));
}, [modal]);
const modalHandler = () => {
setModal(!modal);
};
return [modal, modalHandler, portalElement];
};
export default useModal;
또한 모달이 생성 됐을때, 백그라운드 스크롤을 막아주었다.
// utils/scroll.ts
export const preventScroll = () => {
const currentScrollY = window.scrollY;
document.body.style.position = "fixed";
document.body.style.width = "100%";
document.body.style.top = `-${currentScrollY}px`; // 현재 스크롤 위치
document.body.style.overflowY = "scroll";
return currentScrollY;
};
export const allowScroll = (prevScrollY: number) => {
document.body.style.position = "";
document.body.style.width = "";
document.body.style.top = "";
document.body.style.overflowY = "";
window.scrollTo(0, prevScrollY);
};
import { useEffect } from "react";
import { preventScroll, allowScroll } from "@/utils/scroll";
const usePreventScroll = () => {
useEffect(() => {
const prevScrollY = preventScroll();
return () => {
allowScroll(prevScrollY);
};
}, []);
};
export default usePreventScroll;