Sonamu์ ์๋ ์์ฑ๋ TanStack Query Hook์ ์ฌ์ฉํ์ฌ React์์ ํ์
์์ ํ๊ณ ํจ์จ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ด
๋๋ค.
Sonamu + TanStack Query ๊ฐ์
์๋ ์์ฑ
useUser, usePost Hook ์ฝ๋ ์์ฑ ๋ถํ์
ํ์
์์
Service ํ์
์ ์ง ์์ ํ ํ์
์ฒด์ธ
์๋ ์บ์ฑ
๋ฉ๋ชจ๋ฆฌ ์บ์ ์ค๋ณต ์์ฒญ ์ ๊ฑฐ
์๋ ์ฌ๊ฒ์ฆ
ํฌ์ปค์ค ์ ๊ฐฑ์ ์ฃผ๊ธฐ์ ํด๋ง
Sonamu์์ TanStack Query Hook ์ฌ์ฉํ๊ธฐ
Hook ์๋ ์์ฑ
Sonamu๋ ๊ฐ Service ํจ์๋ง๋ค TanStack Query Hook์ ์๋ ์์ฑํฉ๋๋ค.
์์ฑ๋๋ Hook (services.generated.ts):
import { useQuery, queryOptions } from "@tanstack/react-query";
import qs from "qs";
export namespace UserService {
// 1. ์ผ๋ฐ ํจ์
export async function getUser<T extends UserSubsetKey>(
subset: T,
id: number,
): Promise<UserSubsetMapping[T]> {
return fetch({
method: "GET",
url: `/api/user/findById?${qs.stringify({ subset, id })}`,
});
}
// 2. Query Options (์ฌ์ฌ์ฉ ๊ฐ๋ฅ)
export const getUserQueryOptions = <T extends UserSubsetKey>(subset: T, id: number) =>
queryOptions({
queryKey: ["User", "getUser", subset, id],
queryFn: () => getUser(subset, id),
});
// 3. React Hook (์๋ ์์ฑ)
export const useUser = <T extends UserSubsetKey>(
subset: T,
id: number,
options?: { enabled?: boolean },
) =>
useQuery({
...getUserQueryOptions(subset, id),
...options,
});
}
์์ฑ ๊ท์น:
- ํจ์๋ช
:
get{Entity} โ Hook๋ช
: use{Entity}
- Query Key:
["{Entity}", "{methodName}", ...params]
- ํ์
์์ ๋ณด์กด (Subset ์ง์)
๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
useUser Hook
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ๋ฐ์ดํฐ ์กฐํ Hook์
๋๋ค.
import { UserService } from "@/services/services.generated";
function UserProfile({ userId }: { userId: number }) {
// ์๋ ์์ฑ๋ Hook ์ฌ์ฉ
const { data, error, isLoading } = UserService.useUser("A", userId);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return <div>User not found</div>;
return (
<div>
<h1>{data.username}</h1>
<p>{data.email}</p>
</div>
);
}
ํน์ง:
data๋ UserSubsetMapping["A"] | undefined ํ์
- ์๋ ์บ์ฑ: ๊ฐ์ userId๋ก ๋ค์ ํธ์ถํ๋ฉด ์บ์ ์ฌ์ฉ
- ์๋ ์ฌ๊ฒ์ฆ: ํฌ์ปค์ค ์, ์ฌ์ฐ๊ฒฐ ์ ์๋ ๊ฐฑ์
- ์ค๋ณต ์ ๊ฑฐ: ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๋์์ ํธ์ถํด๋ API๋ 1๋ฒ๋ง
Subset๊ณผ ํ์
์์ ์ฑ
Subset์ ๋ฐ๋ผ ์ ํํ ํ์
์ด ๋ฐํ๋ฉ๋๋ค.
function UserProfile({ userId }: { userId: number }) {
// Subset "A": ๊ธฐ๋ณธ ์ ๋ณด
const { data: basicUser } = UserService.useUser("A", userId);
console.log(basicUser?.id); // โ
OK
console.log(basicUser?.username); // โ
OK
console.log(basicUser?.bio); // โ ์ปดํ์ผ ์๋ฌ (Subset A์ ์์)
// Subset "B": bio ํฌํจ
const { data: userWithBio } = UserService.useUser("B", userId);
console.log(userWithBio?.bio); // โ
OK
// Subset "C": ์ ์ฒด ํ๋
const { data: fullUser } = UserService.useUser("C", userId);
console.log(fullUser?.createdAt); // โ
OK
}
ํ์
์ฒด์ธ:
Backend Entity
โ Subset Mapping
โ Service Function
โ TanStack Query Hook
โ React Component
๋ชจ๋ ๋จ๊ณ์์ ํ์
์ด ๋ณด์กด๋ฉ๋๋ค!
๊ณ ๊ธ ์ฌ์ฉ๋ฒ
์กฐ๊ฑด๋ถ ํ์นญ
ํน์ ์กฐ๊ฑด์์๋ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
function UserProfile({ userId }: { userId: number | null }) {
const { data, isLoading } = UserService.useUser(
"A",
userId!,
{ enabled: userId !== null } // userId๊ฐ null์ด๋ฉด ํธ์ถ ์ํจ
);
if (!userId) return <div>Please select a user</div>;
if (isLoading) return <div>Loading...</div>;
if (!data) return <div>User not found</div>;
return <div>{data.username}</div>;
}
enabled ์ต์
:
true: ์ฆ์ ๋ฐ์ดํฐ ๋ก๋ (๊ธฐ๋ณธ๊ฐ)
false: ๋ฐ์ดํฐ ๋ก๋ํ์ง ์์
- ์ฌ์ฉ ์ฌ๋ก: ๋ก๊ทธ์ธ ์ฌ๋ถ, ํ๋ผ๋ฏธํฐ ์กด์ฌ ์ฌ๋ถ ๋ฑ
์์กด์ ํ์นญ
์ด์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ์์ฒญ์ ์ฌ์ฉํฉ๋๋ค.
function UserDashboard({ userId }: { userId: number }) {
// 1๋จ๊ณ: ์ฌ์ฉ์ ์ ๋ณด
const { data: user } = UserService.useUser("A", userId);
// 2๋จ๊ณ: ์ฌ์ฉ์์ ๊ฒ์๊ธ (user๊ฐ ์์ ๋๋ง)
const { data: posts } = PostService.usePosts(
"A",
user ? user.id : 0,
{ enabled: !!user } // user๊ฐ ์์ ๋๋ง ํธ์ถ
);
return (
<div>
<h1>{user?.username}</h1>
<div>Posts: {posts?.length || 0}</div>
</div>
);
}
Query Options ์ฌ์ฌ์ฉ
๋์ผํ Query Options๋ฅผ ์ฌ๋ฌ ๊ณณ์์ ์ฌ์ฌ์ฉํฉ๋๋ค.
import { UserService } from "@/services/services.generated";
import { useQueryClient } from "@tanstack/react-query";
function UserProfile({ userId }: { userId: number }) {
const queryClient = useQueryClient();
// Options ์ฌ์ฌ์ฉ
const queryOptions = UserService.getUserQueryOptions("A", userId);
// 1. Hook์์ ์ฌ์ฉ
const { data } = useQuery(queryOptions);
// 2. ์๋ refetch
function handleRefresh() {
queryClient.invalidateQueries(queryOptions);
}
// 3. Prefetch
function handleMouseEnter() {
queryClient.prefetchQuery(queryOptions);
}
return <div onMouseEnter={handleMouseEnter}>{data?.username}</div>;
}
Mutation (๋ฐ์ดํฐ ๋ณ๊ฒฝ)
useMutation์ผ๋ก ์์ฑ/์์ /์ญ์
TanStack Query์ useMutation์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํฉ๋๋ค.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/services.generated";
function EditProfile({ userId }: { userId: number }) {
const queryClient = useQueryClient();
// Mutation ์ ์
const updateMutation = useMutation({
mutationFn: (data: { username?: string; bio?: string }) =>
UserService.updateProfile(data),
onSuccess: () => {
// ์ฑ๊ณต ์ User ์ฟผ๋ฆฌ ๋ฌดํจํ
queryClient.invalidateQueries({
queryKey: ["User"],
});
},
});
async function handleSubmit(data: { username: string; bio: string }) {
await updateMutation.mutateAsync(data);
alert("Updated!");
}
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({/* data */});
}}>
<input name="username" />
<textarea name="bio" />
<button type="submit" disabled={updateMutation.isPending}>
{updateMutation.isPending ? "Saving..." : "Save"}
</button>
</form>
);
}
๋๊ด์ ์
๋ฐ์ดํธ
API ์๋ต์ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ์ฆ์ UI๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
function EditProfile({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const updateMutation = useMutation({
mutationFn: (data: { username: string }) =>
UserService.updateProfile(data),
// 1. Mutation ์์ ์ (๋๊ด์ ์
๋ฐ์ดํธ)
onMutate: async (newData) => {
// ์งํ ์ค์ธ ์ฟผ๋ฆฌ ์ทจ์
await queryClient.cancelQueries(
UserService.getUserQueryOptions("A", userId)
);
// ์ด์ ๋ฐ์ดํฐ ๋ฐฑ์
const previousUser = queryClient.getQueryData(
UserService.getUserQueryOptions("A", userId).queryKey
);
// ๋๊ด์ ์ผ๋ก ์บ์ ์
๋ฐ์ดํธ
queryClient.setQueryData(
UserService.getUserQueryOptions("A", userId).queryKey,
(old: any) => ({ ...old, username: newData.username })
);
return { previousUser };
},
// 2. ์ฑ๊ณต ์
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["User"],
});
},
// 3. ์คํจ ์ (๋กค๋ฐฑ)
onError: (err, newData, context) => {
queryClient.setQueryData(
UserService.getUserQueryOptions("A", userId).queryKey,
context?.previousUser
);
},
});
return (
<button onClick={() => updateMutation.mutate({ username: "newname" })}>
Update Username
</button>
);
}
์ฌ์ฉ์ ๊ฒฝํ:
- ๋ฒํผ ํด๋ฆญ โ ์ฆ์ UI ์
๋ฐ์ดํธ (๋๊ธฐ ์์)
- ๋ฐฑ๊ทธ๋ผ์ด๋์์ API ํธ์ถ
- ์ฑ๊ณต ์ ์๋ฒ ๋ฐ์ดํฐ๋ก ํ์
- ์คํจ ์ ์๋ ๋กค๋ฐฑ
์บ์ ๊ด๋ฆฌ
์บ์ ๋ฌดํจํ
ํน์ ์ฟผ๋ฆฌ์ ์บ์๋ฅผ ๋ฌดํจํํ์ฌ ์ฌ๋ก๋ํฉ๋๋ค.
import { useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/services.generated";
function SomeComponent({ userId }: { userId: number }) {
const queryClient = useQueryClient();
function handleUpdate() {
// 1. ํน์ User ์ฟผ๋ฆฌ๋ง ๋ฌดํจํ
queryClient.invalidateQueries(UserService.getUserQueryOptions("A", userId));
// 2. ๋ชจ๋ User ์ฟผ๋ฆฌ ๋ฌดํจํ
queryClient.invalidateQueries({
queryKey: ["User"],
});
// 3. ํน์ ๋ฉ์๋์ ๋ชจ๋ ์ฟผ๋ฆฌ ๋ฌดํจํ
queryClient.invalidateQueries({
queryKey: ["User", "getUser"],
});
}
}
์บ์ ์ง์ ์์
API ํธ์ถ ์์ด ์บ์๋ฅผ ์ง์ ์์ ํฉ๋๋ค.
function LikeButton({ postId }: { postId: number }) {
const queryClient = useQueryClient();
function handleLike() {
// ์บ์ ์ง์ ์์
queryClient.setQueryData(
PostService.getPostQueryOptions("A", postId).queryKey,
(old: any) => ({
...old,
likes: old.likes + 1,
isLiked: true,
})
);
// ๋ฐฑ๊ทธ๋ผ์ด๋์์ API ํธ์ถ
PostService.likePost(postId);
}
return <button onClick={handleLike}>Like</button>;
}
Prefetching
๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํ์ฌ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํต๋๋ค.
function UserList({ userIds }: { userIds: number[] }) {
const queryClient = useQueryClient();
// ํธ๋ฒ ์ ๋ฏธ๋ฆฌ ๋ก๋
function handleMouseEnter(userId: number) {
queryClient.prefetchQuery(
UserService.getUserQueryOptions("A", userId)
);
}
return (
<ul>
{userIds.map((id) => (
<li
key={id}
onMouseEnter={() => handleMouseEnter(id)}
>
<Link to={`/users/${id}`}>User {id}</Link>
</li>
))}
</ul>
);
}
์ค์ ์์
๋ฌดํ ์คํฌ๋กค
ํ์ด์ง๋ค์ด์
๋์ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํฉ๋๋ค.
import { useInfiniteQuery } from "@tanstack/react-query";
import { PostService } from "@/services/services.generated";
function PostList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery({
queryKey: ["Post", "list"],
queryFn: ({ pageParam = 1 }) =>
PostService.getPosts("A", { page: pageParam, pageSize: 20 }),
getNextPageParam: (lastPage, pages) => {
return lastPage.length === 20 ? pages.length + 1 : undefined;
},
initialPageParam: 1,
});
// ๋ชจ๋ ํ์ด์ง์ ๊ฒ์๊ธ์ ํํํ
const posts = data?.pages.flat() || [];
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? "Loading..." : "Load More"}
</button>
)}
</div>
);
}
์ค์๊ฐ ํด๋ง
์ฃผ๊ธฐ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐฑ์ ํฉ๋๋ค.
function RealtimeFeed() {
const { data } = FeedService.useLatest(
"A",
{},
{
refetchInterval: 3000, // 3์ด๋ง๋ค ๊ฐฑ์
refetchIntervalInBackground: false, // ๋ฐฑ๊ทธ๋ผ์ด๋์์๋ ์ํจ
}
);
return (
<div>
{data?.items.map((item) => (
<div key={item.id}>{item.content}</div>
))}
</div>
);
}
์ฌ๋ฌ Hook ์กฐํฉ
function UserDashboard({ userId }: { userId: number }) {
// ๋ณ๋ ฌ๋ก ์ฌ๋ฌ ๋ฐ์ดํฐ ๋ก๋
const userQuery = UserService.useUser("A", userId);
const postsQuery = PostService.usePostsByUser("A", userId);
const statsQuery = UserService.useStats(userId);
// ๋ชจ๋ ์ฟผ๋ฆฌ๊ฐ ๋ก๋๋ ๋๊น์ง ๋๊ธฐ
if (userQuery.isLoading || postsQuery.isLoading || statsQuery.isLoading) {
return <div>Loading...</div>;
}
// ํ๋๋ผ๋ ์๋ฌ๋ฉด ์๋ฌ ํ์
if (userQuery.error || postsQuery.error || statsQuery.error) {
return <div>Error loading data</div>;
}
return (
<div>
<h1>{userQuery.data?.username}</h1>
<div>Posts: {postsQuery.data?.length}</div>
<div>Views: {statsQuery.data?.totalViews}</div>
</div>
);
}
TanStack Query ์ค์
์ ์ญ ์ค์
// app/providers.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// ๋ฐ์ดํฐ๊ฐ stale ์ํ๋ก ๊ฐ์ฃผ๋๋ ์๊ฐ (5์ด)
staleTime: 5000,
// ํฌ์ปค์ค ์ ์ฌ๊ฒ์ฆ
refetchOnWindowFocus: true,
// ์ฌ์ฐ๊ฒฐ ์ ์ฌ๊ฒ์ฆ
refetchOnReconnect: true,
// ์๋ฌ ์ฌ์๋
retry: 1,
// ์ฌ์ฉํ์ง ์๋ ๋ฐ์ดํฐ ์บ์ ์ ์ง ์๊ฐ (5๋ถ)
gcTime: 5 * 60 * 1000,
},
},
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
}
๊ฐ๋ฐ ์ค ์บ์ ์ํ๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์ธํฉ๋๋ค.
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
์ฑ๋ฅ ์ต์ ํ
Subset์ผ๋ก ์ต์ ํ
์ํฉ์ ๋ง๋ Subset์ ์ ํํ์ฌ ๋คํธ์ํฌ ๋น์ฉ ์ ๊ฐ:
// ๋ชฉ๋ก ํ๋ฉด: ์ต์ ์ ๋ณด๋ง
const { data: users } = UserService.useUsers("A");
// ์์ธ ํ๋ฉด: ์ ์ฒด ์ ๋ณด
const { data: user } = UserService.useUser("C", userId);
์ ํ์ ์ฌ๊ฒ์ฆ
๋ถํ์ํ ์ฌ๊ฒ์ฆ์ ๋ง์ต๋๋ค:
// ์์ฃผ ๋ณํ์ง ์๋ ๋ฐ์ดํฐ
const { data } = UserService.useUser("A", userId, {
staleTime: Infinity, // ์์ํ fresh
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
๋ณ๋ ฌ ์์ฒญ
์ฌ๋ฌ ๋ฐ์ดํฐ๋ฅผ ๋์์ ๋ก๋:
function Dashboard({ userId }: { userId: number }) {
// ๋ชจ๋ Hook์ด ๋ณ๋ ฌ๋ก ์คํ๋จ
const { data: user } = UserService.useUser("A", userId);
const { data: posts } = PostService.usePosts("A", { userId });
const { data: stats } = StatsService.useUserStats(userId);
// ์ธ ์์ฒญ์ด ๋์์ ์คํ๋์ด ๋น ๋ฆ!
}
์ฃผ์์ฌํญ
TanStack Query Hook ์ฌ์ฉ ์ ์ฃผ์์ฌํญ: 1. Hook์ ์ปดํฌ๋ํธ ๋ด๋ถ์์๋ง ํธ์ถ (React ๊ท์น) 2.
Subset ํ๋ผ๋ฏธํฐ ํ์: useUser("A", userId) 3. enabled: false๋ก ์กฐ๊ฑด๋ถ ํ์นญ ๊ตฌํ 4.
Mutation ์ฑ๊ณต ์ ์บ์ ๋ฌดํจํ ํ์ 5. Query Key๋ ์๋ ์์ฑ๋๋ฏ๋ก ์๋ ์์ฑ ๊ธ์ง 6. ์์ฑ๋
Hook์ ์์ ๊ธ์ง (์ฌ์์ฑ ์ ๋ฎ์ด์) 7. QueryClientProvider๋ก ์ฑ ์ ์ฒด ๊ฐ์ธ๊ธฐ
๋ค์ ๋จ๊ณ
Service ๋์ ์๋ฆฌ
์๋ ์์ฑ ์ดํดํ๊ธฐ
Service ์ฌ์ฉํ๊ธฐ
Service ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
TanStack Query ๊ณต์ ๋ฌธ์
TanStack Query ๋ ์์๋ณด๊ธฐ
Subset System
Subset ์์คํ