npx shadcn-ui@latest add dialog
shadcn의 dialog를 설치한다.
modal의 기본인 modal base를 만든다.
'use client';
import { ReactNode } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
interface ModalProps {
title: string;
description: string;
isOpen: boolean;
onClose: () => void;
children?: ReactNode;
}
export default function Modal({
title,
description,
isOpen,
onClose,
children,
}: ModalProps) {
const onChange = (open: boolean) => {
if (!open) {
onClose();
}
};
return (
<Dialog open={isOpen} onOpenChange={onChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div>{children}</div>
</DialogContent>
</Dialog>
);
}
실제 사용하려고 하는 store-modal.tsx를 만든다.
'use client';
import { useStoreModal } from '@/hooks/use-store-modal';
import Modal from '@/components/ui/modal';
export default function StoreModal() {
const storeModal = useStoreModal();
return (
<Modal
title='Create Store'
description='Add a new store to manage products and categories'
isOpen={storeModal.isOpen}
onClose={storeModal.onClose}
>
Body!
</Modal>
);
}
어떤 컴포넌트에서건 store-modal을 띄워야하기 때문에 zustand
를 설치하여, useStoreModal 이라는 훅을 만든다.
import { create } from 'zustand';
interface useStoreModalStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
export const useStoreModal = create<useStoreModalStore>((set) => ({
// initial value 들
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
실제 store-modal을 전역에서 띄워야하기 때문에, modalProvider를 만들어 루트의 layout.tsx에 써준다.
'use client';
import { useEffect, useState } from 'react';
import StoreModal from '@/components/modals/store-modal';
export default function ModalProvider() {
const [isMounted, setIsMounted] = useState(false);
// 오직 클라이언트 컴포넌트에서만 실행됨
useEffect(() => {
setIsMounted(true);
}, []);
// Layout는 서버컴포넌트이기 때문에 하이드레이션 에러를 방지하기 위해서 isMounted 상태를 만듬. 서버사이드에서는 모달 안뜸.
if (!isMounted) {
return null;
}
return (
<>
<StoreModal />
</>
);
}
마지막으로 절대 닫히지 않는 모달을 만들기 위해 /app/(root)/page.tsx에 isOpen이 false면, onOpen이 실행되어 열리는 로직을 써준다.
// 뭔가 redux의 useSelector 같은 느낌이랄까?!
const onOpen = useStoreModal((state) => state.onOpen);
const isOpen = useStoreModal((state) => state.isOpen);
useEffect(() => {
if (!isOpen) {
onOpen();
}
}, [isOpen, onOpen]);