💡 fetching, caching, 서버 데이터 동기화를 지원하는 라이브러리. 비동기 쿼리의 전 과정 관리
refetchOnWindowFocus: 브라우저 포커스 시 refetch (default: true)refetchOnMount: 컴포넌트가 새로 mount되면 refetch (default: true)refetchOnReconnect: 네트워크 재연결 시 refetch (default: true)staleTime cacheTime QueryClientProvider로 감싸고 내부에서 QueryClient가 key 기반으로 캐싱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>
)
}


| 상태 | 의미 |
|---|---|
isPending | 아직 첫 데이터가 없음 (초기 로딩) |
isFetching | 네트워크 요청이 실행 중 (refetch 포함) |
import {
QueryClient,
QueryClientProvider,
useQuery,
} from '@tanstack/react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const { isPending, error, data } = useQuery({
queryKey: ['repoData'],
queryFn: () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(
(res) => res.json(),
),
})
if (isPending) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{' '}
<strong>✨ {data.stargazers_count}</strong>{' '}
<strong>🍴 {data.forks_count}</strong>
</div>
)
}
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>
</>
)
}
placeholderData: prev => prev 로 임시로 표기할 데이터 지정 가능
structuralSharing: 변경되지않은 데이터 재사용
meta: 쿼리에 대한 추가정보 제공
isFetching(쿼리함수), isPending(서버요청), isLoading(쿼리 첫번째 가져오기)
refetch 시 데이터 새롭게 갱신
const ids = [1, 2, 3]
const results = useQueries({
queries: ids.map((id) => ({
queryKey: ['post', id],
queryFn: () => fetchPost(id),
staleTime: Infinity,
})),
})
// 두 query에 대한 반환값이 배열로 묶여 반환된다!!
// 만일 반환된 배열에 대해 통합된 값을 불러오고 싶다면, 아래와 같이 combine 설정을 통해 데이터를 한 번에 반환할 수 있다. 이외에도 배열을 다루는 메서드들을 이용해 반환값에 대한 전처리를 수행할 수 있다!
const ids = [1, 2, 3]
const combinedQueries = useQueries({
queries: ids.map((id) => ({
queryKey: ['post', id],
queryFn: () => fetchPost(id),
})),
combine: (results) => {
return {
data: results.map((result) => result.data),
pending: results.some((result) => result.isPending),
}
},
})
function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return axios.post('/todos', newTodo)
},
onSuccess: () => queryClient.invalidateQueries(['todos'])
})
return (
<div>
{mutation.isLoading ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
getNextPageParam: 다음 페이지를 가져올 때 사용할 파라미터를 반환getPreviousPageParam: 이전 페이지용 파라미터(필요하다면)initialPageParam: 첫 요청에 사용할 초기 파라미터const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['movies'],
queryFn: fetchMovies,
getNextPageParam: lastPage => lastPage.nextCursor
})
import { Fragment, useState, useEffect, useRef } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
// ...
export default function MovieList() {
const [searchText, setSearchText] = useState('')
const [queryText, setQueryText] = useState('')
const observerEl = useRef<HTMLDivElement | null>(null)
const {
data,
// isLoading,
isFetching,
// isFetched,
hasNextPage,
fetchNextPage
} = useInfiniteQuery<Page>({
// ...
})
useEffect(() => {
const currentObserverEl = observerEl.current
const io = new IntersectionObserver(entries => {
if (entries[0].isIntersecting && hasNextPage) {
fetchNextPage()
}
})
if (currentObserverEl) {
io.observe(currentObserverEl)
}
return () => {
if (currentObserverEl) {
io.disconnect()
}
}
}, [hasNextPage, fetchNextPage])
// ...
return (
<>
{/* ... */}
{/* {isLoading ? <div>로딩 중..</div> : null}
{isFetched && hasNextPage && (
<button
disabled={isFetching}
onClick={() => fetchNextPage()}>
{isFetching ? '로딩 중..' : '더 보기!'}
</button>
)} */}
{isFetching ? <div>로딩 중..</div> : null}
<div
ref={observerEl}
style={{
display: isFetching ? 'none' : 'block',
height: '20px'
}}
/>
</>
)
}
import { useState, useEffec, useCallback } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useInView } from 'react-intersection-observer'
// ...
export default function MovieList() {
const [searchText, setSearchText] = useState('')
const [queryText, setQueryText] = useState('')
const { ref, inView } = useInView()
// ...
// useEffect(() => {
// const currentObserverEl = observerEl.current
// const io = new IntersectionObserver(entries => {
// if (entries[0].isIntersecting && hasNextPage) {
// fetchNextPage()
// }
// })
// if (currentObserverEl) {
// io.observe(currentObserverEl)
// }
// return () => {
// if (currentObserverEl) {
// io.disconnect()
// }
// }
// }, [hasNextPage, fetchNextPage])
useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage()
}
}, [inView, hasNextPage])
// ...
return (
<>
{/* ... */}
{isFetching ? <div>로딩 중..</div> : null}
<div
ref={ref}
style={{
display: isFetching ? 'none' : 'block',
height: '20px'
}}
/>
</>
)
}
React Query의 성능과 캐싱 전략은 queryKey 설계에 크게 영향 받음.
['todos']
['todos', userId]
['product', productId, 'reviews']
REST 방식과 유사한 패턴이 가장 직관적.
['user', userId]
['orders', userId, page]
// 비추천 ❌
['todos', { page, filter }]
// 추천 ⭕
['todos', page, filter]
쿼리의 조건이 되는 값은 반드시 key에 포함해야 한다.
useQuery({
queryKey: ['search', keyword],
enabled: !!keyword
})
QueryClientProvider)SWRConfigqueryFn 지정 useSWR(key) 사용React Query Devtools로 캐싱, 상태 확인이 매우 쉬움
React Query는 useInfiniteQuery를 공식 제공
(SWR은 커스텀 구현 필요)
select 옵션으로 raw data → 가공 데이터로 매핑 가능
여러 컴포넌트에서 동일한 쿼리를 사용할 경우
React Query는 refetch를 batch로 처리해 렌더링 성능 향상
cacheTime 기반으로 자동 GC 실행