TanStack Query(React Query)로 서버 상태를 관리하는 방법, useQuery와 useMutation, 캐싱과 자동 재패칭의 원리를 직접 만든 useFetch와 비교합니다.
🤔 서버 상태 vs 클라이언트 상태
지금까지 배운 useState와 Context는 클라이언트 상태를 관리합니다 — 폼 값, UI 토글, 사용자 선택 등 브라우저에서 만들어지고 브라우저에 사는 데이터입니다. 그런데 실제 앱 데이터 대부분은 서버에서 옵니다. 서버 상태는 성격이 다릅니다:
- 비동기로 가져와야 합니다
- 서버의 데이터가 언제든지 바뀔 수 있습니다 (다른 사용자가 수정)
- 캐싱해서 불필요한 중복 요청을 줄여야 합니다
- 오래된 데이터를 자동으로 갱신해야 합니다
이 문제를 해결하는 것이 TanStack Query(옛 React Query)입니다.
📦 설치 및 설정
npm install @tanstack/react-query @tanstack/react-query-devtools
// main.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5분간 신선한 데이터로 취급
retry: 2, // 실패 시 2번 재시도
},
},
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} /> {/* 개발 도구 */}
</QueryClientProvider>
)
🔍 useQuery — 데이터 패칭
import { useQuery } from '@tanstack/react-query'
interface Post { id: number; title: string; body: string }
async function fetchPosts(): Promise<Post[]> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
if (!res.ok) throw new Error('API 요청 실패')
return res.json()
}
function PostList() {
const { data: posts, isLoading, error, isRefetching } = useQuery({
queryKey: ['posts'], // 캐시 키 — 동일 키면 캐시된 데이터 사용
queryFn: fetchPosts, // 데이터를 가져오는 함수
})
if (isLoading) return <div>불러오는 중...</div>
if (error) return <div>오류: {error.message}</div>
return (
<>
{isRefetching && <span className="text-xs text-gray-400">업데이트 중...</span>}
<ul>
{posts?.map((post) => (
<li key={post.id}>
<h3>{post.title}</h3>
</li>
))}
</ul>
</>
)
}
같은 queryKey를 여러 컴포넌트에서 사용하면 하나의 요청만 발생하고, 캐시된 데이터를 공유합니다. 이것이 직접 만든 useFetch와의 결정적 차이입니다.
✏️ useMutation — 데이터 변경
import { useMutation, useQueryClient } from '@tanstack/react-query'
async function createPost(newPost: Omit<Post, 'id'>): Promise<Post> {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
body: JSON.stringify(newPost),
headers: { 'Content-Type': 'application/json' },
})
if (!res.ok) throw new Error('생성 실패')
return res.json()
}
function NewPostForm() {
const queryClient = useQueryClient()
const [title, setTitle] = useState('')
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// 성공 시 게시글 목록 캐시 무효화 → 자동 재패치
queryClient.invalidateQueries({ queryKey: ['posts'] })
setTitle('')
},
onError: (error) => {
console.error('생성 실패:', error)
},
})
return (
<form onSubmit={(e) => { e.preventDefault(); mutation.mutate({ title, body: '...', userId: 1 }) }}>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="제목" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '저장 중...' : '게시글 추가'}
</button>
{mutation.isError && <p className="text-red-500">{mutation.error.message}</p>}
</form>
)
}
🔄 직접 만든 useFetch vs TanStack Query
| 기능 | useFetch (직접) | TanStack Query |
|---|---|---|
| 캐싱 | 없음 | 자동 |
| 중복 요청 방지 | 없음 | 자동 (같은 queryKey) |
| 자동 재패치 | 없음 | 탭 포커스, 재연결 시 자동 |
| 재시도 | 없음 | 기본 3회 |
| 로딩/에러 상태 | 수동 | 자동 |
| Mutation | useEffect 직접 구현 | useMutation |
✅ 정리
- TanStack Query는 서버 상태 관리 전문 도구 — 캐싱, 재패치, 재시도를 자동으로 처리합니다.
useQuery: queryKey + queryFn으로 데이터를 패칭하고 캐싱합니다.useMutation: 생성/수정/삭제 +invalidateQueries로 캐시 갱신.- 실무 프로젝트에서는 직접
useFetch를 만드는 것보다 TanStack Query를 사용하세요.
관련 주제
- react-router-dom 설치
- Routes Route Link
- 페이지 이동
- 동적 경로
- 개발·프로그래밍
- 개발·프로그래밍 강의
- 리액트 입문 — 컴포넌트로 만드는 웹
- 무료강의
- 무료 온라인 강의
- NUGUNA
- 누구나
댓글
0/1000
불러오는 중...
