
📅 2025-11-19
➡️ TanStack Query 생명 주기 및 주요 옵션에 대해 새롭게 알게 된 것 또는 헷갈리는 부분 정리
클라이언트 내부에서만 존재하는 상태와 달리 아래 요소를 계속 고려해야 함
TanStack Query는 이런 복잡한 과정을 아래처럼 자동화함
▶️ 개발자는 “언제, 무엇을 요청할지”에만 집중하면 되고 요청 타이밍, 캐시 갱신은 TanStack Query가 대신 해줌
🔗 https://web.dev/articles/stale-while-revalidate?hl=ko
Cache-Control: max-age=1, stale-while-revalidate=59
▶️ TanStack Query는 이 SWR 전략을 내부적으로 활용하여 데이터를 효율적으로 관리함
QueryClientProvider로 전체 앱을 감싸 캐시 공유
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
Provider 하위 모든 컴포넌트가 같은 캐시를 공유
Redux처럼 전역 상태를 제공하는 것과 비슷하지만 서버 상태에 특화된 형태로 자동화되어 있음

A 컴포넌트가 처음 데이터를 요청할 때
useQuery({ queryKey: ["todos"], queryFn: fetchTodos });useQuery 실행"todos" 키로 데이터가 있는지 먼저 확인 → 아직 없음 → 초기 상태{ data: undefined, isLoading: true, isFetching: true }queryFn)fetchTodos(); // GET /todos["todos"] 키로 저장{
todos: [ /* 서버 데이터 */ ] // fetchTodos의 반환값
}{ data: todos, isLoading: false, isFetching: false }useQuery({ queryKey: ["todos"], queryFn: fetchTodos });"todos"를 캐시에 조회{ data: todos, isLoading: false, isFetching: false }isFetching이 true 일 수도 있음)const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries(["todos"]);
},
});mutation.mutate() 실행createTodo 함수가 실행되어 새 todo 생성 요청queryClient.invalidateQueries(["todos"]);"todos" 캐시를 stale(오래됨)로 표시fetchTodos 재실행"todos"를 구독 중인 컴포넌트 A·B·C 모두 자동으로 최신 UI로 리렌더링🔗 https://tanstack.com/query/v5/docs/framework/react/devtools
npm i @tanstack/react-query-devtools
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
const queryClient = new QueryClient();
<QueryClientProvider client={queryClient}>
{/* Devtools UI */}
<ReactQueryDevtools initialIsOpen={false} />
<App />
</QueryClientProvider>;

| 상태 | 언제? | 특징 |
|---|---|---|
| fresh | 방금 패칭했거나 staleTime 안쪽 | “지금은 다시 요청할 필요 없음” |
| stale | staleTime이 지남 | 필요 시 자동/수동으로 refetch 가능 |
| active | 1개 이상의 컴포넌트가 해당 쿼리를 사용 중 | gcTime이 카운트되지 않음 (유지됨) |
| inactive | 어떤 컴포넌트도 사용하지 않을 때 | Devtools에서 회색 표시, gcTime 카운트 시작 |
| deleted | inactive 상태로 gcTime이 지나면 | 캐시에서 완전히 제거됨 → 다음 요청은 “처음 요청”처럼 동작 |
| fetching | queryFn이 실제 서버 요청 중 | isFetching: true이며, Devtools에서 해당 쿼리에 작은 회전 아이콘 표시 |
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 5000 } }
});
const { data } = useQuery({
queryKey: ["todos"],
queryFn: getTodos,
staleTime: 5000,
});
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 2000,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
}
}
});
refetchOnMount
refetchOnWindowFocus
refetchOnReconnect
const queryClient = new QueryClient({
defaultOptions: { queries: { gcTime: 2000 }
});
const { data } = useQuery({
queryKey: ["todos"],
queryFn: getTodos,
gcTime: 2000, // 2초 후 삭제
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: 5 } }
});
쿼리 개별 설정
const { data } = useQuery({
queryKey: ["todos"],
queryFn: getTodos,
retry: 10,
});
| staleTime | 동작 |
|---|---|
> 0 | 패칭 직후 일정 시간 fresh → 자동 재요청 X |
0 | 패칭 직후 바로 stale → 필요 시(refetch 이벤트에 의해) 재요청 가능 |
| 케이스 | 캐시 존재 | 서버 요청 중? | isPending | isFetching | 설명 |
|---|---|---|---|---|---|
| 첫 진입, 캐시 없음 | ❌ | ✅ | true | true | 초기 로딩 상태 |
| 캐시 있고, refetch 없음 | ✅ | ❌ | false | false | 화면은 캐시 기반, 백그라운드 패칭 없음 |
| 캐시 있고, 백그라운드 refetch | ✅ | ✅ | false | true | SWR 패턴, UI는 캐시 → 뒤에서 새 데이터 패칭 |
| 캐시 없음인데 retry 중 | ❌ | ✅ | true | true | 네트워크 에러 등으로 재시도 중 |
true → 컴포넌트 마운트 시 자동 실행false → 자동 실행하지 않고, 수동 refetch할 때만 실행 (검색, 버튼 액션에 유용)const [keyword, setKeyword] = useState("");
const { data, refetch, isFetching } = useQuery({
queryKey: ["search", keyword],
queryFn: () => searchApi(keyword),
enabled: false, // 자동 실행 X
});
return (
<div>
<input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
<button onClick={() => refetch()}>검색</button>
{isFetching && <p>검색 중...</p>}
{data && <SearchResult data={data} />}
</div>
);const { data: user } = useQuery({ queryKey: ["user"], queryFn: fetchUser });
const { data: todos } = useQuery({
queryKey: ["todos", user?.id],
queryFn: () => fetchTodos(user.id),
enabled: !!user, // user가 있을 때만 실행
});특정 필드만 꺼내 쓰기 (username만 선택)
const { data: username } = useQuery({
queryKey: ["user"],
queryFn: fetchUser,
select: (user) => user.username,
});
// JSX
<div>이름: {username}</div>
목록에서 필요한 값만 매핑 (todos에서 content만 추출)
const { data: todoTexts } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
select: (todos) => todos.map((t) => t.content),
});
서버 데이터 → 화면에서 필요한 형태로 변환 (정렬/필터링 등 데이터 가공)
const { data: todosData } = useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
select: (todos) => {
const sorted = [...todos].sort(
(a, b) => new Date(b.createdAt) - new Date(a.createdAt)
);
const completed = sorted.filter((t) => t.completed);
const pending = sorted.filter((t) => !t.completed);
return { completed, pending };
},
});
// JSX
<TodoSection title="할 일" list={todosData.pending} />
<TodoSection title="완료" list={todosData.completed} />