| 서론
안녕하세요, 팡일입니다!
이번 포스팅에서는 TanStack Query(이하 React Query)의 가장 핵심적인 훅인 useQuery를 깊이 있게 살펴보려고 합니다.
단순히 “데이터를 가져오는 방법”을 넘어서, React Query가 내부적으로 데이터를 어떻게 관리하는지, 그리고 우리가 어떤 옵션들을 통해 그 동작을 제어하고 애플리케이션을 최적화할 수 있는지를 실제 코드와 함께 차근차근 정리해보겠습니다.
| 들어가며: React Query는 단순한 데이터 페처가 아니다
많은 React 개발자들이 서버 데이터를 가져오기 위해 useEffect와 useState를 조합한 패턴을 사용해본 경험이 있을 것입니다.
하지만 이 방식에는 늘 같은 문제가 따라옵니다.
- 로딩 상태 관리
- 에러 상태 처리
- 불필요한 API 호출
- 캐싱 로직 구현
이 모든 것들이 반복적인 보일러플레이트 코드로 쌓이게 됩니다.
React Query는 바로 이 서버 상태(Server State)를 “가져오는 것(fetch)”이 아니라, “관리(manage)”하기 위해 탄생한 라이브러리입니다.
그리고 이 관리의 핵심에는 바로 캐시(Cache)가 있습니다.
- 한 번 가져온 데이터를 캐시에 저장하고
- 언제 재사용할지
- 언제 다시 서버에 요청할지
이 모든 판단을 React Query가 대신 처리해줍니다.
그 결과 우리는 비즈니스 로직과 UI에 더 집중할 수 있게 됩니다.
| 개발 환경 설정 및 Devtools
React Query를 사용하기 위한 최소한의 개발 환경 설정입니다.
(현재 프로젝트에서는 이미 설정이 완료되어 있다고 가정합니다.)
1) 패키지 설치
먼저 필요한 패키지들을 설치합니다.
- axios : HTTP 통신을 위한 클라이언트
- json-server : 목업 API 서버
npm install @tanstack/react-query @tanstack/react-query-devtools axios
npm install -D json-server
2) json-server로 API 서버 실행하기
프로젝트 루트에 위치한 db.json 파일이 우리의 간단한 데이터베이스 역할을 합니다.
{
"posts": [
{
"id": "1",
"title": "블로그에 오신걸 환영합니다. 수정되었습니다."
},
{
"id": "2",
"title": "react 강좌 첫 번째 시간"
}
]
}
package.json의 scripts에 아래와 같이 추가하면 npm run server 명령어로 API 서버를 실행할 수 있습니다.
// package.json
"scripts": {
"server": "json-server --watch db.json"
}
3) QueryClient 설정 및 ReactQueryDevtools
React Query의 모든 기능은 QueryClient를 통해 제공됩니다.
따라서 애플리케이션 최상단에서 QueryClientProvider로 앱을 감싸주어야 합니다.
// src/App.jsx
import TodoList from "./TodoList";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// QueryClient는 캐시와 쿼리 전반을 관리하는 핵심 객체입니다.
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<TodoList />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default App;
특히 ReactQueryDevtools는 쿼리 캐시의 상태 변화를 실시간으로 시각화해주는 강력한 도구이므로, 개발 중에는 반드시 활성화해두는 것을 추천합니다.
| 캐시의 생명주기: Fresh, Stale, Inactive, 그리고 GC
React Query의 캐시 데이터는 명확한 생명주기(lifecycle)를 가집니다.
Devtools에서는 색상으로 현재 상태를 한눈에 확인할 수 있습니다.
- Fresh (신선한 상태, 초록색)
: 데이터가 최신이라고 간주되는 상태
→ 이 상태에서는 재요청이 발생하지 않습니다. - Stale (오래된 상태, 빨간색)
: 데이터가 오래되었다고 판단된 상태
→ 화면에는 캐시 데이터를 사용하지만, 특정 조건에서 백그라운드 refetch가 발생합니다. (이것이 바로 stale-while-revalidate 전략입니다.) - Inactive (비활성 상태, 회색)
: 해당 쿼리를 사용하는 컴포넌트가 모두 언마운트된 상태
→ 일정 시간이 지나면 GC 대상이 됩니다. - Garbage Collected (GC)
: 캐시에서 완전히 제거된 상태
| 생명주기를 제어하는 useQuery 핵심 옵션 (1) – 시간 설정
이 캐시 생명주기는 옵션을 통해 직접 제어할 수 있습니다.
1) staleTime
- 데이터가 Fresh 상태로 유지되는 시간
- 기본값: 0
staleTime: 0
이는 데이터를 가져오자마자 즉시 Stale 상태가 된다는 의미입니다.
만약 데이터 변경이 잦지 않다면 다음과 같이 설정할 수 있습니다.
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 1000 * 60 * 5, // 5분
});
→ 불필요한 API 호출을 크게 줄일 수 있습니다.
2) gcTime
- Inactive 상태의 데이터가 캐시에 유지되는 시간
- 기본값: 1000 * 60 * 5 (5분)
staleTime = 데이터의 유통기한
gcTime = 냉장고 정리 주기
- gcTime은 항상 staleTime보다 같거나 길게 설정하는 것이 일반적입니다.
- 그래야 캐시를 재활용할 수 있습니다.
| 생명주기를 제어하는 useQuery 핵심 옵션 (2) – 자동 재요청
React Query는 사용자 경험을 위해 특정 상황에서 자동으로 refetch를 수행합니다.
- refetchOnWindowFocus (기본값: true)
- refetchOnMount (기본값: true)
- refetchOnReconnect (기본값: true)
필요하지 않다면 개별적으로 끌 수 있습니다.
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
refetchOnWindowFocus: false,
refetchOnMount: false,
});
| 그 외 유용한 useQuery 옵션들
- enabled
→ 조건부 쿼리 실행 (버튼 클릭 후 실행 등) - retry
→ 실패 시 재시도 횟수 (기본값: 3) - select
→ 서버 응답 데이터를 가공해서 전달 - placeholderData
→ 로딩 중 보여줄 임시 데이터
| 실전 예제: TodoList.jsx 코드 분석
const { data, refetch } = useQuery({
queryKey: ["pstList"],
queryFn: async () => {
const response = await fetch("http://localhost:3000/posts");
return await response.json();
},
});
1) 동작 흐름
- 컴포넌트 마운트
- 캐시 없음 → 서버 요청
- 데이터 수신
- staleTime이 0 → 즉시 Stale
- 포커스 복귀 시 자동 refetch
2) 최적화 제안
staleTime: 1000 * 60 // 1분
→ 실시간성이 필요 없다면 API 호출 수를 크게 줄일 수 있습니다.
| 맺음말
이번 포스팅에서는 useQuery의 핵심인 캐시 생명주기와 옵션들을 살펴보았습니다.
React Query를 제대로 활용하기 위한 열쇠는 “언제 데이터를 다시 가져올 것인가”를 이해하는 것입니다.
'💻 개발 > 🦕 React' 카테고리의 다른 글
| [React] React + Vite + TS + Firebase Hosting + Github Action 구축하기 (0) | 2026.01.18 |
|---|---|
| [React] TanStack Query(React Query) 활용하기 - useMutation으로 데이터 생성·수정·삭제 마스터하기 (0) | 2026.01.15 |
| [React] 코드 스플리팅을 적용했을 뿐인데, 초기 로딩이 줄어들었습니다 (0) | 2025.12.27 |
| [React] public 이미지에서 import 구조로 (이미지 최적화 개선기) (1) | 2025.12.27 |
| [React] target="_blank" ESLint 경고의 의미와 해결 방법 (react/jsx-no-target-blank) (0) | 2025.12.27 |
