plogger

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 + useEffectTanStack Query
로딩/에러 처리직접 구현자동 제공
캐싱없음자동 캐싱
중복 요청 방지직접 구현자동 처리
백그라운드 갱신직접 구현자동 처리
코드량많음적음