React Query (TanStack Query)
목차
2.
1. React Query란?
서버 데이터를 React 컴포넌트에서 쉽게 가져오고, 캐싱하고, 동기화하기 위한 라이브러리
기존 방식의 문제
// ❌ 기존 방식 - 매번 직접 관리해야 할 것들
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
setIsLoading(true)
fetch('/api/boards')
.then(res => res.json())
.then(data => { setData(data); setIsLoading(false) })
.catch(err => { setError(err); setIsLoading(false) })
}, [])
JavaScript
복사
React Query 방식
// ✅ React Query - 캐싱, 로딩, 에러, 리패치 자동 처리
const { data, isLoading, isError } = useQuery({
queryKey: ['boards'],
queryFn: () => fetch('/api/boards').then(res => res.json()),
})
JavaScript
복사
React Query가 자동으로 처리하는 것들
기능 | 설명 |
캐싱 | 같은 키로 요청 시 서버 요청 없이 캐시 반환 |
중복 요청 제거 | 동시에 같은 쿼리 실행 시 요청 1번만 전송 |
백그라운드 리패치 | 창 포커스 시 자동으로 최신 데이터 갱신 |
로딩/에러 상태 | isLoading, isError 자동 제공 |
페이지 이탈 후 복귀 | 이전 데이터 즉시 표시 후 백그라운드 갱신 |
재시도 | 실패 시 자동 재시도 (횟수 설정 가능) |
2. 설치
npm install @tanstack/react-query @tanstack/react-query-devtools
Bash
복사
패키지 | 역할 |
@tanstack/react-query | 핵심 기능 (useQuery, useMutation 등) |
@tanstack/react-query-devtools | 개발용 캐시 상태 시각화 도구 |
3. QueryClient - 캐시 저장소
QueryClient는 React Query의 캐시 저장소이자 설정 컨테이너다.
앱 전체에서 하나의 인스턴스만 생성한다.
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 네트워크 실패 시 재시도 횟수 (기본값: 3)
staleTime: 1000 * 30, // 데이터를 "신선"하다고 보는 시간(ms) (기본값: 0)
},
},
})
JavaScript
복사
defaultOptions.queries 주요 옵션
옵션 | 타입 | 기본값 | 설명 |
retry | number \| boolean | 3 | 실패 시 재시도 횟수. false면 재시도 안 함 |
staleTime | number | 0 | 캐시를 "신선"하다고 보는 시간(ms). 이 시간 이내면 서버 재요청 없음 |
gcTime | number | 5 * 60 * 1000 | 사용되지 않는 캐시를 메모리에서 유지하는 시간(ms) |
refetchOnWindowFocus | boolean | true | 창 포커스 시 자동 리패치 여부 |
refetchOnMount | boolean | true | 컴포넌트 마운트 시 리패치 여부 |
이 프로젝트 설정
// src/main.jsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1, // 기본 3회 → 1회로 줄여 빠른 에러 감지
staleTime: 1000 * 30, // 30초간 캐시 유효 (게시판 특성상 자주 바뀌지 않음)
},
},
})
JavaScript
복사
4. QueryClientProvider - 전역 공급자
QueryClientProvider는 React Context를 사용해 앱 전체에 QueryClient를 공급하는 컴포넌트다.
useQuery, useMutation, useQueryClient 등의 훅이 이 Context에서 QueryClient를 가져온다.
// src/main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient({ /* 설정 */ })
createRoot(document.getElementById('root')).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</StrictMode>
)
JavaScript
복사
구조 원리
<QueryClientProvider client={queryClient}> ← QueryClient를 Context에 등록
<App>
<ListPage>
<List>
useQuery(...) ← Context에서 QueryClient를 꺼내 캐시 조회
</List>
</ListPage>
</App>
</QueryClientProvider>
Plain Text
복사
주의사항QueryClientProvider 밖에서 useQuery 등을 호출하면 에러가 발생한다.
<App />을 반드시 <QueryClientProvider> 안에 넣어야 한다.
5. useQuery - 데이터 조회
서버에서 데이터를 읽어오는 훅. GET 요청에 주로 사용한다.
기본 구조
const { data, isLoading, isError, error } = useQuery({
queryKey: ['고유한 키'], // 캐시 식별자
queryFn: () => fetch(...), // 실제 데이터를 가져오는 함수
})
JavaScript
복사
queryKey - 캐시 식별자
queryKey는 캐시를 구분하는 배열 형태의 고유 키다.
키가 바뀌면 자동으로 새 데이터를 요청한다.
// 단순 키 - 변하지 않는 데이터
queryKey: ['boards']
// 파라미터 포함 - 페이지마다 다른 캐시
queryKey: ['boards', page, size] // ['boards', 1, 10], ['boards', 2, 10] 각각 별도 캐시
// ID 포함 - 게시글마다 다른 캐시
queryKey: ['board', id] // ['board', 1], ['board', 2] 각각 별도 캐시
JavaScript
복사
queryKey 설계 원칙
파라미터가 달라지면 다른 데이터가 나오는 경우 반드시 키에 포함한다.
queryFn - 데이터 요청 함수
queryFn은 Promise를 반환해야 한다.
// axios를 사용하는 경우 - .then()으로 실제 데이터만 추출
queryFn: () => boardsApi.list(page, size).then((res) => res.data)
// fetch를 사용하는 경우
queryFn: () => fetch('/api/boards').then(res => res.json())
// async/await 방식
queryFn: async () => {
const res = await boardsApi.list(page, size)
return res.data
}
JavaScript
복사
반환값
const {
data, // queryFn이 반환한 데이터 (초기에는 undefined)
isLoading, // 처음 로딩 중 (데이터 없고 fetch 중)
isFetching, // 백그라운드 포함 모든 fetch 중
isError, // 에러 발생 여부
error, // 에러 객체
isSuccess, // 성공 여부
refetch, // 수동으로 다시 가져오기
status, // 'pending' | 'error' | 'success'
} = useQuery({ queryKey, queryFn })
JavaScript
복사
isLoading vs isFetching
•
isLoading : 캐시 데이터가 없고 처음 요청 중일 때만 true
•
isFetching : 백그라운드 리패치 포함 어떤 fetch든 진행 중이면 true
주요 옵션
useQuery({
queryKey: ['boards', page],
queryFn: () => boardsApi.list(page),
enabled: !!id, // false면 쿼리 실행 안 함 (조건부 실행)
staleTime: 1000 * 60, // 1분간 신선 (전역 설정 오버라이드)
placeholderData: (prev) => prev, // 키 변경 시 이전 데이터를 임시로 표시
select: (data) => data.list, // 반환 데이터 변환/선택
retry: false, // 실패 시 재시도 안 함
})
JavaScript
복사
enabled - 조건부 실행
// id가 있을 때만 요청 (useBoard.js)
const { data } = useQuery({
queryKey: ['board', id],
queryFn: () => boardsApi.select(id).then(res => res.data),
enabled: !!id, // id가 undefined/null이면 요청 안 함
})
JavaScript
복사
placeholderData - 페이지 전환 시 깜빡임 방지
// useBoards.js - 페이지 이동 시 이전 목록을 유지해 화면 깜빡임 방지
useQuery({
queryKey: ['boards', page, size],
queryFn: () => boardsApi.list(page, size).then(res => res.data),
placeholderData: (previousData) => previousData,
// ↑ 새 데이터 도착 전까지 이전 페이지 데이터를 임시 표시
})
JavaScript
복사
6. useMutation - 데이터 변경
서버 데이터를 생성/수정/삭제하는 훅. POST, PUT, DELETE 요청에 사용한다.
기본 구조
const mutation = useMutation({
mutationFn: (변수) => 서버요청함수(변수),
onSuccess: (data) => { /* 성공 후 처리 */ },
onError: (error) => { /* 실패 후 처리 */ },
})
// 호출
mutation.mutate(전달할값)
JavaScript
복사
mutationFn
// 단순 인자
mutationFn: (id) => boardsApi.remove(id)
// 객체 인자 (여러 값 전달)
mutationFn: ({ data, headers }) => boardsApi.insert(data, headers)
// 배열 인자
mutationFn: (idList) => filesApi.removeFiles(idList)
JavaScript
복사
콜백 옵션
useMutation({
mutationFn: (data) => boardsApi.insert(data),
onSuccess: (responseData, variables, context) => {
// 요청 성공 시 호출
// responseData: 서버 응답값
// variables: mutate()에 전달한 값
},
onError: (error, variables, context) => {
// 요청 실패 시 호출
console.error(error.message)
},
onSettled: (data, error, variables) => {
// 성공/실패 무관하게 항상 호출 (finally와 유사)
},
onMutate: async (variables) => {
// 요청 직전에 호출 (낙관적 업데이트에 활용)
},
})
JavaScript
복사
반환값
const {
mutate, // 뮤테이션 실행 함수 (비동기 결과 무시)
mutateAsync, // 뮤테이션 실행 함수 (Promise 반환, await 가능)
isPending, // 요청 진행 중
isSuccess, // 요청 성공
isError, // 요청 실패
error, // 에러 객체
data, // 성공 응답 데이터
reset, // 상태 초기화
} = useMutation({ mutationFn })
JavaScript
복사
mutate vs mutateAsync
// mutate - fire-and-forget, 에러는 onError로 처리
mutation.mutate(data)
// mutateAsync - Promise 반환, try/catch 가능
try {
const result = await mutation.mutateAsync(data)
console.log(result)
} catch (error) {
console.error(error)
}
JavaScript
복사
isPending - 버튼 비활성화
// 저장 중 버튼 비활성화 (useBoardMutations.js)
return {
insertBoard: (data, headers) => insertMutation.mutate({ data, headers }),
isInserting: insertMutation.isPending, // 요청 중이면 true
}
// 컴포넌트에서
<button type="submit" disabled={isInserting}>
{isInserting ? '저장 중...' : '저장'}
</button>
JavaScript
복사
7. useQueryClient - 캐시 직접 조작
useQueryClient는 QueryClient 인스턴스에 직접 접근하는 훅이다.
주로 뮤테이션 성공 후 캐시를 무효화하거나 직접 업데이트할 때 사용한다.
import { useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
JavaScript
복사
invalidateQueries - 캐시 무효화 (가장 자주 사용)
캐시를 "만료됨"으로 표시해 다음 렌더링 시 서버에서 최신 데이터를 가져오게 한다.
// 특정 키 무효화
queryClient.invalidateQueries({ queryKey: ['boards'] })
// 특정 키 + 파라미터 무효화
queryClient.invalidateQueries({ queryKey: ['board', id] })
// 'boards'로 시작하는 모든 캐시 무효화 (부분 매칭)
queryClient.invalidateQueries({ queryKey: ['boards'] })
// → ['boards'], ['boards', 1, 10], ['boards', 2, 10] 모두 무효화
JavaScript
복사
getQueryData / setQueryData - 캐시 읽기/쓰기
// 현재 캐시 읽기 (서버 요청 없음)
const boards = queryClient.getQueryData(['boards', 1, 10])
// 캐시 직접 업데이트 (서버 요청 없이 UI 즉시 반영 - 낙관적 업데이트)
queryClient.setQueryData(['board', id], (oldData) => ({
...oldData,
board: { ...oldData.board, title: newTitle }
}))
JavaScript
복사
prefetchQuery - 미리 요청
// 다음 페이지를 미리 가져와 캐시에 저장
await queryClient.prefetchQuery({
queryKey: ['boards', nextPage],
queryFn: () => boardsApi.list(nextPage).then(res => res.data),
})
JavaScript
복사
이 프로젝트에서의 무효화 패턴
// useBoardMutations.js
const queryClient = useQueryClient()
// 글 등록 성공 → 목록 캐시 무효화
onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ['boards'] })
// → 목록 페이지 재진입 시 최신 목록 로드
}
// 글 수정 성공 → 목록 + 단건 캐시 무효화
onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ['boards'] }) // 목록
queryClient.invalidateQueries({ queryKey: ['board', id] }) // 상세
}
// 파일 삭제 성공 → 해당 게시글 캐시 무효화
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['board', id] })
// → 파일 목록이 즉시 갱신됨
}
JavaScript
복사
8. ReactQueryDevtools - 디버깅 도구
개발 환경에서 캐시 상태를 시각적으로 확인하는 플로팅 패널이다.
프로덕션 빌드에는 자동으로 포함되지 않는다.
// src/main.jsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
{/* ↑ 화면 우하단에 플로팅 버튼으로 표시 */}
</QueryClientProvider>
JavaScript
복사
확인 가능한 정보
항목 | 설명 |
쿼리 목록 | 현재 캐시에 저장된 모든 쿼리 키 |
쿼리 상태 | fresh / stale / fetching / inactive |
캐시 데이터 | 쿼리별 실제 저장된 데이터 |
마지막 갱신 시간 | 언제 마지막으로 서버에서 가져왔는지 |
수동 리패치 | Devtools에서 직접 리패치 실행 가능 |
쿼리 상태 색상
색상 | 상태 | 의미 |
초록 | fresh | 신선한 데이터 (staleTime 이내) |
노랑 | stale | 만료됨 (다음 접근 시 리패치 예정) |
파랑 | fetching | 현재 서버에서 가져오는 중 |
회색 | inactive | 사용 중인 컴포넌트 없음 (gcTime 후 삭제) |
9. 캐시 라이프사이클
컴포넌트 마운트
│
▼
캐시에 데이터 있음?
├─ 없음 ──► 서버 요청 → 데이터 저장 → isLoading: true 동안 로딩 표시
│
└─ 있음
│
├─ fresh? (staleTime 이내)
│ └─ 캐시 즉시 반환 (서버 요청 안 함)
│
└─ stale? (staleTime 초과)
└─ 캐시 즉시 반환 + 백그라운드에서 서버 재요청 (isFetching: true)
│
└─ 새 데이터 도착 → 캐시 교체 → UI 갱신
Plain Text
복사
staleTime vs gcTime
데이터 도착
│
├── staleTime 이내 → "fresh" (재요청 안 함)
│
└── staleTime 초과 → "stale" (다음 접근 시 백그라운드 재요청)
컴포넌트 언마운트
│
└── gcTime 이내 → 캐시 유지 (다시 마운트 시 즉시 사용)
gcTime 초과 → 캐시 삭제 (다시 마운트 시 새 요청)
Plain Text
복사
10. 이 프로젝트에서의 활용 패턴
전체 구조
main.jsx
QueryClient (retry: 1, staleTime: 30s)
QueryClientProvider
App
└─ 각 페이지 → 컴포넌트 → 커스텀 훅
├─ useBoards(page, size) → useQuery(['boards', page, size])
├─ useBoard(id) → useQuery(['board', id])
└─ useBoardMutations(id) → useMutation + useQueryClient
Plain Text
복사
데이터 흐름 - 목록 조회
ListPage 마운트
└─ useBoards(1, 10) 호출
└─ useQuery({ queryKey: ['boards', 1, 10], ... })
├─ 캐시 없음 → boardsApi.list(1, 10) 호출 → GET /api/boards?page=1&size=10
│ isLoading: true → 로딩 표시
│ 응답 도착 → 캐시 저장 → UI 표시
│
└─ 캐시 있음 (30초 이내) → 즉시 반환, 서버 요청 없음
Plain Text
복사
데이터 흐름 - 글 등록 후 목록 갱신
Insert 컴포넌트
└─ onSubmit → insertBoard(formData) 호출
└─ insertMutation.mutate({ data: formData, headers })
└─ boardsApi.insert() → POST /api/boards
└─ onSuccess:
├─ queryClient.invalidateQueries(['boards'])
│ ↓
│ ['boards', 1, 10] 캐시 "만료"로 표시
│
├─ Swal 성공 알림
└─ navigate('/boards')
↓
ListPage 마운트
└─ useBoards() 재실행
└─ 캐시 만료 → 서버 재요청 → 최신 목록 표시
Plain Text
복사
커스텀 훅으로 분리하는 이유
❌ 컴포넌트 안에 직접 작성
List.jsx → useQuery 로직 포함
Update.jsx → useMutation 로직 포함
→ 로직이 컴포넌트에 섞여 재사용 불가
✅ 커스텀 훅으로 분리
useBoards.js → 목록 조회 로직 캡슐화
useBoard.js → 단건 조회 로직 캡슐화
useBoardMutations.js → CRUD 로직 캡슐화
→ 여러 컴포넌트에서 재사용 가능
→ 테스트 용이
→ 컴포넌트는 UI에만 집중
Plain Text
복사
실제 코드 예시 비교
// useBoards.js
export const useBoards = (page = 1, size = 10) => {
const { data, isLoading, isError } = useQuery({
queryKey: ['boards', page, size], // 페이지마다 별도 캐시
queryFn: () => boardsApi.list(page, size).then(res => res.data),
placeholderData: (prev) => prev, // 페이지 이동 시 이전 데이터 유지
})
return { list: data?.list ?? [], pagination: data?.pagination ?? {}, isLoading, isError }
}
// useBoard.js
export const useBoard = (id) => {
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['board', id], // id마다 별도 캐시
queryFn: () => boardsApi.select(id).then(res => res.data),
enabled: !!id, // id 없으면 요청 안 함
})
return { board: data?.board ?? null, fileList: data?.fileList ?? [], isLoading, isError, refetch }
}
// useBoardMutations.js
export const useBoardMutations = (id) => {
const queryClient = useQueryClient()
const insertMutation = useMutation({
mutationFn: ({ data, headers }) => boardsApi.insert(data, headers),
onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ['boards'] }) // 목록 갱신
await successAlert('등록 성공', '게시글 등록이 완료되었습니다.')
navigate('/boards')
},
})
return {
insertBoard: (data, headers) => insertMutation.mutate({ data, headers }),
isInserting: insertMutation.isPending,
}
}
JavaScript
복사
빠른 참조
@tanstack/react-query
├─ QueryClient → 캐시 저장소, 전역 설정
├─ QueryClientProvider → Context로 앱 전체에 공급
├─ useQuery → 데이터 읽기 (GET)
│ ├─ queryKey → 캐시 식별자 (배열)
│ ├─ queryFn → 서버 요청 함수
│ ├─ enabled → 조건부 실행
│ └─ placeholderData → 이전 데이터 임시 표시
├─ useMutation → 데이터 쓰기 (POST/PUT/DELETE)
│ ├─ mutationFn → 서버 요청 함수
│ ├─ onSuccess → 성공 콜백
│ ├─ onError → 실패 콜백
│ └─ isPending → 요청 진행 중 여부
├─ useQueryClient → QueryClient 인스턴스 접근
│ ├─ invalidateQueries → 캐시 무효화 (가장 자주 사용)
│ ├─ setQueryData → 캐시 직접 수정
│ └─ getQueryData → 캐시 읽기
└─ ReactQueryDevtools → 개발용 캐시 상태 시각화
Plain Text
복사




