
- TanStack Query란?
서버로부터 데이터 가져오기, 데이터 캐싱, 캐시 제어 등 데이터를 쉽고 효율적으로 관리할 수 있는 라이브러리이다.
✍ 대표적인 기능
queryKey)를 지정하게 된다.import { useQuery } from '@tanstack/react-query'
export default function DelayedData() {
const { data } = useQuery({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json()
})
return <div>{JSON.stringify(data)}</div>
}
다음 이미지는 쿼리 키와 일치하는 캐시된 데이터가 없을 때, 서버에서 새로운 데이터를 가져오는 과정이다.
서버에서 데이터를 가져오면 그 데이터는 캐시되고 그 이후 요청부터는 캐시된 데이터를 사용할 수 있다.

반대로 쿼리 키와 일치하는 캐시된 데이터가 있으면 서버에 요청하지 않고 캐시된 데이터를 사용한다.
같은 데이터를 가져오는 요청이 여러 번 발생해도, 캐시된 데이터를 사용하게 되어 중복 요청을 줄일 수 있다.

TanStack Query는 캐시한 데이터를 fresh 상태나 stale 상태로 구분하여 관리한다.
캐시된 데이터가 신선하다면 해당 데이터를 사용하고, 데이터가 상했다면 서버에 다시 요청에 새로운(신선한) 데이터를 가져온다.
데이터가 상하는 데까지 걸리는 시간은 staleTime 옵션으로 지정한다.
신선함의 여부는 isStale로 확인할 수 있다.
import { useQuery } from '@tanstack/react-query'
export default function DelayedData() {
const { data, isStale } = useQuery({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
staleTime: 1000 * 10 // 10초 후 상함. 즉, 10초 동안 신선함.
})
return (
<>
<div>데이터가 {isStale ? '상했어요..' : '신선해요!'}</div>
<div>{JSON.stringify(data)}</div>
</>
)
}
아래 명령어를 통해 TanStack Query를 설치한다. ESLint 플러그인을 사용할 수도 있다.
npm i @tanstack/react-query
npm i -D @tanstack/eslint-plugin-query
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'plugin:@tanstack/eslint-plugin-query/recommended'
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true }
],
// TanStack Query 권장 규칙! (plugin:@tanstack/eslint-plugin-query/recommended)
// '@tanstack/query/exhaustive-deps': 'error',
// '@tanstack/query/stable-query-client': 'error',
// '@tanstack/query/no-rest-destructuring': 'warn'
}
}
@tanstack/query/exhaustive-deps : 쿼리 함수에서 사용하는 외부 변수는 쿼리 키에 추가해야 한다.@tanstack/query/stable-query-client : 애플리케이션에서 하나의 쿼리 클라이언트를 생성해 사용해야 한다.@tanstack/query/no-rest-destructuring : 쿼리의 반환에서 나머지 매개변수(...rest)를 사용하면 안 된다.프로젝트 범위를
<QueryClientProvider>로 랩핑하고, 사용할queryClient인스턴스를 연결하면 사용할 준비가 끝난다.
import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
import DelayedData from '~/components/DelayedData'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<DelayedData />
</QueryClientProvider>
)
}
가장 기본적인 쿼리 훅으로, 컴포넌트에서 데이터를 가져올 때 사용한다.
const 반환 = useQuery<데이터타입>(옵션)
💻 지연 응답 API 예제
응답 데이터는 간단한 메시지와 응답 시간을 포함한다.
import { useQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData() {
const { data } = useQuery<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
staleTime: 1000 * 10 // 10초
})
return <div>{data?.time}</div>
}
// 단일 아이템 쿼리 키
useQuery({ queryKey: ['hello'] })
// 다중 아이템 쿼리 키
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
// 서로 같은 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { b: 2, c: undefined, a: 1 }] })
// 서로 다른 쿼리
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2 }] })
useQuery({ queryKey: ['hello', 'world', 123, { a: 1, b: 2, c: 3 }] })
useQuery({ queryKey: ['hello', 'world'] })
useQuery({ queryKey: [123, 'world', { a: 1, b: 2, c: 3 }, 'hello'] })
wait의 값이 다르면, 각각 별개의 요청을 전송한다.import { useQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData({ wait = 1000 }: { wait: number }) {
const { data } = useQuery<ResponseValue>({
queryKey: ['delay', wait],
queryFn: async () => (await fetch(`https://api.heropy.dev/v0/delay?t=${wait}`)).json(),
staleTime: 1000 * 10
})
return <div>{data?.time}</div>
}
import { QueryProvider } from './queryProvider'
import DelayedData from './components/DelayedData'
export default function App() {
return (
<QueryProvider>
<DelayedData />
<DelayedData wait={2000} />
<DelayedData wait={3000} />
</QueryProvider>
)
}
❗기본적으로 쿼리 함수(queryFn)에서 사용하는 변수는 쿼리 키에 포함되어야 한다.
그러면 변수가 변경될 때마다 자동으로 다시 가져올 수 있다.
exhaustive-deps 규칙을 비활성화한다.error 객체로 확인할 수 있고 기본적으로는 null이다.import { useQuery } from '@tanstack/react-query'
type ResponseValue = {
message: string
time: string
}
export default function DelayedData() {
const { data, error } = useQuery<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => {
const res = await fetch('https://api.heropy.dev/v0/delay?t=1000')
const data = await res.json()
if (!data.time) {
throw new Error('문제가 발생했습니다!')
}
return data
},
staleTime: 1000 * 10,
retry: 1
})
return (
<>
{data && <div>{JSON.stringify(data)}</div>}
{error && <div>{error.message}</div>}
</>
)
}
select)를 사용하면 가져온 데이터를 변형할 수 있다.queryFn이 반환하는 데이터를 인수로 받아서 select에서 처리하고 반환하면 최종 데이터가 된다.useQuery의 3번째 제네릭 타입으로 선언할 수 있다.💻 사용자 정보 API 예제
import { useQuery } from '@tanstack/react-query'
type Users = User[]
interface User {
id: string
name: string
age: number
}
export default function UserNames() {
const { data } = useQuery<Users, Error, string[]>({
queryKey: ['users'],
queryFn: async () => {
const res = await fetch('https://api.heropy.dev/v0/users')
const { users } = await res.json()
return users
},
staleTime: 1000 * 10,
select: data => data.map(user => user.name)
})
return (
<>
<h2>User Names</h2>
<ul>{data?.map((name, i) => <li key={i}>{name}</li>)}</ul>
</>
)
}
undefined)가 되면 화면이 깜빡일 수 있다.placedholderData 옵션을 사용하면, 쿼리 함수가 호출되는 pending에서 임시로 표시할 데이터를 미리 지정할 수 있다.// ...
export default function Movies() {
// ...
const { data: movies } = useQuery<Movie[]>({
queryKey: ['movies', searchText], // 검색어
queryFn: async () => {
const res = await fetch(`https://omdbapi.com?apikey=7035c60c&s=${searchText}`)
const { Search: movies } = await res.json()
return movies
},
placeholderData: prev => prev
})
// ...
}
// 이전 데이터
const prevUser = {
id: 'abc123',
name: 'Neo',
age: 22,
contact: {
email: 'neo@gmail.com',
address: {
country: 'Korea',
city: 'Seoul'
}
}
}
// 새로운 데이터
const newUser = {
id: 'abc123',
name: 'Neo',
age: 22,
contact: {
email: 'neo@gmail.com',
address: {
country: 'Korea',
city: 'Suwon' // 변경된 부분!
}
}
}
isFetching은queryFn이 아직 실행 중인지의 여부로, 데이터를 가져오는 중을 나타낸다.isPending은 캐시된 데이터가 없고 쿼리가 아직 완료되지 않은 상태의 여부로,initialData혹은placeholderData옵션으로 데이터를 제공하면 출력 대기(Pending)가 필요하지 않으므로 false를 반환한다.
enabled옵션을 false로 지정하면 쿼리가 대기 상태로 시작하므로 true를 반환한다.- isLoading은 isFetching && isPending와 같은 의미로, 쿼리의 첫 번째 가져오기가 진행 중인 경우를 나타낸다.
// ...
export default function DelayedData() {
const { data, isFetching, isPending, isLoading } = useQuery<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
staleTime: 1000 * 10
})
return (
<>
<div>isFetching: {JSON.stringify(isFetching)}</div>
<div>isPending: {JSON.stringify(isPending)}</div>
<div>isLoading: {JSON.stringify(isLoading)}</div>
<div>{data?.time}</div>
</>
)
}
refetch 함수를 사용하면 데이터를 항상 새롭게 다시 가져올 수 있다.// ...
export default function DelayedData() {
const { data, isStale, refetch } = useQuery<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
staleTime: 1000 * 10
})
return (
<>
<div>{data?.time}</div>
<div>데이터가 상했나요?: {JSON.stringify(isStale)}</div>
<button onClick={() => refetch()}>데이터 가져오기!</button>
</>
)
}
staleTime을 기반으로 데이터를 가져오려면 queryClient.fetchData()메서드를 사용할 수 있다.import { useQuery, useQueryClient, queryOptions } from '@tanstack/react-query'
// ...
const options = queryOptions<ResponseValue>({
queryKey: ['delay'],
queryFn: async () => (await fetch('https://api.heropy.dev/v0/delay?t=1000')).json(),
staleTime: 1000 * 10
})
export default function DelayedData() {
const queryClient = useQueryClient()
const { data, isStale } = useQuery(options)
function fetchData() {
const data = await queryClient.fetchQuery(options)
console.log(data) // 캐시된 데이터 or 새로 가져온 데이터
}
return (
<>
<div>{data?.time}</div>
<div>데이터가 상했나요?: {JSON.stringify(isStale)}</div>
<button onClick={fetchData}>데이터 가져오기!</button>
</>
)
}
❗단, queryKey와 staleTime을 기존 쿼리와 동일하게 제공해야 한다.(queryFn은 생략 가능)
만약 캐시된 데이터가 필요하다면 queryClient.getQueryData()를 사용한다.
데이터가 상해도 새로 가져오지 않고, 캐시된 데이터만 반환한다. (없다면 undefined)
간단히 TanStack Query(React Query)에 대해 정리를 해봤는데 생각보다 내용이 방대한 라이브러리인 것 같다.
유용한 기능들을 많이 가지고 있어서 앞으로 프로젝트를 할 때 실제로 사용해보고 싶은 생각이 들었다.
위에서 설명한 것들 외에도 여러 옵션들과 기능들이 추가로 있으니 필요할 때 찾아보자.
참고 링크
잘해내실 수 있습니다!