Search

React Query

React Query (TanStack Query)

목차

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 - 데이터 요청 함수

queryFnPromise를 반환해야 한다.
// 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 - 캐시 직접 조작

useQueryClientQueryClient 인스턴스에 직접 접근하는 훅이다. 주로 뮤테이션 성공 후 캐시를 무효화하거나 직접 업데이트할 때 사용한다.
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
복사