포스팅 목차
- React Query의 캐싱이란?
- staleTime과 gcTime — 가장 중요한 두 가지 개념
- queryKey 설계 전략
- prefetchQuery / initialData / placeholderData
- 공부하면서 생겼던 의문점
- 참고 자료
1. React Query의 캐싱이란?
React Query는 서버에서 받아온 데이터를 메모리에 저장(캐싱) 해두고, 동일한 요청이 들어오면 서버에 다시 요청하지 않고 캐시에서 꺼내 쓰는 방식으로 동작합니다.
덕분에 불필요한 네트워크 요청을 줄이고, 사용자 입장에서는 화면이 더 빠르게 그려지는 경험을 할 수 있습니다.
React Query 캐싱의 핵심은 아래 두 가지 질문으로 요약됩니다.
- 이 데이터가 아직 신선한가(fresh)? → 캐시를 그대로 쓸 것인가, 서버에 재요청할 것인가
- 이 데이터를 언제 메모리에서 지울 것인가? → 캐시를 언제 버릴 것인가
이 두 가지를 제어하는 옵션이 바로 staleTime과 gcTime입니다.
2. staleTime과 gcTime — 가장 중요한 두 가지 개념
데이터의 상태 흐름
React Query에서 데이터는 아래 순서로 상태가 변합니다.
fetch 성공
↓
fresh (신선한 상태) ──── staleTime 경과 ────→ stale (오래된 상태)
↓
컴포넌트 언마운트 (사용자 없음)
↓
inactive 상태
↓
gcTime 경과 후 메모리에서 제거
staleTime
staleTime은 데이터가 fresh 상태를 유지하는 시간입니다.
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5, // 5분 동안 fresh 상태 유지
});
staleTime이내 → 데이터가fresh상태 → 서버에 재요청하지 않고 캐시를 그대로 사용staleTime경과 → 데이터가stale상태 → 다음 요청 시 백그라운드에서 서버에 재요청
💡 기본값은
0입니다. 즉, 기본적으로 데이터를 fetch하는 즉시stale상태가 됩니다.
staleTime을 언제 늘려야 할까요?
| 데이터 종류 | 추천 staleTime | 이유 |
|---|---|---|
| 실시간 데이터 (주가, 채팅) | 0 |
항상 최신 데이터 필요 |
| 사용자 프로필 | 1000 * 60 * 5 (5분) |
자주 바뀌지 않음 |
| 공지사항, 카테고리 목록 | 1000 * 60 * 60 (1시간) |
거의 바뀌지 않음 |
| 정적 설정값 | Infinity |
앱 실행 중 변하지 않음 |
gcTime (구 cacheTime)
gcTime은 데이터가 메모리에서 유지되는 시간입니다.
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
gcTime: 1000 * 60 * 10, // 10분 후 메모리에서 제거
});
컴포넌트가 언마운트되어 해당 쿼리를 사용하는 구독자가 없어지면 데이터는 inactive 상태가 됩니다.
이 inactive 상태에서 gcTime이 경과하면 메모리에서 완전히 제거됩니다.
💡 기본값은
1000 * 60 * 5(5분) 입니다.
💡 React Query v5에서
cacheTime이gcTime으로 이름이 바뀌었습니다. GC는 Garbage Collection의 약자입니다.
staleTime vs gcTime 핵심 차이
두 개념이 헷갈리는 경우가 많은데, 아래 표로 정리하면 명확합니다.
| 구분 | staleTime | gcTime |
|---|---|---|
| 역할 | 데이터의 신선도 유지 시간 | 메모리에서 데이터 보존 시간 |
| 경과 후 동작 | 백그라운드 재요청 발생 | 캐시 데이터 완전 삭제 |
| 기본값 | 0 (즉시 stale) |
5분 |
| 관련 상태 | fresh → stale | inactive → 삭제 |
중요한 규칙이 하나 있습니다.
gcTime은 항상 staleTime보다 크거나 같아야 합니다.
만약 staleTime이 gcTime보다 크면, 데이터가 아직 fresh 상태인데 메모리에서 먼저 삭제되는 상황이 발생할 수 있습니다.
// ❌ 잘못된 설정 — staleTime이 gcTime보다 큰 경우
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 10, // 10분
gcTime: 1000 * 60 * 5, // 5분 → staleTime보다 작으면 안 됨
});
// ✅ 올바른 설정
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5, // 5분
gcTime: 1000 * 60 * 10, // 10분 → staleTime보다 크거나 같아야 함
});
3. queryKey 설계 전략
queryKey란?
queryKey는 React Query에서 캐시의 식별자 역할을 합니다.
같은 queryKey를 사용하는 쿼리는 동일한 캐시를 공유합니다.
// 이 두 쿼리는 같은 캐시를 공유합니다
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
queryKey는 배열로 작성합니다
queryKey는 항상 배열 형태로 작성하는 것이 컨벤션입니다.
// X 문자열로만 작성 (권장하지 않음)
useQuery({ queryKey: "posts", queryFn: fetchPosts });
// O 배열로 작성
useQuery({ queryKey: ["posts"], queryFn: fetchPosts });
계층적으로 설계합니다
queryKey를 계층적으로 설계하면 관련된 캐시를 한 번에 무효화하거나 관리하기 쉬워집니다.
// 게시글 관련 queryKey 계층 구조
["posts"] // 게시글 전체 목록
["posts", "list"] // 게시글 목록 (필터 없음)
["posts", "list", { page: 1 }] // 1페이지 게시글 목록
["posts", "detail", 1] // id=1 게시글 상세
["posts", "detail", 2] // id=2 게시글 상세
이렇게 설계하면 게시글 관련 캐시를 한 번에 무효화할 수 있습니다.
// "posts"로 시작하는 모든 캐시 무효화
queryClient.invalidateQueries({ queryKey: ["posts"] });
// 게시글 목록만 무효화
queryClient.invalidateQueries({ queryKey: ["posts", "list"] });
// id=1 게시글 상세만 무효화
queryClient.invalidateQueries({ queryKey: ["posts", "detail", 1] });
queryKey Factory 패턴
프로젝트가 커질수록 queryKey를 문자열로 직접 관리하면 오타나 불일치가 생기기 쉽습니다.
queryKey Factory 패턴으로 중앙 관리하는 것을 권장합니다.
// src/queries/postQueryKeys.ts
export const postQueryKeys = {
// 최상위 키
all: ["posts"] as const,
// 목록 관련
lists: () => [...postQueryKeys.all, "list"] as const,
list: (filters: { page: number; limit: number }) =>
[...postQueryKeys.lists(), filters] as const,
// 상세 관련
details: () => [...postQueryKeys.all, "detail"] as const,
detail: (id: number) => [...postQueryKeys.details(), id] as const,
};
// 사용할 때
useQuery({
queryKey: postQueryKeys.list({ page: 1, limit: 10 }),
queryFn: () => fetchPosts({ page: 1, limit: 10 }),
});
useQuery({
queryKey: postQueryKeys.detail(1),
queryFn: () => fetchPost(1),
});
// 무효화할 때
queryClient.invalidateQueries({ queryKey: postQueryKeys.all });
4. prefetchQuery / initialData / placeholderData
세 가지 모두 데이터를 미리 제공해서 로딩 상태를 줄이는 방법입니다.
하지만 사용 목적과 동작 방식이 다릅니다.
prefetchQuery
prefetchQuery는 사용자가 해당 페이지나 컴포넌트에 도달하기 전에 미리 서버에서 데이터를 fetch해서 캐시에 저장해두는 방법입니다.
const queryClient = useQueryClient();
// 마우스를 버튼에 올렸을 때 미리 fetch
const handleMouseEnter = () => {
queryClient.prefetchQuery({
queryKey: postQueryKeys.detail(postId),
queryFn: () => fetchPost(postId),
staleTime: 1000 * 60, // 이미 캐시가 있고 1분 이내면 재요청 안 함
});
};
return (
<button onMouseEnter={handleMouseEnter} onClick={handleClick}>
게시글 보기
</button>
);
사용자가 버튼을 클릭해서 상세 페이지로 이동했을 때, 이미 캐시에 데이터가 있으면 로딩 없이 바로 화면이 그려집니다.
prefetchQuery가 효과적인 상황
- 사용자가 다음에 볼 가능성이 높은 데이터를 미리 로드할 때
- 리스트 → 상세 페이지 이동 시 로딩을 없애고 싶을 때
- Next.js에서 서버 컴포넌트에서 미리 데이터를 채워둘 때
initialData
initialData는 쿼리 캐시에 초기값을 직접 넣어두는 방법입니다.
이미 다른 쿼리에서 가져온 데이터 중 일부를 재활용할 때 유용합니다.
// 목록 쿼리에서 이미 가져온 데이터
const { data: postList } = useQuery({
queryKey: postQueryKeys.list({ page: 1, limit: 10 }),
queryFn: fetchPosts,
});
// 상세 쿼리의 initialData로 목록의 특정 항목을 미리 제공
const { data: post } = useQuery({
queryKey: postQueryKeys.detail(postId),
queryFn: () => fetchPost(postId),
// 목록에서 해당 게시글을 찾아서 초기값으로 사용
initialData: () => postList?.posts.find((p) => p.id === postId),
// initialData가 언제 만들어진 건지 알려줌
// 목록 쿼리의 데이터 업데이트 시간을 기준으로 신선도 판단
initialDataUpdatedAt: () =>
queryClient.getQueryState(postQueryKeys.list({ page: 1, limit: 10 }))
?.dataUpdatedAt,
});
💡
initialData는 캐시에 실제로 저장됩니다. 따라서initialDataUpdatedAt을 함께 설정해서 React Query가 신선도를 올바르게 판단할 수 있도록 해야 합니다.
placeholderData
placeholderData는 실제 데이터가 오기 전까지 임시로 보여줄 데이터를 제공하는 방법입니다.initialData와 달리 캐시에 저장되지 않습니다.
const { data, isPlaceholderData } = useQuery({
queryKey: postQueryKeys.list({ page: currentPage, limit: 10 }),
queryFn: () => fetchPosts({ page: currentPage, limit: 10 }),
// 페이지 전환 시 이전 페이지 데이터를 임시로 보여줌 (깜빡임 방지)
placeholderData: keepPreviousData,
});
return (
<div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
{data?.posts.map((post) => <PostItem key={post.id} post={post} />)}
</div>
);
keepPreviousData는 React Query v5에서 제공하는 유틸로, 새 데이터를 가져오는 동안 이전 데이터를 그대로 보여줍니다.
페이지네이션에서 페이지를 넘길 때 화면이 깜빡이는 문제를 해결할 때 자주 사용합니다.
세 가지 비교 정리
| 구분 | prefetchQuery | initialData | placeholderData |
|---|---|---|---|
| 데이터 출처 | 서버 (미리 fetch) | 기존 캐시 또는 직접 지정 | 직접 지정 |
| 캐시 저장 여부 | O 저장됨 | O 저장됨 | X 저장 안 됨 |
| 로딩 상태 | 없음 | 없음 | isPlaceholderData: true |
| 주요 사용 사례 | 페이지 이동 전 미리 로드 | 목록 → 상세 데이터 재활용 | 페이지네이션 깜빡임 방지 |
5. 공부하면서 생겼던 의문점
Q. staleTime을 전역으로 설정할 수 있나요?
네, QueryClient 생성 시 defaultOptions로 전역 설정이 가능합니다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 전역 기본값 1분
gcTime: 1000 * 60 * 5, // 전역 기본값 5분
},
},
});
개별 useQuery에서 설정하면 전역 설정을 덮어씁니다.
Q. invalidateQueries와 refetchQueries의 차이가 뭔가요?
| 구분 | invalidateQueries | refetchQueries |
|---|---|---|
| 동작 | 캐시를 stale로 표시 | 즉시 서버에 재요청 |
| 재요청 시점 | 해당 쿼리가 다음에 사용될 때 | 호출 즉시 |
| 사용 시점 | mutation 후 관련 데이터 갱신 | 즉시 최신 데이터가 필요할 때 |
일반적으로 mutation(생성/수정/삭제) 후에는 invalidateQueries를 사용하는 것이 권장됩니다.
현재 화면에서 사용 중인 쿼리만 재요청하고, 사용하지 않는 쿼리는 다음에 필요할 때 재요청하기 때문에 효율적입니다.
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// 게시글 생성 후 목록 캐시 무효화
queryClient.invalidateQueries({ queryKey: postQueryKeys.lists() });
},
});
Q. React Query v4에서 v5로 올라오면서 뭐가 바뀌었나요?
캐싱 전략과 관련된 주요 변경 사항만 정리하면 아래와 같습니다.
| 구분 | v4 | v5 |
|---|---|---|
| cacheTime | cacheTime |
gcTime으로 이름 변경 |
| keepPreviousData | 옵션으로 전달 | placeholderData: keepPreviousData |
| onSuccess/onError | useQuery 옵션에서 지원 | useQuery에서 제거, useMutation에서만 지원 |
| 쿼리 함수 인자 | queryKey 포함 객체 |
signal만 포함 (queryKey 제거) |
6. 참고 자료
- TanStack Query 공식 문서: https://tanstack.com/query/latest/docs/framework/react/overview
- Caching 공식 문서: https://tanstack.com/query/latest/docs/framework/react/guides/caching
- Query Keys 공식 문서: https://tanstack.com/query/latest/docs/framework/react/guides/query-keys
- Prefetching 공식 문서: https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
- v4 → v5 마이그레이션 가이드: https://tanstack.com/query/latest/docs/framework/react/guides/migrating-to-v5
댓글