> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# TanStack Query Hook

> Sonamu Service와 TanStack Query 통합

Sonamu의 자동 생성된 TanStack Query Hook을 사용하여 React에서 타입 안전하고 효율적으로 데이터를 관리하는 방법을 알아봅니다.

## Sonamu + TanStack Query 개요

<CardGroup cols={2}>
  <Card title="자동 생성" icon="wand-magic-sparkles">
    useUser, usePost Hook 코드 작성 불필요
  </Card>

  <Card title="타입 안전" icon="shield-check">
    Service 타입 유지 완전한 타입 체인
  </Card>

  <Card title="자동 캐싱" icon="database">
    메모리 캐시 중복 요청 제거
  </Card>

  <Card title="자동 재검증" icon="arrows-rotate">
    포커스 시 갱신 주기적 폴링
  </Card>
</CardGroup>

## Sonamu에서 TanStack Query Hook 사용하기

### Hook 자동 생성

Sonamu는 각 Service 함수마다 **TanStack Query Hook을 자동 생성**합니다.

**생성되는 Hook (services.generated.ts)**:

```typescript theme={null}
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입니다.

```typescript theme={null}
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에 따라 정확한 타입이 반환됩니다.

```typescript theme={null}
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
```

모든 단계에서 타입이 보존됩니다!

## 고급 사용법

### 조건부 페칭

특정 조건에서만 데이터를 가져옵니다.

```typescript theme={null}
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`: 데이터 로드하지 않음
* 사용 사례: 로그인 여부, 파라미터 존재 여부 등

### 의존적 페칭

이전 데이터를 다음 요청에 사용합니다.

```typescript theme={null}
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를 여러 곳에서 재사용합니다.

```typescript theme={null}
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`으로 데이터를 변경합니다.

```typescript theme={null}
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를 업데이트합니다.

```typescript theme={null}
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 호출
* 성공 시 서버 데이터로 확정
* 실패 시 자동 롤백

## 캐시 관리

### 캐시 무효화

특정 쿼리의 캐시를 무효화하여 재로드합니다.

```typescript theme={null}
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 호출 없이 캐시를 직접 수정합니다.

```typescript theme={null}
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

데이터를 미리 로드하여 사용자 경험을 향상시킵니다.

```typescript theme={null}
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>
  );
}
```

## 실전 예제

### 무한 스크롤

페이지네이션 대신 무한 스크롤을 구현합니다.

```typescript theme={null}
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>
  );
}
```

### 실시간 폴링

주기적으로 데이터를 갱신합니다.

```typescript theme={null}
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 조합

```typescript theme={null}
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 설정

### 전역 설정

```typescript theme={null}
// 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

개발 중 캐시 상태를 시각적으로 확인합니다.

```typescript theme={null}
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을 선택하여 네트워크 비용 절감:

```typescript theme={null}
// 목록 화면: 최소 정보만
const { data: users } = UserService.useUsers("A");

// 상세 화면: 전체 정보
const { data: user } = UserService.useUser("C", userId);
```

### 선택적 재검증

불필요한 재검증을 막습니다:

```typescript theme={null}
// 자주 변하지 않는 데이터
const { data } = UserService.useUser("A", userId, {
  staleTime: Infinity, // 영원히 fresh
  refetchOnWindowFocus: false,
  refetchOnReconnect: false,
});
```

### 병렬 요청

여러 데이터를 동시에 로드:

```typescript theme={null}
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);

  // 세 요청이 동시에 실행되어 빠름!
}
```

## 주의사항

<Warning>
  **TanStack Query Hook 사용 시 주의사항**: 1. Hook은 **컴포넌트 내부**에서만 호출 (React 규칙) 2.
  **Subset 파라미터 필수**: `useUser("A", userId)` 3. `enabled: false`로 **조건부 페칭** 구현 4.
  Mutation 성공 시 **캐시 무효화** 필수 5. Query Key는 **자동 생성**되므로 수동 작성 금지 6. 생성된
  Hook은 **수정 금지** (재생성 시 덮어씀) 7. `QueryClientProvider`로 앱 전체 감싸기
</Warning>

## 다음 단계

<CardGroup cols={2}>
  <Card title="Service 동작 원리" icon="diagram-project" href="/ko/frontend-integration/generated-services/how-services-work">
    자동 생성 이해하기
  </Card>

  <Card title="Service 사용하기" icon="code" href="/ko/frontend-integration/generated-services/using-services">
    Service 기본 사용법
  </Card>

  <Card title="TanStack Query 공식 문서" icon="book" href="https://tanstack.com/query/latest">
    TanStack Query 더 알아보기
  </Card>

  <Card title="Subset System" icon="layer-group" href="/ko/core-concepts/entity-system/subset-system">
    Subset 시스템
  </Card>
</CardGroup>
