Zustand를 활용한 효율적인 모달 관리하기
Zustand를 활용한 효율적인 모달 관리하기
이 글에서 Next.js의 App Router와 Zustand 상태 관리 라이브러리를 사용하여 모달을 구현하고 관리하는 방법을 설명하겠습니다.
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//@/store/modal/modal-store.ts
import { PostUploadSchema } from "@/schemas";
import { TPostsWithAuthorAndLikes } from "@/types";
import { Post } from "@prisma/client";
import { z } from "zod";
import { create } from "zustand";
export type ModalType =
| "post-upload"
| "post-view"
| "profile"
| "timeline"
| "delete-confirm"
| "edit-confirm"
| "save-confirm";
export type ModalData = null | {
post?: Post | TPostsWithAuthorAndLikes | null;
onConfirm?: null | (() => void);
title?: string;
description?: string;
formData?: z.infer<typeof PostUploadSchema> | null;
};
interface ModalState {
type: ModalType | null;
previousType: ModalType | null;
setType: (type: ModalType) => void;
isOpen: boolean;
openModal: () => void;
closeModal: () => void;
closeConfirmModal: () => void;
data: ModalData;
previousData: ModalData | null;
setData: (data: ModalData) => void;
}
const useModal = create<ModalState>((set) => ({
type: null,
previousType: null,
setType: (type) =>
set((state) => ({
type,
previousType: type === "edit-confirm" ? state.type : null,
previousData: type === "edit-confirm" ? state.data : null
})),
isOpen: false,
openModal: () => set({ isOpen: true }),
closeModal: () =>
set({
type: null,
previousType: null,
isOpen: false,
data: null,
previousData: null
}),
closeConfirmModal: () =>
set((state) => ({
type: state.previousType,
data: {
...state.previousData,
formData: state.data?.formData // 현재 formData 유지
},
previousType: null,
previousData: null,
isOpen: !!state.previousType
})),
data: null,
previousData: null,
setData: (data) => set({ data, previousData: null })
}));
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;
주요 기능 요약
ModalProvider
- 모달의 컨테이너 역할
- 모든 모달을 전역에서 관리
Zustand
로 상태 관리- 모달 타입, 데이터, 열림/닫힘 상태를 효율적으로 관리
- 모달 컴포넌트
- Zustand의 상태를 구독하여 필요한 UI를 렌더링
- 데이터 패칭 및 삭제 요청과 같은 기능 포함
이 구현은 전역 모달 관리가 필요한 프로젝트에서 유용하며, 상태 관리와 UI 처리를 분리하여 코드 가독성을 높입니다. Next.js와 Zustand의 조합으로 간결하면서도 강력한 모달 시스템을 구축할 수 있습니다.
This post is licensed under CC BY 4.0 by the author.