๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
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>
  );
}

DevTools

๊ฐœ๋ฐœ ์ค‘ ์บ์‹œ ์ƒํƒœ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
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๋กœ ์•ฑ ์ „์ฒด ๊ฐ์‹ธ๊ธฐ

๋‹ค์Œ ๋‹จ๊ณ„