shadcn의 dialog 컴포넌트로 만들기

npx shadcn-ui@latest add dialog

위치는 component/ui 디렉토리 하위에 위치해 있다!

흐름

  1. shadcn의 dialog를 설치한다.

  2. 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>
      );
    }
    
  3. 실제 사용하려고 하는 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>
      );
    }
    
  4. 어떤 컴포넌트에서건 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 }),
    }));
    
  5. 실제 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 />
        </>
      );
    }
    
  6. 마지막으로 절대 닫히지 않는 모달을 만들기 위해 /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]);