React Query 도입 가이드
목차
React Query가 필요한 이유
현재 프로젝트의 API 호출 패턴
프로젝트에서 사용 중인 API:
- 조회(GET):
getBoard,getMoves,getGamesByPlayer - 생성(POST):
createGame,addMove,validateMove,getPossibleMoves - 수정(PUT):
endGame
React Query가 해결하는 문제
React Query는 서버 상태(Server State) 관리 라이브러리입니다.
서버 상태 vs 클라이언트 상태:
- 클라이언트 상태: UI 상태, 폼 입력값 등 (예:
activeModal,player1Name)- 현재
useState로 잘 관리 중 ✅
- 현재
- 서버 상태: 백엔드에서 가져온 데이터 (예: 게임 목록, 이동 기록)
- 현재 수동으로 관리 중 ⚠️
서버 상태의 특징:
- 비동기적: API 호출은 시간이 걸림
- 공유됨: 여러 컴포넌트에서 같은 데이터 필요
- 변경 가능: 다른 사용자나 시스템이 데이터를 바꿀 수 있음
- 캐싱 필요: 같은 데이터를 반복 요청하면 비효율적
현재 코드의 문제점
예시: HistorySelectContent.tsx
export const HistorySelectContent = () => {
const [playerName, setPlayerName] = useState('');
const [games, setGames] = useState<Game[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSearch = async () => {
const name = playerName.trim();
if (!name) {
setError('닉네임을 입력해주세요.');
return;
}
setError(null);
setLoading(true);
try {
const data = await getGamesByPlayer(name);
setGames(data);
} catch (e) {
setError('게임 목록을 불러오는 중 오류가 발생했습니다.');
} finally {
setLoading(false);
}
};
// ...
};
문제점 분석
1. 보일러플레이트 코드 과다
loading,error,data상태를 매번 수동으로 관리try-catch-finally블록 반복- 모든 API 호출마다 동일한 패턴 반복
2. 캐싱 없음
- 같은 플레이어 이름으로 다시 검색하면 또 API 호출
- 이미 받은 데이터를 재사용하지 못함
- 불필요한 네트워크 요청 → 느린 UX
3. 재시도 로직 없음
- 네트워크 오류 시 사용자가 수동으로 다시 클릭해야 함
- 일시적인 네트워크 문제도 영구적인 에러로 표시
4. 백그라운드 업데이트 없음
- 데이터가 오래되어도 자동으로 새로고침하지 않음
- 사용자가 탭을 전환했다 돌아와도 오래된 데이터 표시
5. 낙관적 업데이트(Optimistic Update) 불가
- 게임 종료 시 UI가 즉시 반영되지 않음
- API 응답을 기다려야만 UI 업데이트
React Query 도입 시 개선점
1. 코드 간소화
Before (현재):
const [games, setGames] = useState<Game[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSearch = async () => {
setError(null);
setLoading(true);
try {
const data = await getGamesByPlayer(name);
setGames(data);
} catch (e) {
setError('오류 발생');
} finally {
setLoading(false);
}
};
After (React Query):
const { data: games, isLoading, error, refetch } = useQuery({
queryKey: ['games', playerName],
queryFn: () => getGamesByPlayer(playerName),
enabled: !!playerName.trim(), // 이름이 있을 때만 실행
});
줄어든 코드:
- 15줄 → 5줄 (67% 감소)
- 상태 관리 로직 제거
- 에러 처리 자동화
2. 자동 캐싱
// 첫 번째 검색: "홍길동"
useQuery(['games', '홍길동'], () => getGamesByPlayer('홍길동'));
// → API 호출 ✅
// 두 번째 검색: "홍길동" (5초 후)
useQuery(['games', '홍길동'], () => getGamesByPlayer('홍길동'));
// → 캐시에서 즉시 반환 ⚡ (API 호출 X)
// 세 번째 검색: "이순신"
useQuery(['games', '이순신'], () => getGamesByPlayer('이순신'));
// → 새로운 키이므로 API 호출 ✅
장점:
- 네트워크 요청 감소 → 빠른 UX
- 서버 부하 감소
- 오프라인 환경에서도 캐시된 데이터 표시 가능
3. 자동 재시도
useQuery({
queryKey: ['games', playerName],
queryFn: () => getGamesByPlayer(playerName),
retry: 3, // 실패 시 3번 재시도
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// 1초 → 2초 → 4초 간격으로 재시도 (지수 백오프)
});
장점:
- 일시적인 네트워크 오류 자동 복구
- 사용자가 수동으로 재시도할 필요 없음
4. 백그라운드 자동 갱신
useQuery({
queryKey: ['games', playerName],
queryFn: () => getGamesByPlayer(playerName),
staleTime: 5 * 60 * 1000, // 5분간 신선한 데이터로 간주
refetchOnWindowFocus: true, // 탭 전환 시 자동 갱신
refetchInterval: 30000, // 30초마다 자동 갱신 (선택적)
});
시나리오:
- 사용자가 게임 목록 조회
- 다른 탭으로 이동
- 5분 후 다시 돌아옴
- React Query가 자동으로 최신 데이터 요청 ✅
5. 낙관적 업데이트
const mutation = useMutation({
mutationFn: (gameId) => endGame(gameId, 'cho'),
onMutate: async (gameId) => {
// API 호출 전에 UI 즉시 업데이트
await queryClient.cancelQueries(['games']);
const previousGames = queryClient.getQueryData(['games']);
queryClient.setQueryData(['games'], (old) =>
old.map((game) =>
game.id === gameId ? { ...game, winner: 'cho' } : game
)
);
return { previousGames }; // 롤백용
},
onError: (err, gameId, context) => {
// 실패 시 이전 상태로 롤백
queryClient.setQueryData(['games'], context.previousGames);
},
});
장점:
- 즉각적인 UI 반응 (체감 속도 향상)
- 실패 시 자동 롤백
설치 및 설정
1. 패키지 설치
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools --save-dev
2. QueryClient 설정
파일 생성: src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5분
cacheTime: 10 * 60 * 1000, // 10분
retry: 1, // 1번 재시도
refetchOnWindowFocus: true,
},
mutations: {
retry: 0, // mutation은 재시도 안 함
},
},
});
3. Provider 설정
파일 수정: src/App.tsx 또는 src/main.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { queryClient } from './lib/queryClient';
function App() {
return (
<QueryClientProvider client={queryClient}>
<GameProvider>
{/* 기존 앱 컴포넌트 */}
</GameProvider>
{/* 개발 환경에서만 표시되는 디버깅 도구 */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
적용 예시
예시 1: 게임 목록 조회 (Query)
Before:
// HistorySelectContent.tsx
const [games, setGames] = useState<Game[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSearch = async () => {
setError(null);
setLoading(true);
try {
const data = await getGamesByPlayer(playerName);
setGames(data);
} catch (e) {
setError('오류 발생');
} finally {
setLoading(false);
}
};
After:
// HistorySelectContent.tsx
import { useQuery } from '@tanstack/react-query';
const {
data: games = [],
isLoading,
error,
refetch,
} = useQuery({
queryKey: ['games', 'player', playerName],
queryFn: () => getGamesByPlayer(playerName),
enabled: !!playerName.trim(), // 이름이 있을 때만 실행
staleTime: 2 * 60 * 1000, // 2분간 캐시 유지
});
// 버튼 클릭 시
<Button onClick={() => refetch()}>
{isLoading ? '불러오는 중...' : '게임 목록 불러오기'}
</Button>
예시 2: 이동 기록 조회 (Query with Params)
새 파일: src/hooks/queries/useGameMoves.ts
import { useQuery } from '@tanstack/react-query';
import { getMoves } from '../../api/gameApi';
export const useGameMoves = (gameId: number | null) => {
return useQuery({
queryKey: ['moves', gameId],
queryFn: () => getMoves(gameId!),
enabled: !!gameId, // gameId가 있을 때만 실행
staleTime: 1 * 60 * 1000, // 1분
});
};
// 사용 예시
const { data: moves, isLoading } = useGameMoves(gameId);
예시 3: 게임 생성 (Mutation)
새 파일: src/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 }: { player1: string; player2: string }) =>
createGame(player1, player2),
onSuccess: (newGame) => {
// 게임 목록 캐시 무효화 (자동 재조회)
queryClient.invalidateQueries({ queryKey: ['games'] });
// 또는 직접 캐시 업데이트
queryClient.setQueryData(['games', 'player', newGame.player1], (old: any) =>
old ? [newGame, ...old] : [newGame]
);
},
});
};
// 사용 예시
const createGameMutation = useCreateGame();
const handleStart = () => {
createGameMutation.mutate(
{ player1: 'Alice', player2: 'Bob' },
{
onSuccess: (game) => {
console.log('게임 생성 성공:', game.id);
},
onError: (error) => {
alert('게임 생성 실패');
},
}
);
};
예시 4: 게임 종료 (Mutation with Optimistic Update)
새 파일: src/hooks/mutations/useEndGame.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { endGame } from '../../api/gameApi';
export const useEndGame = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ gameId, winner }: { gameId: number; winner: string }) =>
endGame(gameId, winner),
// 낙관적 업데이트
onMutate: async ({ gameId, winner }) => {
// 진행 중인 쿼리 취소
await queryClient.cancelQueries({ queryKey: ['games'] });
// 이전 데이터 백업
const previousGames = queryClient.getQueryData(['games']);
// 즉시 UI 업데이트
queryClient.setQueryData(['games'], (old: any) =>
old?.map((game: any) =>
game.id === gameId ? { ...game, winner, endedAt: new Date() } : game
)
);
return { previousGames };
},
// 에러 시 롤백
onError: (err, variables, context) => {
queryClient.setQueryData(['games'], context?.previousGames);
},
// 성공 시 서버 데이터로 동기화
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['games'] });
},
});
};
마이그레이션 가이드
단계별 적용 전략
Phase 1: 설정 및 간단한 Query부터 시작
- React Query 설치 및 Provider 설정
HistorySelectContent리팩토링 (가장 명확한 사례)- DevTools로 동작 확인
Phase 2: 나머지 Query 적용
useGameMoves훅 생성 (리플레이 기능)useBoard훅 생성 (보드 상태 조회)
Phase 3: Mutation 적용
useCreateGame훅 생성useAddMove훅 생성useEndGame훅 생성 (낙관적 업데이트 포함)
Phase 4: 고급 기능
- Prefetching (게임 목록 미리 불러오기)
- Infinite Query (무한 스크롤)
- Polling (실시간 업데이트)
우선순위
높음 (즉시 적용 권장):
- ✅
HistorySelectContent(게임 목록 조회) - ✅
useGameMoves(이동 기록 조회)
중간 (점진적 적용):
useCreateGame(게임 생성)useEndGame(게임 종료)
낮음 (선택적):
validateMove,getPossibleMoves(빠른 응답 필요, 캐싱 불필요)
요약
React Query를 사용해야 하는 이유
- 코드 간소화: 보일러플레이트 67% 감소
- 자동 캐싱: 불필요한 API 호출 제거
- 자동 재시도: 네트워크 오류 자동 복구
- 백그라운드 갱신: 항상 최신 데이터 유지
- 낙관적 업데이트: 즉각적인 UI 반응
현재 프로젝트에 적합한 이유
- ✅ 여러 API 호출 존재 (7개)
- ✅ 수동 상태 관리 중 (
loading,error,data) - ✅ 캐싱이 유익한 데이터 (게임 목록, 이동 기록)
- ✅ 실시간성이 중요한 기능 (리플레이, 게임 진행)
결론: React Query 도입 강력 권장! 🚀
'PROJECT > [ 장기게임 ]' 카테고리의 다른 글
| react query란????? (1) | 2025.12.03 |
|---|---|
| state와 context (0) | 2025.12.03 |
| App.tsx 줄이기 (0) | 2025.11.27 |
| 내 기록 (0) | 2025.11.26 |