TanStack Query에서 제공하는 useQuery 훅을 사용해 비동기 데이터를 요청하면,
데이터와 함께 캐싱에 관련된 여러 정보를 응답으로 제공하는 것을 확인할 수 있습니다.
이들 정보 중 다수가 is~와 같은 Boolean 형태를 가지며, 요청의 특정 상태를 나타내는 플래그처럼 보입니다.
다만, 저의 경우 처음 봤을 때 이름만으로 필요한 플래그를 골라내기 어려웠습니다.
예시로 비동기 데이터를 가져오는 동안 스켈레톤 UI를 보여주려고 했을 때,
isPending · isPaused · isLoading 중 어떤 필드를 사용해야 할 지 확실치 않다고 생각했습니다.
useQuery 상태 필드를 헷갈리기 쉬운 이유

저만 헷갈리는 부분은 아닌 듯 합니다..! 특히 그러한 이유는…
각 버전을 거치며 상태 정의가 꾸준히 변경되어 왔습니다.
TanStack Query는 2025년 12월 기준 Latest인 v5.90.16까지 발전하며 ‘요청 상태’에 대한 메이저한 개선 사항을 지속적으로 포함해 왔습니다. 예를 들어 v5에서는 다음 사항들이 변경되었습니다:
- v4의
isInitialLoading이 v5에서는isLoading으로 이름이 변경되었습니다. - v4의
loading이 v5에서는pending으로 이름이 변경되었습니다. - v5의
loading은 v4의initialLoading과 동일하며, 논리적으로isLoading === isPending && isFetching입니다. - ...else
응답 내에 여러 파생 플래그가 존재합니다.
status.pending과isPending중 어떤 것을 사용해야 하나요?
얼추 예상한 바와 같이, (status === pending) <-> isPending이며 (fetchStatus === fetching) <-> isFetching입니다.
status나 fetchStatus Union 타입의 각 값에 isPending, isFetching와 같은 파생 플래그가 대응되는 구조이기 때문입니다.
이러한 방식은 코드를 작성할 때에는 편리하나, 응답 객체를 바라볼 때에 한해서 어떤 값을 사용해야 할지 판단하기 어렵게 만드는 요인입니다.
독립적인 두 가지 상태 체계가 존재합니다.
TanStack Query는 v4 이후부터 Query Status(status)에서 Fetch Status(fetchStatus)를 분리하며,
두 가지의 독립적인 상태를 제공하기 시작했습니다.
이를 통해 하나의 상태 플래그만으로는 모호했던 상황을 자연스럽게 표현 가능해졌지만,
앞서 각 상태 플래그가 어떤 의도로 구분되었는지에 대해 이해가 필요합니다.
상태 필드
Query Status
Query Status는 데이터의 존재 여부에 대한 상태입니다. “화면에 보여줄 데이터가 현재 캐시에 존재하는지 (Do we have any or not?) ”에 대한 여부를 표현합니다.
status === 'pending': 아직 데이터를 받아오지 못한 상태status === 'success': 데이터를 성공적으로 받아온 상태status === 'error': 데이터를 받아오다 에러가 발생한 상태
각 파생 플래그는 status Union 타입의 하나의 값에 대응하므로 상호 배타성을 가집니다.
isPending, isSuccess, isError 중 반드시 하나만 true이며, 모두가 false인 경우는 존재하지 않습니다.
isPending은status === 'pending'과 동일isSuccess는status === 'success'와 동일isError는status === 'error'와 동일
Fetch Status
Fetch Status는 쿼리 함수(queryFn)의 현재 실행 중 여부에 대한 상태입니다. “지금 데이터를 가져오기 위해 요청 중인지 (Is it running or not?) ”에 초점을 맞추고 있습니다.
fetchStatus === 'fetching': 쿼리 함수가 실행 중인 상태fetchStatus === 'paused': 쿼리 함수가 네트워크 오프라인 등의 이유로 실행 중단된 상태fetchStatus === 'idle': 쿼리 함수가 실행 중이지 않은 상태
Query Status와 마찬가지로 fetchStatus의 파생 플래그 또한 상호 배타성을 가집니다.
isFetching, isPaused 중 반드시 하나만 true이며,
모두가 false인 경우는 반드시 fetchStatus === 'idle'를 가리킵니다.
isFetching:fetchStatus === 'fetching'과 동일isPaused:fetchStatus === 'paused'와 동일
isIdle 플래그는 존재하지 않지만, isFetching === false && isPaused === false인 경우로 유추하거나
fetchStatus === 'idle'로 직접 확인합니다.
Fetch Status는 v4에서부터 처음 도입되었습니다. 이전에는 status 하나로 모든 상태를 표현했는데, 아래와 같은 상황에서 불편함이 존재했습니다.
- 단일
loading상태가 네트워크가 오프라인인 경우와 무한 로딩 상태를 구분할 수 없었습니다. - 이미 데이터가 존재하며(
success) 백그라운드에서 데이터를 재요청하는 경우를 판별하기 어려웠습니다. - 비활성화된 쿼리(
enabled: false)와 쿼리가 막 시작된 경우(idle → loading)를 구분하기 모호한 구간이 존재했습니다.
기존의 Query Status에서 Fetch Status를 별도로 분리함으로써, 데이터의 상태와 요청의 진행 상태를 독립적으로 추적할 수 있게 되었습니다.
- 단일
loading상태가 네트워크가 오프라인인 경우와 무한 로딩 상태를 구분할 수 없었습니다.
→ 네트워크가 오프라인 상태인 경우를fetchStatus: 'paused'로 명확히 구분할 수 있습니다. - 이미 데이터가 존재하며(
success) 백그라운드에서 데이터를 재요청하는 경우를 판별하기 어려웠습니다.
→status: 'success'이면서fetchStatus: 'fetching'인 경우로 표현 가능합니다. - 비활성화된 쿼리(
enabled: false)와 쿼리가 막 시작된 경우(idle → loading)를 구분하기 모호한 구간이 존재했습니다.
→idle한 상태를fetchStatus로 명확히 분리했습니다.
Complex Status
isFetched:status === 'success' || status === 'error'와 동일isFetchedAfterMount: 현재 페이지나 컴포넌트가 마운트된 이후에 데이터 요청이 한 번이라도 수행 완료되었는지의 여부
결과가 성공이든 에러든 요청 시도가 한 번이라도 수행 완료되었음을 의미합니다.
화면 진입 시 캐시에 이미 데이터가 있는 경우 바로 isFetched는 true가 되지만,
요청이 발생하지 않았으므로 isFetchedAfterMount: false로 남아있습니다.
이후 재요청이 발생되는 시점에야 isFetchedAfterMount: true가 됩니다.
isLoading:isPending && isFetching과 동일isLoadingError:isError && !isFetched와 동일isInitialLoading:isLoading과 동일 (deprecated)
isLoading은 쿼리 함수의 첫 번째 요청이 진행 중인 상태입니다.
isLoadingError는 첫 번째 요청 시도 자체가 실패해서 화면에 보여줄 데이터가 아예 없는 에러 상태를 가리킵니다.
isRefetching:!isPending && isFetching과 동일isRefetchingError:isError && isFetched와 동일
백그라운드에서 재요청이 진행 중인 경우 isRefetching: true가 됩니다.
isRefetchingError: true은 상태는 이미 이전에 성공한 데이터가 있어 화면에는 데이터가 있지만,
백그라운드 재요청 시도가 실패한 상태입니다.
상태 전이 시나리오
데이터를 요청하는 과정에서 발생할 수 있는 다양한 시나리오들을 살펴보면, Query Status와 Fetch Status가 서로 독립적이며 다양한 상황에서 조합되어 나타날 수 있다는 사실을 더 쉽게 이해할 수 있습니다.
1-a. 초기화(마운트) 및 첫 데이터를 요청하는 경우

useQuery가 마운트되면 Fetch Status는 idle로 초기화되며, 요청을 시작함에 따라 fetching으로 전이됩니다.
데이터를 가져오기 위한 요청이 실제로 수행되고 있다는 사실을 사용자에게 전달하기 위해 스켈레톤 UI를 사용하기 적절한 상태이며,
isLoading이나 isInitialLoading을 활용해 더 간결하게 나타낼 수 있습니다.
Query Status는 기본적으로 pending이지만, initialData가 존재하는 경우 초기부터 success 상태입니다.
단, placeholderData의 경우는 데이터가 없는 경우와 마찬가지로 pending 상태를 가집니다.
이는 TanStack Query가 Stale-While-Revalidate 전략을 핵심으로 따르고 있기 때문입니다.
initialData가 존재하면 요청 자체가 실패한다고 해서status: 'error'상태로 전이되기는 매우 어렵습니다.
1-b. 데이터를 요청하지 않고 대기하는 경우

만약 useQuery가 마운트되는 시점에 enabled: false 옵션이 설정된 경우,
Fetch Status는 idle 상태를 유지하며 요청을 수행하지 않습니다.
Query클래스의 생성자 함수에서 호출되는getDefaultState()메서드는 내부에서idle을 초기값으로 가지므로,fetchFn()호출이 발생하지 않는다면 Fetch Status는fetching으로 전이하지 않고idle로 남아있습니다.
이 때는 스켈레톤 UI나 로딩 인디케이터보다도, 필요한 필터 옵션을 입력하거나 사용자에게 권한을 요청하는 등 사용자 액션을 유도하는 것이 적절합니다.
2-a. 데이터를 성공적으로 받아온 경우

요청에 성공한 경우, 데이터를 화면에 표시하거나 컴포넌트에 전달합니다. useQuery 실행에서 가장 이상적인 시나리오입니다.
2-b. 네트워크 연결이 오프라인인 경우

Fetch Status가 paused일 때는 네트워크 상태가 비활성화되어 요청이 중단된 경우입니다.
TanStack Query는 네트워크가 다시 연결되기를 감지하거나 지수 백오프 재시도 요청을 자체적으로 시도하지만,
그 동안 사용자에게 네트워크 연결을 확인해 달라는 메시지를 표시하도록 할 수 있습니다.
useQuery로 네트워크를 필요로 하지 않는 비동기 요청(ex: 무거운 Web Worker 요청 등)을 수행할 때는 네트워크 상태에 의해 해당 요청이 중단되지 않도록 v4부터 도입된
networkMode옵션을 설정할 수 있습니다.
2-c. 에러 발생 시

useQuery는 요청이 실패한 경우 기본적으로 3번의 재시도를 수행하며, 모든 재시도가 실패한 경우에 Query Status를 error로 전이합니다.
3. 백그라운드 재요청 시

이미 화면에 데이터가 표시되고 있는데, 브라우저 포커싱 혹은 캐시를 사용하는 다른 컴포넌트의 마운트 등
TanStack Query 자체 메커니즘에 의해 백그라운드에서 새로운 데이터 요청을 시도할 수 있습니다.
이때 데이터가 존재하므로 Query Status는 pending이지만, Fetch Status는 fetching이 됩니다.
이 상태에서는 기존에 요청에 성공한 데이터가 존재하므로, 데이터를 가리지 않는 별도의 영역에서 로딩 인디케이터를 표시하는 것이 적절합니다. 재요청에 실패하더라도, 기존의 'Stale'한 데이터를 지속적으로 표시해 사용자 경험에 도움을 줄 수 있습니다.
추가: useQuery 내부 상태 머신과 파생 플래그
"(번역) React Query의 장점 중 하나는 쿼리의 상태 필드에 쉽게 접근할 수 있다는 것입니다. 이를 통해 쿼리가 로딩 중인지, 아니면 에러가 발생했는지를 즉각적으로 알 수 있습니다. 라이브러리는 이를 위해 내부 상태 머신에서 파생된 여러 Boolean 플래그들을 노출합니다.
…
여기서 주의할 점은isFetching플래그는 내부 상태 머신의 일부가 아니라는 것입니다. 이 플래그는 요청이 진행 중(’in-flight’)일 때마다true가 되는 추가적인 상태입니다. 따라서 'fetching이면서 success'일 수 있고, 'fetching이면서 error'일 수도 있습니다. 하지만 'loading(pending)이면서 동시에 success'일 수는 없습니다. 상태 머신이 이를 확실히 보장하기 때문입니다."
― TkDodo, 「Status Checks in React Query」
공식 문서는 status와 같은 값은 내부 상태 머신에 의해 제어되고 있다고 언급하며,
isSuccess와 같은 플래그는 상태 머신의 일부가 아니라고 설명하고 있습니다.
이는 @tanstack/react-query의 핵심 로직이 정의된 @tanstack/query-core 패키지를 살펴보면 자세히 확인할 수 있습니다.
useQuery 내부의 QueryState를 살펴보면, status와 fetchStatus는 타입스크립트의 Discriminated Union 타입으로 정의되어 있습니다.
따라서 아래와 같은 상태 머신의 특성을 가지게 됩니다.
- 유한한(finite): 가능한 모든 유한 상태가 명시적으로 정의되어 있어, 예상치 못한 상태로의 전이가 발생하지 않습니다.
- 결정론적(deterministic):
status는'pending','error','success'중 정확히 하나의 값만 가질 수 있습니다. 동시에 두 가지 상태가 될 수 없으므로,isPending && isSuccess와 같은 논리적으로 불가능한 조합이 실제로 발생하지 않음을 보장합니다.
반면, useQuery의 반환값이 계산되는 QueryObserver.createResult() 메서드를 살펴보면
isPending, isSuccess 등의 Boolean 플래그들은 상태 머신 자체를 구성하는 요소라기보다,
status와 fetchStatus의 값을 기반으로 매번 새롭게 계산되어 제공되는 편의성 플래그임을 확인할 수 있습니다.
추가로, 타입스크립트 4.6버전 이후로 타입 추론 지원이 강화되어 status와 같은 파생 플래그를 수행한 경우도
타입 좁히기를 수행할 뿐만 아니라, isLoading && isError와 같은 논리적으로 가능하지 않은 구문 또한
never 타입으로 분류해 “Unreachable code detected” 경고를 표시합니다.
버전 별 주요 변경사항

v3 → v4
v3에서는 status 하나로 모든 쿼리 상태를 관리했으며, idle · loading · error · success의 네 가지 상태가 존재했습니다.
Fetch Status는 명시적으로 구분되지 않았으며, 대신에 isFetching 플래그를 통해 실제 요청 진행 여부를 확인할 수 있었습니다.
v4로 넘어오며 status에서 idle이 제거되고 loading · error · success 세 가지만 남았으며,
fetchStatus라는 새로운 상태 필드가 도입되어 기존의 status와 함께 이중 상태 체계가 확립되었습니다.
새로 추가된 fetchStatus는 fetching · paused · idle로 구성되어 실제 요청 진행 상태를 별도로 추적하게 되었습니다.
또한 isInitialLoading 플래그가 추가되어 status === 'loading' && fetchStatus === 'fetching'인
'진짜 첫 로딩' 상태를 쉽게 판별할 수 있게 되었습니다.
v4 → v5
v5로 넘어오며 TanStack Query 개발팀은 ‘loading’ 상태가 가지는 모호함을 해소하고자,
status의 loading을 pending으로 변경해 "데이터가 아직 준비되지 않은 대기 상태"라는 의미를 더 명확히 전달했습니다.
이에 맞춰 파생 플래그들도 재정의되었는데, isLoading이 isPending으로 바뀌었고
기존 v4의 isInitialLoading이 새롭게 isLoading로 불리도록 변경되었습니다.
결론
- useQuery의 상태 필드가 헷갈리는 이유는, 버전업 과정에서 실제로 많은 부분이 변경되었기 때문입니다.
- 여러 모호한 시나리오(네트워크 중단, 백그라운드 요청 등)를 명확히 분리해 표현하기 위해 Query Status는 ‘데이터의 존재 유무’에, Fetch Status는 ‘요청 수행 상태’에 초점을 맞추고 있으며 두 상태 필드는 독립적인 관계를 가집니다.
- 과거에는 타입스크립트 사용 시, 파생 플래그(ex:
isFetching) 대신 상태를 나타내는 Union 값(ex:fetchStatus)을 활용하는 편이 권유되었으나, 최신 버전의 타입스크립트에서는 파생 플래그에 대한 타입 지원이 매우 잘 이루어지고 있습니다. - TanStack Query가 v5에 이르기까지 상태 필드에 대해 어떤 메이저한 변경 사항들이 있었는지를 인지한다면, 다양한 버전을 배경으로 작성된 문서들을 참고하며 혼동이 오는 것을 방지할 수 있을 것이라고 생각합니다.

