TanStack Query(React Query)로 서버 상태 관리하기
설치
npm install @tanstack/react-query
QueryClient 설정
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5분간 fresh 유지
retry: 2,
},
},
})
createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
useQuery — 데이터 조회
import { useQuery } from '@tanstack/react-query'
async function fetchPosts() {
const res = await fetch('/api/posts')
if (!res.ok) throw new Error('Failed to fetch')
return res.json()
}
function PostList() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
})
if (isLoading) return <p>로딩 중...</p>
if (isError) return <p>오류: {error.message}</p>
return (
<ul>
{data.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)
}
queryKey — 캐시 키 설계
// 목록
useQuery({ queryKey: ['posts'], queryFn: fetchPosts })
// 상세 — id에 따라 별도 캐시
useQuery({
queryKey: ['posts', postId],
queryFn: () => fetchPost(postId),
})
// 필터 포함
useQuery({
queryKey: ['posts', { status: 'published', page: 1 }],
queryFn: () => fetchFilteredPosts({ status: 'published', page: 1 }),
})
useMutation — 데이터 변경
import { useMutation, useQueryClient } from '@tanstack/react-query'
function CreatePost() {
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newPost) =>
fetch('/api/posts', {
method: 'POST',
body: JSON.stringify(newPost),
}).then(res => res.json()),
onSuccess: () => {
// 포스트 목록 캐시 무효화 → 자동 재조회
queryClient.invalidateQueries({ queryKey: ['posts'] })
},
})
return (
<button
onClick={() => mutation.mutate({ title: '새 글', content: '내용' })}
disabled={mutation.isPending}
>
{mutation.isPending ? '저장 중...' : '저장'}
</button>
)
}
낙관적 업데이트 (Optimistic Update)
서버 응답 전에 UI를 먼저 업데이트해 UX를 개선합니다.
const mutation = useMutation({
mutationFn: updatePost,
onMutate: async (updatedPost) => {
await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] })
const previous = queryClient.getQueryData(['posts', updatedPost.id])
queryClient.setQueryData(['posts', updatedPost.id], updatedPost)
return { previous }
},
onError: (err, updatedPost, context) => {
// 오류 시 롤백
queryClient.setQueryData(['posts', updatedPost.id], context.previous)
},
})
React Query vs 일반 상태 관리
| 항목 | useState + useEffect | TanStack Query |
|---|---|---|
| 로딩/에러 처리 | 직접 구현 | 자동 제공 |
| 캐싱 | 없음 | 자동 캐싱 |
| 중복 요청 방지 | 직접 구현 | 자동 처리 |
| 백그라운드 갱신 | 직접 구현 | 자동 처리 |
| 코드량 | 많음 | 적음 |