간단할 줄 알았는데 코드가 엄청나게 길어졌다..!
custom hook 으로 해결할 수 있을 줄 알았지만 꽤 커져서... 계속 코드분리를 하고 싶었다. 그 전에 일단 Zustand 랑 TanStack Query를 쓰기로 한 게 팀 목표여서 이 부분부터 수정해주기로 했다.
전역상태 관리 코드 수정 (Zustand, TanStack Query)
import { create } from "zustand";
type ScrapStore = {
userId: string | null;
setUserId: (id: string | null) => void;
folderName: string;
setFolderName: (folder: string) => void;
isSaving: boolean;
setIsSaving: (saving: boolean) => void;
existingFolders: string[];
setExistingFolders: (folders: string[]) => void;
selectedFolder: string | null;
setSelectedFolder: (folder: string | null) => void;
};
export const useScrapStore = create<ScrapStore>((set) => ({
userId: null,
setUserId: (id) => set({ userId: id }),
folderName: "",
setFolderName: (folder) => set({ folderName: folder }),
isSaving: false,
setIsSaving: (saving) => set({ isSaving: saving }),
existingFolders: [],
setExistingFolders: (folders) => set({ existingFolders: folders }),
selectedFolder: null,
setSelectedFolder: (folder) => set({ selectedFolder: folder })
}));
이 useScrapStore는 다양한 스크랩 관련 기능에서 공통적으로 사용될 상태와 이를 업데이트하는 함수를 제공한다.
userId: 현재 로그인한 사용자의 ID를 저장. 로그인하지 않았다면 null 반환. setUserId를 통해 다른 컴포넌트에서 userId를 업데이트할 수 있다.
folderName: 현재 사용자가 선택한 폴더의 이름을 저장. 폴더를 선택하거나 이름을 설정할 때 setFolderName을 호출해 업데이트할 수 있다.
isSaving: 스크랩을 저장할 때 isSaving을 true로 설정하여 현재 저장 중임을 나타낸다. 완료 후에는 다시 false로 설정한다.
existingFolders: 현재 사용자 계정에 있는 모든 폴더의 이름 목록을 저장한다. 이 목록은 setExistingFolders를 통해 설정하거나 업데이트할 수 있다.
selectedFolder: 사용자가 현재 선택한 폴더를 나타내며, null일 경우 폴더가 선택되지 않은 상태를 의미한다. setSelectedFolder로 값 설정이 가능.
import { useMutation, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query";
import { supabase } from "@/supabase/supabase";
import { useScrapStore } from "@/store/scrapStore";
import { getUserId } from "@/serverActions/profileAction";
import { useEffect } from "react";
// 폴더 목록을 가져오는 함수
const fetchFolders = async (userId: string): Promise<string[]> => {
const { data, error } = await supabase.from("SCRAP_TABLE").select("folder_name").eq("user_id", userId);
if (error) throw new Error(error.message);
// 중복 폴더 이름 제거
return Array.from(new Set(data.map((item) => item.folder_name)));
};
// 특정 레시피가 이미 스크랩되었는지 확인하는 함수
const isAlreadyScrapped = async (recipeId: string, userId: string): Promise<boolean> => {
const { data, error } = await supabase
.from("SCRAP_TABLE")
.select("scrap_id")
.eq("scrap_id", recipeId)
.eq("user_id", userId);
if (error) throw new Error(error.message);
return data && data.length > 0;
};
// 레시피의 스크랩 수를 가져오는 함수
const fetchRecipeScrapCount = async (recipeId: string): Promise<number> => {
const { data, error } = await supabase.from("TEST_TABLE").select("scrap_count").eq("post_id", recipeId).single();
if (error) throw new Error(error.message);
return data?.scrap_count || 0;
};
// 스크랩 데이터 가져오기 함수
const fetchScraps = async (userId: string): Promise<Scrap[]> => {
const { data, error } = await supabase.from("SCRAP_TABLE").select("*").eq("user_id", userId);
if (error) throw new Error(error.message);
return data || [];
};
interface Scrap {
scrap_id: string;
folder_name: string;
scraped_recipe: string;
created_at: string;
updated_at: string;
}
interface UseScrapData {
existingFolders: string[] | undefined;
scraps: Scrap[] | undefined;
refetchFolders: () => void;
refetchScraps: () => void;
incrementScrapCount: (recipeId: string) => Promise<boolean>;
saveScrap: (params: { recipeId: string; folderName: string }) => Promise<boolean>;
useFetchScrapCount: (recipeId: string) => UseQueryResult<number>;
}
// useScrapData 훅 정의
export const useScrapData = (): UseScrapData => {
const { userId, setUserId } = useScrapStore();
const queryClient = useQueryClient();
// userId 초기화
useEffect(() => {
if (!userId) {
getUserId()
.then((id) => setUserId(id))
.catch((error) => console.error("사용자 ID 설정 오류:", error));
}
}, [userId, setUserId]);
// 폴더 목록 쿼리
const { data: existingFolders, refetch: refetchFolders } = useQuery({
queryKey: ["folders", userId],
queryFn: () => fetchFolders(userId as string),
enabled: !!userId,
staleTime: 5 * 60 * 1000 // 5분 동안 캐싱
});
// 스크랩 데이터 쿼리
const { data: scraps, refetch: refetchScraps } = useQuery({
queryKey: ["scraps", userId],
queryFn: () => fetchScraps(userId as string),
enabled: !!userId
});
// 개별 레시피 스크랩 수를 위한 커스텀 훅
const useFetchScrapCount = (recipeId: string) =>
useQuery({
queryKey: ["scrapCount", recipeId],
queryFn: () => fetchRecipeScrapCount(recipeId),
enabled: !!recipeId
});
// 스크랩 수 증가를 위한 mutation
const { mutateAsync: incrementScrapCount } = useMutation({
mutationFn: async (recipeId: string) => {
const currentCount = await fetchRecipeScrapCount(recipeId);
const newCount = currentCount + 1;
const { error } = await supabase.from("TEST_TABLE").update({ scrap_count: newCount }).eq("post_id", recipeId);
if (error) throw new Error(error.message);
queryClient.setQueryData(["scrapCount", recipeId], newCount);
return true;
}
});
// 스크랩 저장 mutation
const { mutateAsync: saveScrap } = useMutation({
mutationFn: async ({ recipeId, folderName }: { recipeId: string; folderName: string }) => {
if (!userId) throw new Error("로그인 된 사용자가 없습니다.");
const alreadyScrapped = await isAlreadyScrapped(recipeId, userId);
if (alreadyScrapped) {
alert("이미 스크랩 한 레시피입니다.");
return false;
}
const { data: recipeData, error: fetchError } = await supabase
.from("TEST_TABLE")
.select("*")
.eq("post_id", recipeId)
.single();
if (fetchError) throw new Error(fetchError.message);
const { error: insertError } = await supabase.from("SCRAP_TABLE").insert({
user_id: userId,
scrap_id: recipeId,
folder_name: folderName,
scraped_recipe: JSON.stringify(recipeData),
created_at: new Date(),
updated_at: new Date()
});
if (insertError) throw new Error(insertError.message);
await incrementScrapCount(recipeId);
refetchFolders();
return true;
}
});
return {
existingFolders,
scraps,
refetchFolders,
refetchScraps,
incrementScrapCount,
saveScrap,
useFetchScrapCount
};
};
fetchFolders
userId를 받아 SCRAP_TABLE에서 폴더 목록을 가져온다.Set을 사용해 중복 폴더를 제거한다.isAlreadyScrapped
recipeId와 userId로 특정 레시피가 이미 스크랩되었는지 확인한다.fetchRecipeScrapCount
recipeId를 기준으로 TEST_TABLE에서 스크랩 수를 가져온다.fetchScraps
userId로 SCRAP_TABLE의 스크랩 데이터를 모두 가져온다.useScrapData
useScrapStore의 userId를 기반으로 여러 가지 쿼리와 mutation을 설정한다.
existingFolders, scraps, refetchFolders, refetchScraps, incrementScrapCount, saveScrap, useFetchScrapCount 등 다양한 스크랩 관련 기능을 제공한다.
오늘 TIL을 쓴 이유이기도 한... !! 의 표현이 뭔지 궁금했다.
튜터님도 종종 !! 두개를 썼던 것 같아서 간단하게 정리해보았다.
userId가 존재할 때에만 해당 쿼리가 활성화되도록 설정하는 것!
!!userId는 truthy나 falsy 여부를 명확하게 판단하기 위한 방법이다.!userId는 userId의 부정(null이나 undefined일 경우 true)이다.!를 붙여 !!userId로 만들면 userId가 존재하면 true를, 없으면 false를 반환하게 된다.enabled 옵션은 react-query의 옵션으로, true일 때에만 쿼리가 실행된다.userId가 존재할 때에만 쿼리가 활성화되도록 하여, userId가 null일 때 불필요한 쿼리 실행을 막아준다.따라서 enabled: !!userId는 userId가 정의된 경우에만 해당 쿼리가 실행되도록 설정하는 중요한 옵션이다.
!!