TanStack Query
하니2025.12.04
loading ...
TanStack Query란?
- 웹 서비스를 위한 Data Fetching 라이브러리
등장 배경
- 대부분 웹 프레임워크는 데이터를 전체적으로 조회하거나 관리하는 것에 대한 명확한 방법을 제공하지 않음
- 그렇기에 상태 관리 라이브러리를 사용하기도 함(Zustand)
- 대부부분의 상태 관리 라이브러리는 클라이언트에서의 상태를 관리하는 데에 뛰어남!🫅
- 하지만 서버 상태를 관리하는 데에는 효과적이지 않음😭
- 서버 상태의 특성
- 캐싱
- 중복 요청 제거
- 오래된 데이터 업데이트
- 데이터가 가져온지 오래된 시점을 파악
- 성능 최적화
- 메모리 관리 및 GC
- TanStack Query는 서버 상태를 관리하는 최고의 라이브러리
- 사용하면 좋은점
- 캐싱 및 성능 최적화를 코드 몇 줄로 대체 가능
- 유지보수 용이
- 통신 속도 증가로 인한 사용자 경험 상승
- 대역폭 절약 및 메모리 성능 향상
주요 개념
Query란?
- 데이터를 가져오는 행위를 ‘쿼리’라고 부름
- 이 행위를 위해 useQuery 훅을 사용
- Query Key: 캐시를 관리하기 위한 이름표
- Query Function: 실제로 fetch나 axios 등 데이터를 가져오는 비동기 함수
Mutation란?
- 데이터를 변경하는 행위를 ‘뮤테이션’이라고 부름(생성, 수정, 삭제)
- 이 행위를 위해 useMutation 훅을 사용
- Mutation Key(선택): 뮤테이션 식별 및 로딩 상태 추적을 위한 key
- Mutation Function: 실제로 fetch나 axios 등 데이터를 변경하는 비동기 함수
Query Invalidation이란?
- 캐시된 데이터를 '무효화'하여 다시 가져오도록 하는 행위를 '쿼리 무효화'라고 부름
- 주로 뮤테이션 성공 후 관련된 쿼리를 최신 상태로 업데이트하기 위해 사용
- queryClient.invalidateQueries()를 통해 특정 Query Key의 캐시를 stale 상태로 만듦
- 무효화된 쿼리는 자동으로 refetch되어 최신 데이터를 가져옴
동작 원리
useQuery
const { data } = useQuery({
queryKey: ['user']
queryFn: async () => (await fetch('url').json()
}
- ‘user’라는 key로 api 호출
- key가 캐시된 데이터를 사용할지 다시 호출을 할지 결정하는 기준
‘user’라는 쿼리 키로 캐시된 데이터가 없을 때
- 캐시된 데이터가 없다면 호출 함수를 실행하여 서버로부터 데이터를 받아옴
- 그 데이터가 캐시되고 이후 요청에서는 캐시된 데이터 사용 가능
- Miss 상황이라고 부름
‘user’라는 쿼리 키로 캐시된 데이터가 있을 때
- 서버에 요청을 전송하지 않고, 캐시된 데이터 사용
- 같은 데이터를 가져오는 요청이 여러 번 발생해도, 캐시된 데이터를 사용하게 되어 중복 요청량 감소
- Hit 상황이라고 부름
❓❓ 캐시된 데이터가 있으면 서버로 요청을 아예 못보내는건가? 캐시된 데이터가 너무 예전 데이터라 다시 요청을 보내고 싶다면?
데이터의 신선도
- Tanstack Query는 캐시한 데이터를 Fresh한 데이터와 Stale 데이터로 구분하여 관리
- 캐시된 데이터가 Fresh 상태라면 캐시데이터 사용, Stale한 상태라면 서버로 요청
캐싱된 데이터도 사용 가능한 기간이라는게 있다.
캐싱된 데이터의 사용 가능 기간을 지정하는 방법
- staleTime 옵션으로 지정이 가능
- isStale로 상태 확인 가능
const { data, isStale } = useQuery({
queryKey: ['user']
queryFn: async () => (await fetch('url').json()
- staleTime 100초
- 100초 뒤에 호출하면 isStale이 true로 변경
설치 및 구성
npm i @tanstack/react-query
# or
pnpm add @tanstack/react-query
# or
yarn add @tanstack/react-query
npm i -D @tanstack/eslint-plugin-query
# or
pnpm add -D @tanstack/eslint-plugin-query
# or
yarn add -D @tanstack/eslint-plugin-query
- Tanstack Query 코드를 작성하면서 버그 및 문법 오류를 잡아주는 eslint 플러그인도 제공
$ npm i @tanstack/react-query-devtools
# or
$ pnpm add @tanstack/react-query-devtools
# or
$ yarn add @tanstack/react-query-devtools
- Tanstack Query의 모든 내부 동작을 시각화해주는 도구
Provider 세팅
- Tanstack Query도 내부적으로 React Context로 구성
- 프로젝트에서 Tanstack Query에서 제공하는 hook을 사용하기 위한 Provider 세팅 필요
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
- new QueryClient를 생성하면 쿼리 캐시 저장소가 생성
- Tanstack Query 패키지에서 제공되는 useQuery와 같은 hook들은 Provider로 제공된 캐시 저장소에서 사용
- queryClient 저장소 생성 ⇒ queryClientProvider로 랩핑 ⇒ 프로젝트 내부에 컴포넌트에서 해당 저장소에 접근 가능 ⇒ hooks 사용 ⇒ Provider로 제공된 캐시 저장소에서 사용됨
핵심 기능
useQuery
- 가장 기본적인 쿼리 hook
- 데이터를 가져올 때 사용
import { useQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData() {
const
- time과 message가 data 객체로 오는 api
- data가 아닌 query 결과 전체를 불러오면 아래와 같은 형식으로 응답
result 속성 목록
| 반환 속성 | 설명 | 타입 |
| data | 성공적으로 가져온 데이터. | TData |
| dataUpdatedAt | 최근에 데이터를 성공적으로 가져온 시간(유닉스 타임스탬프). | number |
| error | 오류가 발생했을 때의 오류 객체. 오류가 발생하지 않았다면 null. | null \| TError |
|
- 데이터가 아직 없는 상태
- "쿼리가 아직 시작 안 했거나, 첫 데이터를 받기 전"
- 로딩 중 + 캐시 없음
- "데이터를 가져오는 중이고, 보여줄 캐시도 없는 상태"
- 성공적으로 데이터를 받음
- "쿼리가 성공하고 데이터가 있는 상태"
- 에러 발생
- "쿼리 실행 중 에러가 발생한 상태"
const { data, isLoading, isError, error } = useQuery({...});
if (isLoading) return <Spinner />;
if (isError
useQuery 옵션
useQuery 옵션 목록
| 옵션 | 설명 | 기본값 | 타입 |
| enabled | 쿼리 자동 실행 여부. false인 경우, 대기 상태(pending)로 시작. | true | boolean \| (query: Query) => boolean |
| gcTime | 비활성 캐시 데이터(Inactive)가 메모리에 남아 있는 시간(ms). | 5 * 60 * 1000 | number \| Infinity |
|
주요 옵션
- queryKey
- 고유한 쿼리 식별자
- 배열로 전송(다중 키 가능)
- 필수 옵션
- queryFn
- 데이터를 가져오는 함수, 데이터 반환 or 오류 쓰로잉 필수
- 필수 옵션
- select
- 가져온 데이터 변형 가능
- 최종 데이터 타입을 3번째 제네릭 타입으로 선언 가능
- enabled
- 쿼리 자동 실행 여부 옵션
- false인 경우, 대기 상태(pending)로 시작
- retry
- staleTime
queryKey
- 쿼리 키는 쿼리를 식별하는 고유한 값으로, 배열 형태로 지정
- 다중 쿼리 키를 사용 가능(배열 요소들 순서 중요)
useQuery({ queryKey: ["Jay"] });
useQuery({ queryKey: ["Jay", "study", 555, { a:
- queryFn에 props 값이 다르면 별개의 요청을 전송해야 함
- 그렇기 때문에 식별을 위한 key와 queryFn에 들어가는 props를 전부 key로 담아서 전송
function DelayedData({ wait = 1000 }: { wait: number }) {
const { data } = useQuery<ResponseValue>({
queryKey: ['delay',
- 만약 key 배열에 props를 안담는다면?
- 매번 다른 값을 담아서 api 호출을 해야 하는데, 식별용 key만 있으니까 이전에 호출해서 받은 캐싱된 데이터로 처리됨!
- eslint에 다음 rules를 추가하면 함수에서 사용된 변수가 key로 지정되지 않았을 때, 에러를 발생시킬 수 있음 "@tanstack/query/exhaustive-deps": "error",
queryFn
- 데이터를 가져오는 비동기 함수
- 데이터 반환 or 오류 쓰로잉 필수
- 쓰로잉한 오류는 반환되는 error로 확인 가능
- error 기본 값은 null
queryFn: async () => {
const res = await fetch('https://api.heropy.dev/v0/delay?t=1000')
const data = await res.json()
if (!data.time)
select
- 가져온 data를 변형
- 쿼리 함수가 반환하는 데이터를 인수로 받아 변형 시키면 최종 데이터가 됨
const { data } = useQuery<Users, Error, string[]>({
queryKey: ['users'],
queryFn: async () => {
const
- data에 user.name만 담긴 배열이 반환됨
- 3번째로 담긴 타입이 최종 반환 타입
"users": [
{
"id": "ywTTX",
"name": "Neo",
"age": 85,
"isValid": true,
"emails": [
"neo1@heropy.dev"
- 실제 네트워크 요청에 대한 응답은 그대로 오지만
- console.log(data);의 값은 아래와 같이 출력
['Neo', 'Trinity', 'Emily', 'John', 'Smith', 'Evan', 'Lewis']
placeholderData
- isPending 상태에서 임시로 표현할 데이터를 지정
- 쿼리 함수가 실행되기 전 상태라면 지정한 데이터가 미리 담겨져 있음
export default function DelayedData() {
const { data } = useQuery<Users, Error, string[]>({
queryKey: ["users"],
queryFn
- console에서 미리 지정된 디폴트 값이 출력되고, query 함수가 호출되어 데이터를 받아오면 반환된 데이터로 변경
- 데이터를 받아오기 전이라 화면이 깨지는 것을 방지하기 위해 사용
useMutation
- 데이터 변경 작업을 위한 useMutation hook 제공
- 데이터 변경 작업을 처리하고 useQuery와 같이 여러 상태를 확인 가능
- useQuery는 ‘가져오기’ / useMutation은 ‘보내기’
useMutation 옵션
useMutation 옵션 목록
| 옵션 | 설명 | 기본값 | 타입 |
| gcTime | 비활성 캐시 데이터(Inactive)가 메모리에 남아 있는 시간(ms). | - | number \| Infinity |
| meta | 활용할 추가 정보를 지정. | - | Record<string, unknown> |
| mutationFn | 실행할 비동기 변이 함수. 필수 옵션! | - | |
useMutation 결과 반환 속성 목록
| 반환 속성 | 설명 | 타입 |
| data | 성공적으로 가져온 데이터. | undefined \| unknown |
| error | 오류가 발생했을 때의 오류 객체. 오류가 발생하지 않았다면 null. | null \| TError |
| failureCount | 변이의 실패 횟수. 변이가 실패할 때마다 증가하고 변이가 성공하면 0으로 재지정. | number |
onSuccess
- 뮤테이션이 성공했을 때 실행되는 콜백 함수
- 주로 쿼리 무효화에 사용
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
- 캐시된 데이터는 mutation이 발생하기 이전 데이터
- 캐시된 데이터와 수정된 데이터 동기화가 필요함
- 그래서 queryClient 내부에 invalidateQueries 옵션으로 mutation한 key를 넣어서 해당 캐시 데이터를 최신화하는 것
- 쿼리 무효화 흐름
posts: [게시글1, 게시글2, 게시글3]
mutate({ title: '새 글', content: '...' })
↓
Tanstack Query Devtools
- provider 내부에 Devtools 컴포넌트를 삽입
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient()
- 클릭 시 현재 캐시된 데이터 확인 및 데이터 리프레싱 등을 가능
- Production 모드에서는 사용 불가
)
return <div>{JSON.stringify(data)}</div>
<div>데이터가 {isStale ? '상했어요..' : '신선해요!'}</div>
<div>{JSON.stringify(data)}</div>
<QueryClientProvider client={queryClient}>
{
data
}
=
useQuery
<
ResponseValue
>
(
{
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
return <div>{data?.time}</div>
errorUpdateCount
| errorUpdatedAt | 최근에 오류가 발생한 시간(유닉스 타임스탬프). | number |
| failureCount | 쿼리의 실패 횟수. 쿼리가 실패할 때마다 증가하고 쿼리가 성공하면 0으로 재지정. | number |
| failureReason | 쿼리의 재시도 실패 이유. 쿼리가 성공하면 null로 재지정. | null \| TError |
| fetchStatus | 'fetching': 쿼리 함수가 실행 중.(첫 대기 및 백그라운드 다시 가져오기 포함, isFetching)<br>'paused': 쿼리 함수의 가져오기가 일시 중단됨.(isPaused)<br>'idle': 쿼리 함수가 동작 중이지 않음. | 'fetching' \| 'paused' \| 'idle' |
| isError | 쿼리 함수에서의 오류 발생 여부. | boolean |
| isFetched | 쿼리의 첫 데이터 가져오기가 완료되었는지 여부. | boolean |
| isFetchedAfterMount | 컴포넌트 연결 후 가져오기가 완료되었는지 여부. 컴포넌트 연결 전에 캐시된 데이터를 표시하지 않는 용도로 사용. | boolean |
| isFetching | 쿼리 함수가 실행 중.(첫 대기 및 백그라운드 다시 가져오기 포함) | boolean |
| isLoading | 쿼리 함수의 첫 번째 가져오기가 진행 중. isFetching && isPending와 같음. | boolean |
| isLoadingError | 쿼리 함수의 첫 번째 가져오기 중 실패 여부. | boolean |
| isPaused | 쿼리 가져오기가 일시 중단됨. | boolean |
| isPending | 캐시된 데이터가 없고 쿼리가 아직 완료되지 않은 상태. | boolean |
| isPlaceholderData | 표시된 데이터가 대체 데이터인지 여부. | boolean |
| isRefetchError | 쿼리가 다시 가져오기를 시도하는 중에 실패했는지 여부. | boolean |
| isRefetching | 백그라운드에서 다시 가져오기가 진행 중인지의 여부. isFetching && !isPending와 같음. | boolean |
| isStale | 캐시된 데이터가 무효화(Invalidated)되거나 staleTime이 경과된 여부. | boolean |
| isSuccess | 쿼리 데이터를 성공적으로 가져왔는지 여부. | boolean |
| refetch | 데이터를 새롭게 다시 가져오는 함수. throwOnError: true 옵션을 사용해야 오류가 발생. | (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult> |
| status | 'pending': 캐시된 데이터가 없고 아직 완료되지 않은 상태.(isPending)<br>'error': 오류가 발생한 상태.(isError)<br>'success': 데이터를 성공적으로 가져온 상태.(isSuccess) | 'pending' \| 'error' \| 'success' |
)
alert
(
"에러 발생"
)
;
return <UserData data={data} />;
| 쿼리가 생성되거나 캐시되기 전에 사용하는 초기 데이터. |
| initialDataUpdatedAt | 초기 데이터의 마지막 업데이트 시간 지정. | - | number \| (() => number \| undefined) |
| meta | 활용할 추가 정보를 지정. | - | Record<string, unknown> |
| networkMode | 네트워크 모드 지정. | 'online' | 'online' \| 'always' \| 'offlineFirst' |
| notifyOnChangeProps | 컴포넌트 리랜더링을 위해 변경 여부를 확인할 쿼리의 특정 반환 속성 목록. 예시: ['data', 'error'] | 컴포넌트에서 접근한 반환 속성 | string[] \| "all" \| (() => string[] \| "all") |
| placeholderData | 대기(Pending) 중인 상태에서 사용할 데이터. | - | TData \| (previousValue: TData \| undefined, previousQuery: Query \| undefined) => TData |
| queryClient | 커스텀 쿼리 클라인트 연결 | - | QueryClient |
| queryFn | 데이터를 가져오는 쿼리 함수로, 꼭 데이터를 반환하거나 오류를 던져야 함. 기본 쿼리 함수가 지정되지 않은 경우에만 필수 옵션! | - | (context: QueryFunctionContext) => Promise<TData> |
| queryKey | 고유한 쿼리 키(식별자). 필수 옵션! | - | unknown[] |
| queryKeyHashFn | 쿼리 키를 해시하는 함수. | - | (queryKey: QueryKey) => string |
| refetchInterval | 데이터 자동 갱신(다시 가져오기)의 시간 간격(ms). | - | number \| false \| ((query: Query) => number \| false \| undefined) |
| refetchIntervalInBackground | 백그라운드에서 데이터 자동 갱신 여부. | false | boolean |
| refetchOnMount | useQuery 연결 시 데이터 갱신 여부. - true: 연결 시 데이터가 상한 경우만 갱신. - always: 연결 시 데이터 항상 갱신. | true | boolean \| "always" \| ((query: Query) => boolean \| "always") |
| refetchOnReconnect | 네트워크 재연결 시 데이터 갱신 여부. | true | boolean \| "always" \| ((query: Query) => boolean \| "always") |
| refetchOnWindowFocus | 브라우저 화면 포커스 시 데이터 갱신 여부. | true | boolean \| "always" \| ((query: Query) => boolean \| "always") |
| retry | 쿼리 실패 시 재시도 횟수. | 3 | boolean \| number \| (failureCount: number, error: TError) => boolean |
| retryDelay | 재시도 시간 간격(ms). | - | number \| (retryAttempt: number, error: TError) => number |
| retryOnMount | useQuery 연결 시 재시도 여부. | true | boolean |
| select | 가져온 데이터를 변형(선택)하는 함수. | - | (data: TData) => unknown |
| staleTime | 데이터가 상하는데 걸리는 시간(ms). | 0 | number \| ((query: Query) => number) |
| structuralSharing | 데이터 구조의 재사용을 최적화해, 불변성을 유지하고 불필요한 리렌더링 방지. | true | boolean \| (oldData: unknown \| undefined, newData: unknown) => unknown |
| throwOnError | 쿼리 실패 시 오류를 던질지 여부. | - | - |
1
,
b
:
2
}
]
}
)
;
useQuery({ queryKey: ["Jay", "study", 555, { a: 1, b: 2 }] });
useQuery({ queryKey: ["Jay", "study", 555, { b: 2, c: undefined, a: 1 }] });
useQuery({ queryKey: ["Jay", "study", 555, { a: 1, b: 2 }] });
useQuery({ queryKey: ["Jay", "study", 555, { a: 1, b: 2, c: 3 }] });
useQuery({ queryKey: ["Jay", "study"] });
useQuery({ queryKey: [555, "study", { a: 1, b: 2, c: 3 }, "Jay"] });
wait
]
,
queryFn: async () => (await fetch(`https://api.heropy.dev/v0/delay?t=${wait}`)).json(),
return <div>{data?.time}</div>
{
throw new Error('문제가 발생했습니다!')
res
=
await
fetch
(
'https://api.heropy.dev/v0/users'
)
const { users } = await res.json()
select: data => data.map(user => user.name)
,
"mimeType": "image/jpeg",
"url": "https://picsum.photos/id/406/400/400"
:
async
(
)
=>
{
const res = await fetch("https://api.heropy.dev/v0/users");
const { users } = await res.json();
{ id: "2", age: 20, name: "jay" },
{ id: "2", age: 25, name: "kim" },
select: (data) => data.map((user) => user.name),
return <div>{JSON.stringify(data)}</div>;
(variables: TVariables) => Promise<TData>
| mutationKey | queryClient.setMutationDefaults의 기본값 상속을 위한 키 | - | unknown[] |
| networkMode | 네트워크 모드 지정. | 'online' | 'online' \| 'always' \| 'offlineFirst' |
| onError | 변이 중 오류가 발생할 때 호출되는 함수. | - | (err: TError, variables: TVariables, context?: TContext) => Promise<unknown> \| unknown |
| onMutate | 변이 함수가 실행되기 전에 호출되는 함수. | - | (variables: TVariables) => Promise<TContext \| void> \| TContext \| void |
| onSettled | 변이가 성공하거나 실패해도 항상 호출되는 함수. | - | (data: TData, error: TError, variables: TVariables, context?: TContext) => Promise<unknown> \| unknown |
| onSuccess | 변이가 성공할 때 호출되는 함수. | - | (data: TData, variables: TVariables, context: TContext) => Promise<unknown> \| unknown |
| queryClient | 커스텀 쿼리 클라인트 연결. | - | QueryClient |
| retry | 변이 실패 시 재시도 횟수. | 0 | boolean \| number \| (failureCount: number, error: TError) => boolean |
| retryDelay | 재시도 시간 간격(ms). | - | number \| (retryAttempt: number, error: TError) => number |
| scope | 동시 실행 범위 지정. 같은 범위 ID를 가진 변이는 병렬이 아닌 직렬로 실행. | - | { id: string } |
| throwOnError | 변이 실패 시 오류를 던질지 여부. | undefined | undefined \| boolean \| (error: TError) => boolean |
| failureReason | 변이의 재시도 실패 이유. 쿼리가 성공하면 null로 재지정. | null \| TError |
| isError | 변이 함수에서의 오류 발생 여부. | boolean |
| isIdle | 변이 함수가 실행되기 전의 초기 상태인지 여부 | boolean |
| isPaused | 변이 함수가 일시 중단되었는지 여부 | boolean |
| isPending | 변이 함수가 실행 중인지 여부 | boolean |
| isSuccess | 데이터를 성공적으로 가져왔는지 여부. | boolean |
| mutate | 변이 실행 함수 | (variables: TVariables, { onSuccess, onSettled, onError }) => void |
| mutateAsync | 비동기 변이 실행 함수 | (variables: TVariables, { onSuccess, onSettled, onError }) => Promise<TData> |
| reset | 변이 내부 상태를 초기 상태로 재지정하는 함수 | () => void |
| status | 변이의 현재 상태. idle: 초기 상태 pending: 실행 중 error: 오류 발생 success: 성공 | string |
| submittedAt | 변이가 제출된 시간(유닉스 타임스탬프). | number |
| variables | 변이 실행 함수(mutate)에 전달된 데이터. | undefined \| TVariables |
queryClient.invalidateQueries({ queryKey: ['posts'] });
posts: [게시글1, 게시글2, 게시글3, 새 글] ← 최신화!
export default function App() {
<QueryClientProvider client={queryClient}>