간단할 줄 알았는데 코드가 엄청나게 길어졌다..!
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
가 정의된 경우에만 해당 쿼리가 실행되도록 설정하는 중요한 옵션이다.
!!