PROJECT/[ 장기게임 ]

react query란?????

Lim임 2025. 12. 3. 18:48

React Query 완벽 가이드

목차

  1. Query 기본 개념
  2. Query Key 이해하기
  3. Query 옵션 상세 설명
  4. useQuery 반환값
  5. Mutation 사용법
  6. 실전 예제

Query 기본 개념

useQuery의 구조

const result = useQuery({
  queryKey: ['키', '값'],      // 캐시 식별자
  queryFn: () => fetchData(),   // 데이터를 가져오는 함수
  // ... 옵션들
});

3가지 핵심 요소:

  1. queryKey: 데이터를 구분하는 고유 ID (배열 형태)
  2. queryFn: 실제 API를 호출하는 함수
  3. 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  // 갱신 안 함

시나리오:

  1. 사용자가 게임 목록 조회
  2. 다른 탭으로 이동 (5분 경과)
  3. 다시 돌아옴
  4. 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