useQuery 기본 사용법
useQuery는 TanStack Query의 핵심 훅 중 하나로 서버 데이터를 가져오고, 캐싱하고, 동기화 및 업데이트하는 전체 라이프 사이클을 캡슐화해준다.
useQuery의 파라미터
const result = useQuery({
queryKey, // required
queryFn, // required
// ...options ex) staleTime, gcTime, enabled, select, ...
});
인자로는 하나의 객체를 받으며, queryKey와 queryFn 두 필드는 필수이다.
queryKey: 데이터 고유 식별자
queryKey는 서버 데이터의 고유 주소 라벨이라고 생각하면 된다. 배열 형태로 작성하며, TanStack Query는 이를 다음 세 가지 용도로 활용한다.
1. 캐시 키: 특정 queryKey로 데이터를 가져오면 TanStack Query는 해당 데이터를 저장한다. 동일한 queryKey로 다시 데이터를 요청하면 캐시된 데이터를 즉시 반환한다.
2. 데이터 갱신: 데이터를 업데이트한 후 해당 queryKey를 무효화(invalidate) 하면 TanStack Query가 서버에서 새로 데이터를 가져온다.
3. 의존성 식별: 데이터가 변수(userId, 검색어 등)에 의존한다면, 그 변수를 queryKey 배열에 포함시킨다. 변수가 다르면 서로 다른 데이터이므로, 별도로 캐싱되고 관리된다.
// 모든 할 일 목록
useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
// 특정 할 일 (todoId가 바뀌면 새로운 쿼리-다른 데이터로 취급됨)
useQuery({ queryKey: ['todo', todoId], queryFn: () => fetchTodo(todoId) })
// 필터가 포함된 목록
useQuery({ queryKey: ['todos', { status: 'done' }], queryFn: fetchFilteredTodos })
queryKey는 안정적이고 데이터의 고유 식별자역할을 할 수 있어야 한다.queryKey를 어떻게 설계하느냐에 따라 캐시 관리의 복잡도가 달라지게 된다! 이 부분은 다음 편에서 자세히 다뤄보겠다.
queryFn: 데이터를 가져오는 방법
queryFn은 특정 queryKey와 연관된 데이터를 어떻게 가져올지 TanStack Query에게 알려주는 함수이다. 반드시 Promise를 반환해야 하며, 성공 시 데이터를 resolve, 실패 시 에러를 throw해야 한다. 여기서 보통 API를 호출한다(fetch 또는 axios)
// fetch 사용 시 — HTTP 에러를 직접 처리해야 함
const fetchTodos = async () => {
const response = await fetch('https://api/todos');
if (!response.ok) {
// fetch는 404, 500 같은 HTTP 에러에서 자동으로 throw하지 않음
throw new Error('Failed to fetch todos');
}
return response.json();
};
// axios 사용 시 — HTTP 에러를 자동으로 throw
const fetchTodos = async () => {
const { data } = await axios.get('/todos');
return data;
};
fetch는 네트워크 오류가 아닌 HTTP 에러(404, 500 등)에서 자동으로 throw하지 않는다. 때문에 if (!response.ok) 체크가 반드시 필요하다. axios는 이를 자동으로 처리해주기 때문에 queryFn이 더 간결해진다.
useQuery의 반환값
useQuery는 단순히 패치해온 데이터만 반환하는게 아니라, 쿼리의 현재 상태를 나타내는 다양한 값들을 반환한다. 이 값들을 제대로 이해하고 사용해야 올바를 UI를 구성할 수 있다.
const {
data, // 패칭된 데이터 (성공 시에만 존재)
error, // 에러 객체 (에러 발생 시에만 존재)
status, // 'pending' | 'error' | 'success'
isPending, // 아직 캐시된 데이터가 없는 상태
isError, // 에러 발생 상태
isSuccess, // 데이터 정상 수신 상태
isFetching, // 백그라운드 포함, 현재 fetch가 진행 중이면 true
isLoading, // isPending && isFetching — 최초 로딩 중
} = useQuery({ queryKey, queryFn });
status: 데이터에 대한 상태값
useQuery가 반환하는 핵심적인 값 중에 status라는 값이 있다.status는 "현재 이 쿼리에 데이터가 있냐 없냐" 를 기준으로 쿼리의 상태를 나타내는 값이다. pending, error, success 세 가지 값만 가진다.
const {
status, // 'pending' | 'error' | 'success'
} = useQuery({ queryKey, queryFn });
pending: 아직 데이터가 없는 상태를 나타내며, 두 가지 경우에 해당된다.- 캐시된 데이터가 없고, 아직 fetch 시도도 안한 경우
- 캐시된 데이터가 없고, fetch를 시도했지만 아직 완료되지 않은 경우
"데이터가 없다" 는게 핵심이며, fetch 중인지 여부는 status로 판단할 수 없다!
error: fetch 시도가 에러로 끝난 상태를 나타낸다.- 이 때
error프로퍼티에는 실제 에러 객체가 담겨있게 된다.
- 이 때
success: 데이터를 정상적으로 받은 상태를 나타낸다.- 이 때
data프로퍼티에 실제 데이터가 담겨 있게 된다.enabled: false로 설정되어 fetch를 한 번도 안 했더라도initialData를 설정해뒀다면 처음부터success상태가 된다!
- 이 때
status를 통해서는 "데이터(캐시)가 존재하는가?" 를 알 수 있다.
fetchStatus: 네트워크 요청에 대한 상태값
fetch 진행 여부는 status가 아닌 fetchStatus를 통해 확인할 수 있다. fetchStatus는 지금 queryFn이 실제로 실행 중인지를 나타낸다. status와 마찬가지로 세 가지 값만 가지며 fetching, paused, idle을 갖는다.
const {
fetchStatus, // 'fetching' | 'paused' | 'idle'
} = useQuery({ queryKey, queryFn });
fetching:queryFn이 실행중인 상태를 나타낸다.- 최초 로딩이든 백그라운드 refetch든
queryFn이 실행되고 있으면fetching상태가 된다!
- 최초 로딩이든 백그라운드 refetch든
paused: fetch 하려 했는데 멈춘 상태를 말한다.- 가장 흔한 케이스는 오프라인 상태이다.TanStack Query의
networkMode설정과 연관이 있다. (기본값online— 오프라인이면 fetch를 paused 상태로 대기)
- 가장 흔한 케이스는 오프라인 상태이다.TanStack Query의
idle: fetch하지 않는 상태를 말한다.queryFn이 실행되지 않고 있는 평상시 상태를 나타낸다.
파생 상태값
status 파생값
- 편의를 위한 파생값이다.
isPending === (status === 'pending') // 데이터 없음
isSuccess === (status === 'success') // 데이터 있음
isError === (status === 'error') // 에러 상태
fetchStatus 파생값
- 편의를 위한 파생값이다.
isFetching === (fetchStatus === 'fetching') // queryFn 실행 중
isPaused === (fetchStatus === 'paused') // fetch 중단됨
status + fetchStatus 조합으로 파생되는 값들
isLoading = isFetching && isPending // 최초 fetch가 진행 중인 상태 (데이터없음 + fetching)
// = "캐시도 없고(isPending) + 지금 fetch 중(isFetching)"
isRefetching = isFetching && !isPending // 백그라운드 refetch가 진행 중인 상태 (데이터있음 + fetching)
// = "fetch 중(isFetching) + 캐시는 있음(!isPending)"
에러 관련 세분화 값들
에러 상태를 더 세밀하게 구분하는 값들도 있다.
jsisLoadingError // 최초 fetch 도중 에러가 발생한 경우 true
isRefetchError // refetch 도중 에러가 발생한 경우 true
에러 원인을 추적할 때 유용한 값들은 다음과 같다.
jsfailureCount // fetch 실패 횟수. 성공 시 0으로 리셋
failureReason // 재시도 실패 이유. 성공 시 null로 리셋
errorUpdateCount // 누적 에러 발생 횟수 (리셋 없음)
데이터와 관련된 값들
data // 가장 최근에 성공적으로 받은 데이터. 기본값 undefined
dataUpdatedAt // 마지막으로 success 상태가 된 시각 (timestamp)
error // 에러 객체. 기본값 null
errorUpdatedAt // 마지막으로 error 상태가 된 시각 (timestamp)
함수
refetch(options?)
// 쿼리를 수동으로 다시 패칭하는 함수
// 에러 발생 시 기본적으로 로그만 남김
// 에러를 throw하려면 throwOnError: true 옵션 전달
isPending vs isLoading
공식문서에서는 대부분 쿼리에서 isPending(데이터 없음) → isError(에러) → data(성공 상태로 간주) 순으로 체크하면 모든 케이스를 깔끔하게 커버할 수 있다고 언급하고 있다.
그렇다면 isLoading말고 왜 isPending을 사용하도록 권장하는 걸까?
TypeScript의 타입 narrowing
isPending은 status 필드에서 직접 파생된 값인 반면에, isLoading은 status = 'pending'과 fetchStatus = 'fetching의 조합이다. 따라서 isLoading과 isError가 둘 다 false여도 data가 있다고 보장할 수 없다.
반면, isPending과 isError가 둘 다 false면 TypeScript가 data에서 undefined를 자동으로 제거해주게 된다.
// isLoading 사용 시
const { isLoading, isError, data } = useQuery(...)
if (isLoading) return <Loading />
if (isError) return <Error />
return <div>{data?.title}</div> // data가 undefined일 수 있어서 ?. 필요
// isPending 사용 시
const { isPending, isError, data } = useQuery(...)
if (isPending) return <Loading />
if (isError) return <Error />
return <div>{data.title}</div> // data가 반드시 존재함이 보장됨, ?. 불필요
'libraries > TanStack Query' 카테고리의 다른 글
| [TanStack Query] 서버 상태 이해하기 + TanStack Query 시작하기 (0) | 2026.04.13 |
|---|