어제 오늘 내일

[React] TanStack Query 사용법: 서버 상태 관리 제대로 하기 본문

IT/React

[React] TanStack Query 사용법: 서버 상태 관리 제대로 하기

hi.anna 2026. 8. 8. 22:13

1. 들어가며: 어떤 문제를 해결하는가

React에서 서버 데이터를 가져오는 가장 흔한 방법은 useEffectuseState를 조합하는 것입니다.

function TodoList() {
  const [todos, setTodos] = useState([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    setLoading(true)
    fetch('/api/todos')
      .then((res) => res.json())
      .then((data) => setTodos(data))
      .catch((err) => setError(err))
      .finally(() => setLoading(false))
  }, [])

  // ...
}

간단해 보이지만, 실제 프로덕션에서는 금방 문제가 드러납니다.

  • 같은 데이터를 여러 컴포넌트에서 각자 fetch하면서 중복 요청이 발생
  • 사용자가 다른 탭에 갔다 돌아왔을 때 최신 데이터로 갱신해야 하는데 그 로직이 없음
  • 요청이 실패했을 때 재시도하는 로직을 직접 구현해야 함
  • 데이터를 수정한 뒤 화면을 어떻게 갱신할지 캐시 무효화 로직을 매번 작성해야 함
  • loading, error, data 세 가지 상태를 관리하는 보일러플레이트가 컴포넌트마다 반복됨

TanStack Query(구 React Query)는 이런 "서버 상태(server state)"를 다루는 문제들을 표준화된 방식으로 해결해주는 라이브러리입니다. 클라이언트 상태(useState, Zustand 등으로 관리하는 UI 상태)와 서버 상태는 성격이 달라서, 별도의 도구로 다루는 게 낫다는 철학에서 출발합니다.

2. 설치 및 설정

npm install @tanstack/react-query

앱 최상단에서 QueryClient를 만들고 QueryClientProvider로 감싸줘야 합니다.

// main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'

const queryClient = new QueryClient()

function Root() {
  return (
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  )
}

3. 기본 사용법: useQuery로 데이터 가져오기

useQuery는 두 가지가 핵심입니다. 캐시를 구분하는 queryKey와, 실제로 데이터를 가져오는 queryFn입니다.

import { useQuery } from '@tanstack/react-query'

function TodoList() {
  const { data, isLoading, isError, error } = useQuery({
    queryKey: ['todos'],
    queryFn: () => fetch('/api/todos').then((res) => res.json()),
  })

  if (isLoading) return <p>불러오는 중...</p>
  if (isError) return <p>에러: {error.message}</p>

  return (
    <ul>
      {data.map((todo) => <li key={todo.id}>{todo.title}</li>)}
    </ul>
  )
}

useEffect 버전과 비교하면 loading/error/data 상태를 직접 관리하는 코드가 사라지고, "무엇을(queryKey) 어떻게(queryFn) 가져올지"만 선언하면 된다는 차이가 눈에 띕니다.

4. queryKey와 캐싱의 관계

queryKey는 단순한 이름표가 아니라 캐시를 구분하는 기준입니다. 같은 키를 가진 쿼리는 여러 컴포넌트에서 동시에 호출해도 실제 네트워크 요청은 한 번만 일어나고, 결과를 공유해서 사용합니다.

파라미터가 있는 요청은 배열 뒤에 값을 추가해서 키를 구분합니다.

function TodoDetail({ todoId }) {
  const { data } = useQuery({
    queryKey: ['todos', todoId],
    queryFn: () => fetch(`/api/todos/${todoId}`).then((res) => res.json()),
  })

  return <p>{data?.title}</p>
}

todoId가 바뀌면 TanStack Query가 자동으로 이를 감지해서 새로운 캐시 키로 다시 요청합니다. 의존성 배열을 따로 관리할 필요가 없습니다.

5. 자동 갱신 동작 이해하기

TanStack Query는 기본적으로 몇 가지 상황에서 데이터를 자동으로 다시 가져옵니다(refetch).

  • 브라우저 창에 다시 포커스가 돌아왔을 때 (refetchOnWindowFocus)
  • 네트워크가 끊겼다가 다시 연결됐을 때 (refetchOnReconnect)
  • 컴포넌트가 다시 마운트됐을 때, 캐시가 오래되었다면

이 동작이 필요 없는 쿼리라면 옵션으로 끌 수 있습니다.

useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
  staleTime: 1000 * 60 * 5,      // 5분간은 신선한 데이터로 간주, refetch 안 함
  refetchOnWindowFocus: false,    // 창 포커스 시 자동 refetch 끄기
})

staleTime은 얼마나 자주 바뀌는 데이터인지에 따라 조절하면 됩니다. 자주 바뀌지 않는 설정값 같은 데이터는 길게, 실시간성이 중요한 데이터는 짧게 잡는 식입니다.

6. useMutation으로 데이터 변경하기

조회가 아닌 생성·수정·삭제 같은 작업에는 useMutation을 사용합니다.

import { useMutation, useQueryClient } from '@tanstack/react-query'

function AddTodoForm() {
  const queryClient = useQueryClient()

  const mutation = useMutation({
    mutationFn: (newTodo) =>
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
      }).then((res) => res.json()),
    onSuccess: () => {
      // 'todos' 쿼리를 오래된 것으로 표시 → 자동으로 최신 데이터 다시 조회
      queryClient.invalidateQueries({ queryKey: ['todos'] })
    },
  })

  return (
    <button onClick={() => mutation.mutate({ title: '새 할 일' })} disabled={mutation.isPending}>
      {mutation.isPending ? '추가하는 중...' : '할 일 추가'}
    </button>
  )
}

onSuccess에서 invalidateQueries를 호출하는 패턴이 핵심입니다. 어떤 데이터가 바뀌었는지 직접 상태를 갱신하는 대신, "이 키에 해당하는 캐시는 이제 오래됐다"고 표시만 하면 TanStack Query가 알아서 최신 데이터를 다시 불러와 화면을 갱신해줍니다.

7. 다른 방식과 비교

항목 TanStack Query useEffect + useState Redux/Zustand로 서버 상태 관리
캐싱자동 (queryKey 기준)직접 구현 필요직접 구현 필요
중복 요청 방지자동직접 구현 필요직접 구현 필요
재검증/재시도내장 옵션 제공직접 구현 필요직접 구현 필요
로딩/에러 상태자동 제공직접 관리직접 관리
적합한 대상서버에서 오는 비동기 데이터간단한 일회성 요청UI/클라이언트 상태

중요한 포인트는 TanStack Query가 Redux나 Zustand를 대체하는 게 아니라는 점입니다. 서버에서 가져오는 데이터는 TanStack Query가, 모달 열림 여부나 폼 입력값 같은 순수 클라이언트 상태는 useState나 Zustand가 담당하는 식으로 역할을 나누는 것이 일반적인 조합입니다.

8. 마무리

TanStack Query는 "서버 상태는 클라이언트 상태와 다르게 다뤄야 한다"는 관점을 코드로 구현한 라이브러리입니다. loading/error/data 삼종세트를 매번 손으로 작성하던 코드를 걷어내고, 캐싱과 재검증, 중복 요청 방지 같은 까다로운 문제들을 라이브러리에 위임할 수 있습니다. API를 다루는 컴포넌트가 두 개 이상만 되어도 도입 효과를 바로 체감할 수 있을 만큼, React 프로젝트에서 사실상 표준으로 자리 잡은 도구입니다.

반응형
Comments