Skip to Content
WebReactReact 실무04. TanStack Query 설치와 첫 조회

이번 편의 결과물: BookListPage가 로컬 배열 대신 mock API 응답으로 렌더됩니다. 로딩 스피너와 에러 메시지가 화면에 표시됩니다. · 다루는 개념: QueryClient/QueryClientProvider 설정, json-server mock API 서버, useQuery로 조회, 로딩/에러 상태

이 편에서 만드는 파일

bookshelf/ ├── db.json + (json-server용 mock 데이터) ├── package.json ~ (@tanstack/react-query 추가) └── src/ ├── main.jsx ~ (QueryClientProvider 추가) ├── api/ │ └── books.js + (책 목록 조회 함수) ├── hooks/ │ └── useBooks.js ~ (localStorage+reducer 기반 → useQuery 기반으로 교체) ├── reducers/ │ └── booksReducer.js − 삭제(TanStack Query 캐시가 대체) ├── utils/ │ └── booksStorage.js − 삭제(TanStack Query 캐시가 대체) └── pages/ └── BookListPage.jsx ~ (useBooks 훅으로 서버 데이터 렌더)

개념 정리

TanStack Query는 앱 전체에서 하나의 QueryClient 인스턴스를 공유합니다. QueryClientProvider로 컴포넌트 트리를 감싸면, 그 안 어디서든 useQuery로 서버 데이터를 조회할 수 있습니다.

요소역할
QueryClient캐시 저장소 하나. 앱 전체에서 인스턴스 하나만 만든다
QueryClientProviderReact Context로 QueryClient를 하위 트리에 전달
queryKey캐시 안에서 이 데이터를 식별하는 배열(예: ['books'])
queryFn데이터를 가져오는 비동기 함수. 반환값이 캐시에 저장됨
data, isLoading, isErroruseQuery가 반환하는, 화면이 바로 쓸 수 있는 상태

이 과목은 실제 서버 대신 json-server로 mock API를 띄웁니다. public/practice/bookshelf/db.json에 책 목록과 사용자 데이터가 들어 있고, json-server가 이 파일을 읽어 REST API처럼 응답합니다. 통합·E2E 테스트(15~16편)에서는 MSW로 같은 흐름을 재현하지만, 지금은 실제로 떠 있는 서버에 요청을 보내며 눈으로 확인합니다.

실습

1. mock API 서버 준비

https://zeno.it.kr/practice/bookshelf/db.json 파일을 내려받아 bookshelf/db.json으로 저장합니다.

curl -o db.json https://zeno.it.kr/practice/bookshelf/db.json

db.json은 다음 구조입니다.

// bookshelf/db.json (일부) { "books": [ { "id": 1, "title": "클린 코드", "author": "로버트 C. 마틴", "status": "done", "rating": 5, "pages": 584, "coverColor": "#2563eb", "startedAt": "2026-07-01", "finishedAt": "2026-07-20", "memo": "함수는 한 가지 일만." } ], "users": [ { "id": 1, "email": "zeno@example.com", "password": "password123", "name": "Zeno Kim" } ] }

2. 패키지 설치와 서버 실행

npm install @tanstack/react-query npx json-server db.json --port 3001

json-serverdb.jsonbooks 배열을 http://localhost:3001/books로, users 배열을 http://localhost:3001/users로 그대로 노출합니다. 이 터미널은 실습 내내 켜 둡니다.

3. 조회 함수 작성

// bookshelf/src/api/books.js const API_BASE = 'http://localhost:3001' export async function getBooks() { const response = await fetch(`${API_BASE}/books`) if (!response.ok) { throw new Error(`책 목록을 가져오지 못했습니다 (상태 코드: ${response.status})`) } return response.json() }

4. QueryClientProvider로 앱 감싸기

// bookshelf/src/main.jsx import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { RouterProvider } from 'react-router' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { router } from './router.jsx' import './styles/index.css' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000, retry: 1, }, }, }) createRoot(document.getElementById('root')).render( <StrictMode> <QueryClientProvider client={queryClient}> <RouterProvider router={router} /> </QueryClientProvider> </StrictMode>, )

react_2가 남긴 router.jsx(createBrowserRouter)를 그대로 씁니다. App.jsxreact_2 03편부터 이미 쓰이지 않으므로 새로 만들지 않습니다.

staleTime을 30초로 두면, 같은 화면에 30초 안에 다시 들어와도 캐시를 그대로 쓰고 네트워크 요청을 다시 보내지 않습니다.

5. useBooks 훅을 TanStack Query 기반으로 교체

react_2hooks/useBooks.jsuseReducerlocalStorage로 책 목록을 관리했습니다. 이 편부터는 같은 이름의 훅이 서버 데이터를 조회하는 useQuery 래퍼로 바뀝니다. 훅 이름과 사용법(컴포넌트에서 useBooks() 호출)은 그대로 두고 내부 구현만 교체해, BookListPage가 이후 편에서도 같은 훅 이름을 계속 쓸 수 있게 합니다.

// bookshelf/src/hooks/useBooks.js import { useQuery } from '@tanstack/react-query' import { getBooks } from '../api/books.js' export function useBooks(params = {}) { return useQuery({ queryKey: ['books', params], queryFn: () => getBooks(params), }) }

useBooksuseQuery가 반환하는 객체(data, isLoading, isError, error 등)를 그대로 돌려줍니다. 별도로 감싸지 않아, TanStack Query가 제공하는 값을 컴포넌트가 바로 씁니다.

// bookshelf/src/pages/BookListPage.jsx import { useBooks } from '../hooks/useBooks.js' import BookCard from '../components/BookCard.jsx' export default function BookListPage() { const { data: books, isLoading, isError, error } = useBooks() if (isLoading) { return <p role="status">책 목록을 불러오는 중입니다...</p> } if (isError) { return <p role="alert">목록을 불러오지 못했습니다: {error.message}</p> } return ( <section> <h1>내 서재</h1> <ul> {books.map((book) => ( <li key={book.id}> <BookCard book={book} /> </li> ))} </ul> </section> ) }

isLoading, isErroruseQuery가 대신 관리합니다. 컴포넌트는 세 가지 상태(로딩·에러·성공)를 분기하기만 하면 됩니다.

실행

npx json-server db.json --port 3001 npm run dev

두 명령을 각각 다른 터미널에서 실행합니다. json-server가 먼저 떠 있어야 브라우저에서 목록이 보입니다.

확인

  • 브라우저에서 /로 이동하면 잠깐 “책 목록을 불러오는 중입니다…”가 보인 뒤 6권의 책 목록으로 바뀐다
  • 브라우저 개발자 도구 네트워크 탭에 http://localhost:3001/books 요청이 찍혀 있다
  • json-server를 켠 터미널을 끄고 새로고침하면 “목록을 불러오지 못했습니다”가 표시된다

직접 해보기

staleTime을 0으로 바꾸고 화면을 왔다 갔다 하면 네트워크 탭에서 무엇이 달라지는지 확인해보세요.

staleTime이 0이면 컴포넌트가 다시 마운트될 때마다 데이터를 즉시 오래된 것으로 취급해, 화면 전환마다 /books 요청이 다시 나갑니다. 30초로 두면 그 시간 안에는 캐시를 재사용해 요청이 나가지 않습니다.

getBooks가 실패하는 상황을 재현하려면 어떻게 해야 할지 생각해보세요(서버를 끄는 것 말고 다른 방법).

API_BASE의 포트 번호를 일부러 틀리게 바꾸면(3001 대신 3099) 요청이 실패해 같은 에러 화면을 볼 수 있습니다.

자주 하는 실수

증상원인고치는 법
useQuery를 어디서든 쓸 수 없다는 에러가 난다QueryClientProvider로 감싸지 않음main.jsx에서 AppQueryClientProvider로 감싼다
목록이 영영 안 뜬다json-server를 안 켜거나 포트가 다름3001 포트로 json-server가 떠 있는지 확인
데이터가 화면에서 undefined로 보인다response.json()await 하지 않음getBooks에서 return response.json()으로 프라미스를 반환

확인 문제

문제 14지선다
QueryClientProvider를 앱 최상단에서 한 번만 감싸는 이유는 무엇입니까
문제 24지선다
useQuery의 queryKey 배열이 하는 역할은 무엇입니까
문제 34지선다
staleTime을 30초로 설정했을 때의 동작으로 옳은 것은 무엇입니까
문제 44지선다
이 편에서 json-server를 mock API로 쓰는 이유로 가장 알맞은 것은 무엇입니까
문제 54지선다
BookListPage 컴포넌트가 isLoading, isError를 직접 useState로 관리하지 않는 이유는 무엇입니까

참고 자료

Last updated on