React Query 완벽 가이드
목차
Query 기본 개념
useQuery의 구조
const result = useQuery({
queryKey: ['키', '값'], // 캐시 식별자
queryFn: () => fetchData(), // 데이터를 가져오는 함수
// ... 옵션들
});
3가지 핵심 요소:
- queryKey: 데이터를 구분하는 고유 ID (배열 형태)
- queryFn: 실제 API를 호출하는 함수
- options: 캐싱, 재시도 등의 동작 설정
Query Key 이해하기
Query Key란?
캐시의 주소입니다. 같은 queryKey를 사용하면 같은 캐시를 공유합니다.
기본 형태
// 1. 단순 문자열 배열
queryKey: ['games']
// 2. 문자열 + 변수
queryKey: ['games', playerName]
// 3. 계층적 구조 (권장)
queryKey: ['games', 'player', playerName]
queryKey: ['games', 'detail', gameId]
Query Key 규칙
1. 배열 형태로 작성
// ✅ 올바른 예
queryKey: ['games']
queryKey: ['games', 'player', 'Alice']
// ❌ 잘못된 예
queryKey: 'games' // 문자열 단독 사용 불가
2. 순서가 중요
// 다른 키로 인식됨
queryKey: ['games', 'player', 'Alice']
queryKey: ['player', 'games', 'Alice'] // 완전히 다른 캐시!
3. 값이 바뀌면 새로운 쿼리
// playerName이 'Alice'일 때
queryKey: ['games', 'player', 'Alice'] // 쿼리 1
// playerName이 'Bob'으로 바뀌면
queryKey: ['games', 'player', 'Bob'] // 쿼리 2 (새로운 API 호출)
Query Key 계층 구조 (권장)
// 레벨 1: 리소스 타입
['games'] // 모든 게임
['users'] // 모든 사용자
// 레벨 2: 카테고리
['games', 'list'] // 게임 목록
['games', 'detail'] // 게임 상세
// 레벨 3: 식별자
['games', 'player', 'Alice'] // Alice의 게임들
['games', 'detail', 123] // ID 123인 게임
장점:
- 관련된 쿼리를 한 번에 무효화 가능
- 코드 가독성 향상
- 디버깅 용이
Query Key 무효화 예시
// 특정 플레이어의 게임만 무효화
queryClient.invalidateQueries(['games', 'player', 'Alice']);
// 모든 게임 목록 무효화
queryClient.invalidateQueries(['games', 'player']);
// 게임 관련 모든 쿼리 무효화
queryClient.invalidateQueries(['games']);
Query 옵션 상세 설명
1. staleTime (중요! ⭐)
의미: 데이터가 "신선한" 상태로 간주되는 시간
staleTime: 5 * 60 * 1000 // 5분 = 300,000ms
동작 방식:
- 신선한 데이터 (Fresh): API 호출 없이 캐시에서 즉시 반환
- 오래된 데이터 (Stale): 백그라운드에서 자동으로 재요청
예시:
// staleTime: 5분
useQuery({
queryKey: ['games', 'Alice'],
queryFn: fetchGames,
staleTime: 5 * 60 * 1000,
});
// 시간 흐름:
// 0분: API 호출 ✅ (첫 요청)
// 1분: 캐시 반환 ⚡ (신선함)
// 4분: 캐시 반환 ⚡ (신선함)
// 6분: 캐시 반환 + 백그라운드 재요청 🔄 (오래됨)
권장 값:
- 자주 바뀌는 데이터:
1 * 60 * 1000(1분) - 보통 데이터:
5 * 60 * 1000(5분) - 거의 안 바뀌는 데이터:
Infinity(영구)
2. gcTime (구 cacheTime)
의미: 캐시를 메모리에 보관하는 시간
gcTime: 10 * 60 * 1000 // 10분
동작 방식:
- 쿼리가 사용되지 않는 상태가 되면 타이머 시작
- gcTime이 지나면 메모리에서 완전히 삭제
staleTime vs gcTime 차이:
staleTime: 5분 // 데이터가 "오래됨"으로 표시되는 시간
gcTime: 10분 // 메모리에서 삭제되는 시간
// 시간 흐름:
// 0분: 데이터 로드 ✅
// 5분: 오래된 데이터로 표시 (하지만 캐시는 유지)
// 10분: 메모리에서 삭제 🗑️
권장 값:
- 일반적으로
staleTime보다 길게 설정 - 기본값:
5 * 60 * 1000(5분)
3. enabled
의미: 쿼리 실행 조건
enabled: !!playerName.trim() // playerName이 있을 때만 실행
사용 시나리오:
1) 조건부 실행
const { data } = useQuery({
queryKey: ['games', playerName],
queryFn: () => fetchGames(playerName),
enabled: !!playerName, // playerName이 있을 때만
});
2) 의존성 쿼리
// 먼저 사용자 정보를 가져오고
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
// 사용자 ID가 있을 때만 게임 정보 가져오기
const { data: games } = useQuery({
queryKey: ['games', user?.id],
queryFn: () => fetchGames(user.id),
enabled: !!user?.id, // user.id가 있을 때만
});
4. retry
의미: 실패 시 재시도 횟수
retry: 1 // 1번 재시도
retry: 3 // 3번 재시도
retry: false // 재시도 안 함
고급 사용:
retry: (failureCount, error) => {
// 404 에러는 재시도 안 함
if (error.response?.status === 404) return false;
// 최대 3번까지만
return failureCount < 3;
}
5. retryDelay
의미: 재시도 간격
retryDelay: 1000 // 1초 후 재시도
// 지수 백오프 (권장)
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
// 1초 → 2초 → 4초 → 8초 → ... → 최대 30초
6. refetchOnWindowFocus
의미: 탭 전환 시 자동 갱신
refetchOnWindowFocus: true // 탭 돌아올 때 갱신 (기본값)
refetchOnWindowFocus: false // 갱신 안 함
시나리오:
- 사용자가 게임 목록 조회
- 다른 탭으로 이동 (5분 경과)
- 다시 돌아옴
refetchOnWindowFocus: true면 자동으로 최신 데이터 요청 ✅
7. refetchInterval
의미: 주기적 자동 갱신
refetchInterval: 30000 // 30초마다 자동 갱신
refetchInterval: false // 자동 갱신 안 함 (기본값)
사용 예시:
// 실시간 점수판
const { data: score } = useQuery({
queryKey: ['score'],
queryFn: fetchScore,
refetchInterval: 5000, // 5초마다 갱신
});
8. select
의미: 데이터 변환
select: (data) => data.filter(game => game.winner) // 승부가 난 게임만
예시:
const { data: finishedGames } = useQuery({
queryKey: ['games', 'Alice'],
queryFn: () => fetchGames('Alice'),
select: (data) => data.filter(game => game.endedAt),
});
useQuery 반환값
주요 속성
const {
data, // 성공 시 데이터
error, // 에러 객체
isLoading, // 첫 로딩 중
isFetching, // 백그라운드 로딩 중
isSuccess, // 성공 여부
isError, // 에러 여부
refetch, // 수동 재요청 함수
status, // 'loading' | 'error' | 'success'
} = useQuery(...);
isLoading vs isFetching
// 첫 로딩
isLoading: true // 데이터가 없고 로딩 중
isFetching: true // API 호출 중
// 백그라운드 갱신
isLoading: false // 이미 데이터가 있음
isFetching: true // 백그라운드에서 새 데이터 가져오는 중
사용 예시:
if (isLoading) return <Spinner />; // 첫 로딩 스피너
if (isError) return <Error />;
return (
<>
{isFetching && <Badge>갱신 중...</Badge>} // 백그라운드 표시
<GameList games={data} />
</>
);
Mutation 사용법
기본 구조
const mutation = useMutation({
mutationFn: (newGame) => createGame(newGame),
onSuccess: (data) => {
// 성공 시 실행
},
onError: (error) => {
// 실패 시 실행
},
});
// 사용
mutation.mutate({ player1: 'Alice', player2: 'Bob' });
주요 옵션
1. onSuccess - 성공 시 처리
const mutation = useMutation({
mutationFn: createGame,
onSuccess: (data, variables, context) => {
// data: 서버 응답
// variables: mutate에 전달한 인자
// context: onMutate에서 반환한 값
// 캐시 무효화 (재조회)
queryClient.invalidateQueries(['games']);
// 또는 직접 캐시 업데이트
queryClient.setQueryData(['games'], (old) => [...old, data]);
},
});
2. onMutate - 낙관적 업데이트
const mutation = useMutation({
mutationFn: endGame,
onMutate: async (gameId) => {
// 진행 중인 쿼리 취소
await queryClient.cancelQueries(['games']);
// 이전 데이터 백업
const previousGames = queryClient.getQueryData(['games']);
// 즉시 UI 업데이트 (낙관적)
queryClient.setQueryData(['games'], (old) =>
old.map(game =>
game.id === gameId ? { ...game, ended: true } : game
)
);
// 롤백용 데이터 반환
return { previousGames };
},
onError: (err, variables, context) => {
// 실패 시 롤백
queryClient.setQueryData(['games'], context.previousGames);
},
onSettled: () => {
// 성공/실패 관계없이 실행
queryClient.invalidateQueries(['games']);
},
});
Mutation 반환값
const {
mutate, // 비동기 함수 (await 불가)
mutateAsync, // 비동기 함수 (await 가능)
isLoading, // 로딩 중
isSuccess, // 성공 여부
isError, // 에러 여부
data, // 성공 시 데이터
error, // 에러 객체
reset, // 상태 초기화
} = useMutation(...);
실전 예제
예제 1: 게임 목록 조회
// hooks/queries/usePlayerGames.ts
import { useQuery } from '@tanstack/react-query';
import { getGamesByPlayer } from '../../api/gameApi';
export const usePlayerGames = (playerName: string) => {
return useQuery({
queryKey: ['games', 'player', playerName],
queryFn: () => getGamesByPlayer(playerName),
enabled: !!playerName.trim(),
staleTime: 2 * 60 * 1000, // 2분
gcTime: 5 * 60 * 1000, // 5분
retry: 1,
});
};
// 컴포넌트에서 사용
const { data: games, isLoading, error } = usePlayerGames('Alice');
예제 2: 게임 생성 (Mutation)
// hooks/mutations/useCreateGame.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createGame } from '../../api/gameApi';
export const useCreateGame = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ player1, player2 }) => createGame(player1, player2),
onSuccess: (newGame) => {
// 게임 목록 캐시 무효화
queryClient.invalidateQueries(['games']);
// 또는 직접 추가
queryClient.setQueryData(['games', 'player', newGame.player1], (old) =>
old ? [newGame, ...old] : [newGame]
);
},
});
};
// 컴포넌트에서 사용
const createGameMutation = useCreateGame();
const handleCreate = () => {
createGameMutation.mutate(
{ player1: 'Alice', player2: 'Bob' },
{
onSuccess: (game) => {
console.log('게임 생성 성공:', game.id);
},
onError: (error) => {
alert('게임 생성 실패');
},
}
);
};
예제 3: 의존성 쿼리
// 1단계: 게임 ID 가져오기
const { data: gameId } = useQuery({
queryKey: ['currentGame'],
queryFn: getCurrentGameId,
});
// 2단계: 게임 ID가 있을 때만 이동 기록 가져오기
const { data: moves } = useQuery({
queryKey: ['moves', gameId],
queryFn: () => getMoves(gameId),
enabled: !!gameId, // gameId가 있을 때만 실행
});
예제 4: 실시간 업데이트
// 실시간 점수판
const { data: score } = useQuery({
queryKey: ['score', gameId],
queryFn: () => getScore(gameId),
refetchInterval: 3000, // 3초마다 갱신
refetchIntervalInBackground: true, // 백그라운드에서도 갱신
});
요약 치트시트
Query 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
staleTime |
0 | 데이터가 신선한 시간 |
gcTime |
5분 | 캐시 보관 시간 |
enabled |
true | 쿼리 실행 조건 |
retry |
3 | 재시도 횟수 |
retryDelay |
지수 백오프 | 재시도 간격 |
refetchOnWindowFocus |
true | 탭 전환 시 갱신 |
refetchInterval |
false | 주기적 갱신 |
자주 쓰는 패턴
// 1. 기본 조회
useQuery({
queryKey: ['resource', id],
queryFn: () => fetch(id),
});
// 2. 조건부 조회
useQuery({
queryKey: ['resource', id],
queryFn: () => fetch(id),
enabled: !!id,
});
// 3. 캐싱 최적화
useQuery({
queryKey: ['resource', id],
queryFn: () => fetch(id),
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
});
// 4. 실시간 업데이트
useQuery({
queryKey: ['resource', id],
queryFn: () => fetch(id),
refetchInterval: 5000,
});
디버깅 팁
React Query Devtools 사용
// App.tsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
기능:
- 모든 쿼리 상태 확인
- 캐시 데이터 확인
- 수동으로 쿼리 재실행
- 쿼리 무효화 테스트
이 문서를 참고하시면 React Query를 완벽하게 사용하실 수 있습니다! 🚀
'PROJECT > [ 장기게임 ]' 카테고리의 다른 글
| react query를 도입해보자 (0) | 2025.12.03 |
|---|---|
| state와 context (0) | 2025.12.03 |
| App.tsx 줄이기 (0) | 2025.11.27 |
| 내 기록 (0) | 2025.11.26 |