Post

Zustand를 활용한 모달 관리 구현

Zustand를 활용한 모달 관리 구현

이 글에서는 Next.js의 App RouterZustand 상태 관리 라이브러리를 사용하여 모달을 구현하고 관리하는 방법을 설명한다.


1. Provider 생성 및 설정

모달 컴포넌트를 전역적으로 관리하기 위해 ModalProvider를 작성한다. 이 Provider는 모든 모달을 포함하며, 전역 상태를 통해 어떤 모달을 표시할지 제어한다.

코드 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//@/components/providers/modal-provider.tsx

"use client";

import { useEffect, useState } from "react";
import PostUploadModal from "@/components/modals/post-upload-modal";
import PostViewModal from "@/components/modals/post-view-modal";
import ConfirmModal from "@/components/modals/confirm-modal";

export default function ModalProvider() {
  const [isMounted, setIsMounted] = useState(false);

  // 클라이언트 측에서만 렌더링 되도록 함 (Hydration 오류 방지)
  useEffect(() => {
    setIsMounted(true);
  }, []);

  if (!isMounted) return null;

  return (
    <>
      {/* 모든 모달 컴포넌트를 전역적으로 렌더링 */}
      <PostUploadModal />
      <PostViewModal />
      <ConfirmModal />
    </>
  );
}

Provider를 Layout에 추가

ModalProvider를 최상위 Layout에 추가하여 모든 페이지에서 모달이 사용 가능하도록 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// /app/layout.tsx
import ModalProvider from "@/components/providers/modal-provider";

export default async function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className="antialiased">
        {/* ModalProvider를 Layout에 추가 */}
        <ModalProvider />
        <div className="w-full h-full bg-white dark:bg-black text-black dark:text-white">
          {children}
        </div>
      </body>
    </html>
  );
}

2. 모달 상태 관리 - Zustand

모달의 상태를 관리하기 위해 Zustand를 사용한다. 각 모달의 열림/닫힘 상태, 모달 타입, 그리고 전달받는 데이터를 관리한다.

코드 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//@/store/modal/modal-store.ts
import { create } from "zustand";
import { PostUploadSchema } from "@/schemas";
import { Post } from "@prisma/client";
import { z } from "zod";

// 모달 타입 정의
export type ModalType =
  | "post-upload"
  | "post-view"
  | "profile"
  | "timeline"
  | "delete-confirm"
  | "edit-confirm";

// 모달에 전달할 데이터
export type ModalData = null | {
  post?: Post | null;
  onConfirm?: () => void;
  title?: string;
  description?: string;
  formData?: z.infer<typeof PostUploadSchema> | null;
};

// Zustand 상태 관리
interface ModalState {
  type: ModalType | null;
  setType: (type: ModalType) => void;
  isOpen: boolean;
  openModal: () => void;
  closeModal: () => void;
  data: ModalData;
  setData: (data: ModalData) => void;
}

const useModal = create<ModalState>((set) => ({
  type: null,
  setType: (type) => set({ type }),
  isOpen: false,
  openModal: () => set({ isOpen: true }),
  closeModal: () =>
    set({
      type: null,
      isOpen: false,
      data: null
    }),
  data: null,
  setData: (data) => set({ data })
}));

export default useModal;

주요 동작

  • type: 현재 활성화된 모달의 유형을 관리한다.
  • isOpen: 모달의 열림 상태를 제어한다.
  • data: 모달에 전달할 데이터를 저장한다.

3. 모달 컴포넌트 구현

모달 컴포넌트는 Zustand 상태를 구독하여 모달 열림/닫힘 상태와 데이터를 기반으로 렌더링된다.

코드 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//@/components/modals/post-upload-modal.tsx
"use client";

import useModal from "@/store/modal/modal-store";
import { formatDateForTimeline } from "@/lib/formatDate";
import Image from "next/image";
import { Button } from "../ui/button";
import { useScrollLock } from "@/hooks/use-scroll-lock";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import useUser from "@/store/user/user-store";

const PostUploadModal = () => {
  const { user } = useUser();

  // zustand에 저장된 modal state를 가져와서 사용
  const { isOpen, closeModal, type, data: postData, setType } = useModal();

  const queryClient = useQueryClient();

  // 포스트 삭제 요청
  const { mutate: deletePost, isPending: isDeletePending } = useMutation({
    mutationFn: async () => {
      await axios.delete(`/api/posts/${postData?.post?.id}`);
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["posts"] });
    }
  });

  // 스크롤 락 처리
  useScrollLock(isOpen);

  // isOpen이 true이고 type이 post-view일 때만 모달이 열리도록 함
  if (!isOpen || type !== "post-view") return null;

  // 모달 타입을 post-upload로 변경
  const handleEdit = () => setType("post-upload");

  const handleDelete = () => {
    deletePost();
    closeModal(); // 모달 닫기
  };

  return (
    <div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center">
      <div className="w-[400px] h-[800px] bg-white rounded-md">
        <div className="flex justify-between items-center p-4 border-b">
          <h1 className="text-2xl font-bold">
            {postData?.post?.date
              ? formatDateForTimeline(new Date(postData.post.date))
              : ""}
          </h1>
          <button onClick={closeModal} className="text-2xl font-bold">
            X
          </button>
        </div>
        <div className="flex flex-col items-center gap-4 p-4">
          <Image
            src={postData?.post?.imageUrl || "https://via.placeholder.com/350"}
            alt="Post Image"
            width={350}
            height={350}
            className="object-cover"
          />
          <p className="text-md font-bold">{postData?.post?.title}</p>
          <p className="text-sm text-gray-500">{postData?.post?.content}</p>
          {user?.id === postData?.post?.authorId && (
            <div className="flex gap-2">
              <Button onClick={handleEdit}>Edit</Button>
              <Button onClick={handleDelete} disabled={isDeletePending}>
                Delete
              </Button>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};

export default PostUploadModal;

주요 기능 요약

  1. ModalProvider
    • 모달의 컨테이너 역할.
    • 모든 모달을 전역에서 관리.
  2. Zustand로 상태 관리
    • 모달 타입, 데이터, 열림/닫힘 상태를 효율적으로 관리.
  3. 모달 컴포넌트
    • Zustand의 상태를 구독하여 필요한 UI를 렌더링.
    • 데이터 패칭 및 삭제 요청과 같은 기능 포함.

이 구현은 전역 모달 관리가 필요한 프로젝트에서 유용하며, 상태 관리와 UI 처리를 분리하여 코드 가독성을 높인다. Next.js와 Zustand의 조합으로 간결하면서도 강력한 모달 시스템을 구축할 수 있다.

This post is licensed under CC BY 4.0 by the author.