Next.js 13버전에서 portal을 사용하여 커스텀 모달(custom alert, custom confirm)을 만들어보았다.
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "develofarm",
description: "Find a project",
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
{children}
<div id="portal" className="z-100"></div>
</body>
</html>
)
}
"use client"
import React, { useEffect } from "react"
import ReactDOM from "react-dom"
import useCustomModalStore from "@/store/customModal"
import Button from "./ui/Button"
const CustomModal = () => {
const {
modalMessage,
modalType,
viewCustomModal,
setViewCustomModal,
handler,
} = useCustomModalStore((state) => state)
/** 뒷배경 스크롤 방지 */
useEffect(() => {
if (viewCustomModal) {
document.body.style.cssText = `
position: fixed;
top: -${window.scrollY}px;
overflow-y: scroll;
width: 100%;`
return () => {
const scrollY = document.body.style.top
document.body.style.cssText = ""
window.scrollTo(0, parseInt(scrollY || "0", 10) * -1)
}
}
}, [viewCustomModal])
/** 확인 핸들러 */
const confirmHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
if (e.target === e.currentTarget) {
setViewCustomModal(false)
if (!handler) return console.log("지정된 함수가 없습니다.")
handler()
}
}
/** 취소 핸들러 */
const cancelHandler = (e: React.MouseEvent<HTMLButtonElement>) => {
if (e.target === e.currentTarget) {
setViewCustomModal(false)
}
}
if (!viewCustomModal) return null
return ReactDOM.createPortal(
<>
<div className="fixed top-0 left-0 right-0 bottom-0 flex bg-black bg-opacity-50 z-100" />
<div className="fixed top-[50%] left-[50%] flex transform: translate-x-[-50%] translate-y-[-50%] p-[50px] w-[450px] h-[260px] bg-white z-200 rounded-3xl">
<div className="w-full h-full flex flex-col justify-around">
<h3 className="text-[20px] font-[600] whitespace-pre-line text-center">
{modalMessage}
</h3>
<section className="w-full flex justify-around px-8">
{modalType === "confirm" && (
<Button
handler={cancelHandler}
text="취소"
type="border"
color="#297A5F"
/>
)}
<Button handler={confirmHandler} text="확인" color="#297A5F" />
</section>
</div>
</div>
</>,
document.getElementById("portal") as HTMLElement,
)
}
export default CustomModal
import useCustomModalStore from "@/store/customModal"
export const useCustomModal = () => {
const { setModalMessage, setModalType, setViewCustomModal, setHandler } =
useCustomModalStore((state) => state)
const openCustomModalHandler = (
message: string,
type: string,
handler?: () => void,
) => {
setViewCustomModal(true)
setModalMessage(message)
setModalType(type)
handler && setHandler(handler)
}
return { openCustomModalHandler }
}
1) alert
openCustomModalHandler("검색되었습니다.", "alert")
2) confirm
const onClickMemberCardHandler = () => {
const handler = () => {
router.push("/signin")
}
if (!currentUserId) {
openCustomModalHandler(
`로그인이 필요합니다.
로그인 페이지로 이동하시겠습니까?`,
"confirm",
handler,
)
} else {
setSelectedMember(user as ExtendedUsersType)
setViewMemberModal(true)
}
}
```