TanStack Query로 서버 상태 다루기: 캐시·재시도·에러 처리

서버 데이터를 전역 상태처럼 다루다 생기는 문제를 정리하는 실무 기준

TanStack Query 상태관리 썸네일

핵심 요약

  1. 서버 상태는 원본이 서버에 있으므로 클라이언트 상태와 분리해 전용 도구로 관리한다.
  2. 쿼리 키는 캐시 주소이므로 필터·정렬·페이지를 포함해 정확히 설계한다.
  3. 상세는 응답 값으로 즉시 갱신하고 목록 같은 범위는 무효화해 서버 기준으로 다시 받는다.
  4. 재시도는 네트워크성 오류에만 제한하고 4xx는 재시도하지 않는다.

서버에서 가져온 목록을 Redux나 useState에 넣어 두고, 새로고침·중복 요청·오래된 값 문제를 직접 처리하기 시작하면 코드가 금세 지저분해진다. 이 데이터는 내가 만든 상태가 아니라 서버가 가진 상태의 복사본이기 때문이다. TanStack Query는 이런 "서버 상태"를 캐시와 함께 관리해 주는 도구다. 이 글은 쿼리 키 설계부터 재시도·에러 처리까지 실무에서 자주 부딪히는 지점을 정리했다.

서버 상태와 클라이언트 상태를 구분하기

서버 상태는 원본이 서버에 있고, 언제든 다른 사용자에 의해 바뀌며, 비동기로 가져오고, 오래될 수 있다. 클라이언트 상태는 UI 토글, 입력값, 모달 열림처럼 내 앱 안에서만 의미가 있는 값이다. 이 둘을 같은 저장소에 넣으면 캐시 무효화와 동기화를 직접 구현해야 한다. 원칙은 단순하다.

구분예시관리 도구
서버 상태목록·상세·검색 결과TanStack Query
클라이언트 상태필터 UI·모달·입력값useState 또는 경량 스토어
URL 상태페이지·정렬·검색어라우터 쿼리 파라미터

쿼리 키 설계와 캐시 범위

TanStack Query 공식 문서에서 강조하듯 queryKey는 캐시의 고유 주소다. 키가 다르면 다른 캐시, 키가 같으면 재사용이다. 키를 배열로 두고 가장 일반적인 값에서 구체적인 값으로 이어 가면 무효화 범위를 다루기 쉽다.

목록·상세·필터 조합의 키 규칙

// 목록: 필터·정렬·페이지를 모두 키에 포함
useQuery({ queryKey: ["orders", { status, sort, page }], queryFn: fetchOrders });

// 상세: id를 마지막에
useQuery({ queryKey: ["orders", id], queryFn: () => fetchOrder(id) });

필터 값을 키에 넣지 않으면 필터를 바꿔도 같은 캐시를 보게 된다. 반대로 화면에 영향 없는 값을 키에 넣으면 불필요한 재요청이 늘어난다.

무효화와 낙관적 갱신

const qc = useQueryClient();

const mutation = useMutation({
  mutationFn: updateOrder,
  onSuccess: (updated) => {
    qc.setQueryData(["orders", updated.id], updated);
    qc.invalidateQueries({ queryKey: ["orders"] });
  }
});

상세는 응답 값으로 즉시 갱신하고 목록은 무효화해 서버 기준으로 다시 받는다. 낙관적 갱신을 쓸 때는 onMutate에서 이전 값을 저장하고 onError에서 되돌린다.

재시도·폴링·에러 처리 정책

  • 재시도는 네트워크성 오류에만, 횟수와 백오프를 제한한다 (4xx는 재시도하지 않는다)
  • 폴링은 화면이 보일 때만, refetchInterval과 포커스 옵션을 함께 조정한다
  • 전역 기본값을 정하고 쿼리별로 예외를 둔다

에러 경계와 로딩 UX 연결

로딩·에러를 화면마다 if로 처리하면 빠뜨리기 쉽다. 라우트 단위 에러 경계와 Suspense를 함께 쓰면 실패와 대기를 선언적으로 다룰 수 있다. 스켈레톤은 목록 구조와 비슷하게 만들어 레이아웃 이동을 줄인다.

흔한 안티패턴과 점검 기준

  1. 서버 응답을 useEffect로 클라이언트 상태에 복사한다 — 원본이 둘이 된다
  2. 모든 데이터를 하나의 거대한 키로 캐시한다 — 부분 무효화가 불가능하다
  3. 에러를 콘솔로만 남기고 사용자에게 알리지 않는다
  4. 폴링을 무제한으로 돌려 트래픽을 낭비한다

전역 상태가 정말 필요한지 판단하는 기준은 2026 프론트엔드 상태관리 선택에서, 서버 컴포넌트 캐시와 태그 무효화 경계는 Next.js 16 캐싱 정리에서 이어진다.

초기 설정과 전역 기본값

쿼리마다 옵션을 반복해 적으면 값이 조금씩 달라지고, 나중에 정책을 바꿀 때 모든 화면을 고쳐야 한다. 클라이언트 생성 시 전역 기본값을 정하고, 예외가 필요한 쿼리에서만 옵션을 덮어쓰는 구조가 유지보수에 유리하다.

옵션기본값 예시
staleTime데이터 성격별로 짧게/길게 나눔
retry횟수 제한, 4xx는 제외
refetchOnWindowFocus민감한 데이터는 끔
gcTime메모리와 재방문 빈도로 조정

전역 기본값은 팀 문서에 남기고, 왜 그 값인지 한 줄씩 이유를 적는다. 숫자만 적어 두면 다음 사람이 근거를 모른 채 바꾸고, 그 변경이 다른 화면에 영향을 준다.

개발 환경에서는 캐시 상태를 눈으로 확인할 수 있는 개발 도구를 함께 쓴다. 어떤 키가 살아 있고 언제 무효화됐는지 보이면, 화면이 예전 값을 보여줄 때 서버 문제인지 캐시 문제인지 빠르게 구분할 수 있다. 운영에서는 민감한 키가 로그·화면에 노출되지 않도록 주의한다.

권한과 캐시의 관계

사용자 권한이 바뀌면 캐시된 데이터도 더 이상 유효하지 않을 수 있다. 로그아웃·권한 변경·조직 전환 같은 시점에 캐시를 비우거나 무효화하지 않으면, 이전 권한으로 받은 데이터가 화면에 남을 수 있다. 민감한 데이터를 다루는 화면은 세션 변경을 캐시 초기화 신호로 연결한다.

또한 오류 응답을 데이터처럼 캐시하지 않도록 주의한다. 일시적 장애로 받은 오류가 캐시되면, 서버가 복구된 뒤에도 화면은 실패 상태로 남는다. 실패는 기본적으로 캐시하지 않고 재시도로 회복하게 둔다.

서버 상태 도구는 데이터를 "가져오는" 일뿐 아니라 "언제 다시 가져올지"를 관리하는 도구다. 이 두 가지를 분리해 생각하면 캐시 전략과 무효화 지점을 훨씬 명확하게 정할 수 있다.

초보자가 자주 실수하는 포인트

  • 서버 응답을 useEffect로 클라이언트 상태에 복사해 원본을 둘로 만든다.
  • 필터 값을 쿼리 키에 넣지 않아 필터 변경이 반영되지 않는다.
  • 4xx 오류까지 재시도해 실패 요청을 반복한다.
  • 에러를 콘솔에만 남기고 사용자에게 표시하지 않는다.

체크리스트

  • 서버 상태와 클라이언트 상태를 구분했다.
  • 목록·상세·필터 조합의 쿼리 키 규칙을 문서화했다.
  • 변경 시 무효화할 키 범위를 정했다.
  • 재시도·폴링 정책을 전역 기본값과 예외로 나눴다.
  • 에러 경계와 로딩 스켈레톤을 화면에 연결했다.

자주 묻는 질문

Redux를 완전히 대체할 수 있나?

서버 상태는 대체할 수 있다. 다만 복잡한 클라이언트 전역 상태가 남아 있으면 경량 스토어나 Redux를 함께 쓰는 것이 일반적이다.

캐시 시간은 어떻게 정하나?

데이터 변경 빈도와 정확도 요구로 정한다. 자주 바뀌는 값은 staleTime을 짧게, 거의 안 바뀌는 값은 길게 두고, 쓰기 경로에서 무효화하는 방식을 병행한다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.