tanstack-query

star 1

TanStack Query 서버 상태 관리 가이드. useQuery, useMutation, queryKey 팩토리, Optimistic Update, Prefetching 등 서버 상태 관리 시 참조한다.

0r0loo By 0r0loo schedule Updated 2/28/2026

name: tanstack-query description: TanStack Query 서버 상태 관리 가이드. useQuery, useMutation, queryKey 팩토리, Optimistic Update, Prefetching 등 서버 상태 관리 시 참조한다.

TanStack Query Skill - 서버 상태 관리 규칙

TanStack Query (React Query)를 사용한 서버 상태 관리 규칙을 정의한다. 클라이언트 상태 관리는 ../Zustand/SKILL.md를 참고한다.


1. 기본 개념

서버 상태 vs 클라이언트 상태

  • 서버 상태: API에서 가져오는 데이터 (사용자 목록, 게시글 등)
  • 클라이언트 상태: UI에서만 존재하는 데이터 (모달 열림/닫힘, 폼 입력값 등)
  • TanStack Query는 서버 상태만 관리한다

Stale-While-Revalidate

  • 캐시된 데이터(stale)를 먼저 보여주고, 백그라운드에서 최신 데이터를 가져온다
  • 사용자 경험과 데이터 최신성을 동시에 확보한다

2. useQuery 패턴

기본 사용법

// Good
const { data, isLoading, error } = useQuery({
  queryKey: ['users', { status: 'active' }],
  queryFn: () => userApi.getUsers({ status: 'active' }),
});

// Good - enabled로 조건부 실행
const { data: user } = useQuery({
  queryKey: ['users', userId],
  queryFn: () => userApi.getUserById(userId),
  enabled: !!userId,
});

// Good - select로 데이터 변환
const { data: userNames } = useQuery({
  queryKey: ['users'],
  queryFn: () => userApi.getUsers(),
  select: (users) => users.map((user) => user.name),
});

queryKey 컨벤션

  • 배열 형태로 작성한다
  • 첫 번째 요소는 엔티티명 (복수형)
  • 이후 요소는 필터/파라미터
// 목록
['users']
['users', { status: 'active', page: 1 }]

// 단건
['users', userId]

// 관계 데이터
['users', userId, 'posts']

3. queryKey 팩토리 패턴

queryKey를 객체로 중앙 관리하여 일관성과 재사용성을 확보한다.

export const userKeys = {
  all: ['users'] as const,
  lists: () => [...userKeys.all, 'list'] as const,
  list: (filters: UserFilters) => [...userKeys.lists(), filters] as const,
  details: () => [...userKeys.all, 'detail'] as const,
  detail: (id: string) => [...userKeys.details(), id] as const,
};

// 사용
useQuery({
  queryKey: userKeys.detail(userId),
  queryFn: () => userApi.getUserById(userId),
});

// Invalidation - 모든 user 목록 캐시 무효화
queryClient.invalidateQueries({ queryKey: userKeys.lists() });

4. useMutation 패턴

기본 사용법

const createUser = useMutation({
  mutationFn: (data: CreateUserDto) => userApi.createUser(data),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: userKeys.lists() });
  },
  onError: (error) => {
    toast.error(`사용자 생성 실패: ${error.message}`);
  },
});

// 호출
createUser.mutate({ name: '홍길동', email: 'hong@example.com' });

onSuccess에서 반드시 캐시를 무효화한다

작업 invalidateQueries 대상
생성 목록 쿼리 (lists())
수정 목록 + 해당 상세 (all) 또는 정밀 지정
삭제 목록 쿼리 (lists())

5. Optimistic Update 패턴

사용자 경험을 위해 서버 응답 전에 UI를 먼저 업데이트한다.

const updateUser = useMutation({
  mutationFn: (data: UpdateUserDto) => userApi.updateUser(data),
  onMutate: async (newData) => {
    // 1. 진행 중인 쿼리 취소 (덮어쓰기 방지)
    await queryClient.cancelQueries({ queryKey: userKeys.detail(newData.id) });

    // 2. 이전 데이터 스냅샷 저장
    const previousUser = queryClient.getQueryData(userKeys.detail(newData.id));

    // 3. 캐시를 낙관적으로 업데이트
    queryClient.setQueryData(userKeys.detail(newData.id), (old: User) => ({
      ...old,
      ...newData,
    }));

    // 4. 롤백용 컨텍스트 반환
    return { previousUser };
  },
  onError: (_error, newData, context) => {
    // 에러 시 이전 데이터로 롤백
    queryClient.setQueryData(userKeys.detail(newData.id), context?.previousUser);
  },
  onSettled: (_data, _error, variables) => {
    // 성공/실패 무관하게 캐시 재검증
    queryClient.invalidateQueries({ queryKey: userKeys.detail(variables.id) });
  },
});

6. Prefetching

라우트 전환 시 Prefetching

// 마우스 호버 시 미리 데이터를 가져온다
function UserListItem({ userId }: { userId: string }) {
  const queryClient = useQueryClient();

  const handleMouseEnter = () => {
    queryClient.prefetchQuery({
      queryKey: userKeys.detail(userId),
      queryFn: () => userApi.getUserById(userId),
      staleTime: 1000 * 60 * 5, // 5분 동안 재요청 방지
    });
  };

  return (
    <Link to={`/users/${userId}`} onMouseEnter={handleMouseEnter}>
      사용자 상세
    </Link>
  );
}

7. Suspense 모드

React Suspense와 함께 사용하여 로딩 처리를 선언적으로 한다.

// useSuspenseQuery는 data가 항상 존재함을 보장한다
function UserProfile({ userId }: { userId: string }) {
  const { data: user } = useSuspenseQuery({
    queryKey: userKeys.detail(userId),
    queryFn: () => userApi.getUserById(userId),
  });

  // user는 undefined가 아님 - 타입 안전
  return <div>{user.name}</div>;
}

// 부모에서 Suspense로 감싼다
function UserPage({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<UserProfileSkeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}

8. QueryClient 설정

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,    // 5분 - 데이터가 stale로 간주되기까지의 시간
      gcTime: 1000 * 60 * 30,      // 30분 - 미사용 캐시 제거까지의 시간
      retry: 1,                     // 실패 시 1회 재시도
      refetchOnWindowFocus: false,  // 윈도우 포커스 시 자동 refetch 비활성화
    },
  },
});
옵션 권장값 설명
staleTime 5분 짧으면 요청 과다, 길면 데이터 지연
gcTime 30분 staleTime보다 길어야 한다
retry 1 네트워크 오류 대비 최소 재시도
refetchOnWindowFocus false 프로젝트 요구사항에 따라 조정

9. 캐시 전략 가이드

데이터 특성에 따라 staleTime을 다르게 설정한다.

데이터 유형 staleTime 예시
거의 안 바뀜 Infinity 코드 테이블, 카테고리 목록, 약관
가끔 바뀜 10~30분 사용자 프로필, 설정
자주 바뀜 1~5분 게시글 목록, 댓글
실시간 필요 0 채팅, 알림, 재고 수량

도메인별 staleTime 설정

// queryKey 팩토리에서 기본 옵션을 함께 관리
export const categoryKeys = {
  all: ['categories'] as const,
  list: () => [...categoryKeys.all, 'list'] as const,
};

// 거의 안 바뀌는 데이터
useQuery({
  queryKey: categoryKeys.list(),
  queryFn: fetchCategories,
  staleTime: Infinity,
});

// 자주 바뀌는 데이터
useQuery({
  queryKey: postKeys.list(filters),
  queryFn: () => fetchPosts(filters),
  staleTime: 1000 * 60,  // 1분
});

실시간 데이터: refetchInterval 사용

// 폴링 방식 (WebSocket이 없을 때)
useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  staleTime: 0,
  refetchInterval: 1000 * 30,  // 30초마다 재요청
});

10. 금지 사항

  • useEffect로 데이터 페칭 금지 - TanStack Query를 사용한다
  • queryKey 하드코딩 금지 - queryKey 팩토리 패턴을 사용한다
  • 불필요한 refetchOnWindowFocus: true 설정 금지 - 기본값 대신 명시적으로 관리한다
  • onSuccess 콜백 안에서 상태 동기화 금지 (useState에 서버 데이터 복사 등)
  • queryFn 안에서 에러를 삼키는 try-catch 금지 - TanStack Query가 에러를 관리하게 한다
  • cacheTime 사용 금지 - v5부터 gcTime으로 변경되었다
Install via CLI
npx skills add https://github.com/0r0loo/choblue --skill tanstack-query
Repository Details
star Stars 1
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator