React Query 캐싱은 왜 전역 상태 관리와 다르게 봐야 할까?

결론부터 쓰면, React Query(TanStack Query)는 "서버 상태(server state)"를 캐시하고 동기화하는 도구고, Zustand·Redux 같은 전역 스토어는 "클라이언트 상태"를 다루는 도구다. 이 둘을 하나로 섞어서 관리하면 무효화 로직을 손으로 다 짜야 하는 상황이 온다. 판단 기준은 "이 데이터의 진짜 출처가 서버인가"다.

  • React Query는 서버 상태 캐시, Zustand/Redux는 클라이언트 상태, URL은 공유 가능한 화면 상태, local state는 컴포넌트 내부 일시 상태에 맞는다.
  • 기본값은 staleTime = 0, gcTime = 5분이라 캐시가 있어도 대부분 마운트 시 백그라운드 재요청이 걸린다.
  • 같은 쿼리 키를 쓰는 컴포넌트는 하나의 캐시 항목과 in-flight 요청을 공유해 중복 요청이 줄어든다.
  • theme, 사이드바 열림 상태, 모달 표시 여부처럼 서버가 소유하지 않는 값은 애초에 React Query의 대상이 아니다.

판교에서 진행한 프로젝트 중 하나는 상품 목록을 useEffect+useState로 직접 fetch하고 있었다. 필터가 바뀔 때마다 새 요청을 걸고, 목록 컴포넌트와 필터 요약 컴포넌트가 각자 같은 API를 따로 호출했다. 캐시가 없으니 화면 전환마다 로딩 스피너가 다시 돌았고, 이 지점이 React Query 도입의 실질적 계기였다.

캐시가 있는데도 마운트할 때마다 왜 다시 요청이 나갈까?

기본값이 staleTime = 0이기 때문이다. 캐시된 데이터가 있으면 일단 화면에 즉시 보여주고, 동시에 백그라운드에서 서버에 다시 확인하러 간다. 이 흐름은 "빠르게 보여주되 최신성은 놓치지 않는다"는 절충이지, 캐시 미스가 아니다. 자주 보지만 잘 안 바뀌는 데이터(예: 카테고리 목록)라면 staleTime을 수 분 단위로 늘려서 즉시 재사용 구간을 넓히는 편이 실용적이다. gcTime은 별개로, 아무도 구독하지 않는 캐시가 메모리에서 얼마나 오래 살아있는지를 정하는 값이라 기본 5분이 지나면 정리된다(TanStack Query 공식 문서).

같은 데이터를 여러 컴포넌트가 부르면 캐시는 어떻게 나눠 쓸까?

쿼리 키가 같으면 캐시 항목과 in-flight 요청을 그대로 공유한다. 목록 컴포넌트와 요약 컴포넌트가 같은 ['products', filters] 키로 데이터를 요청하면, 실제 네트워크 요청은 한 번만 나가고 두 컴포넌트는 같은 캐시 결과를 구독한다. 앞서 언급한 프로젝트에서 중복 호출이 사라진 것도 이 구조 덕분이었다. 반대로 쿼리 키를 세밀하게 안 나누면 필요 이상으로 캐시가 공유돼 원치 않는 재사용이 생길 수 있으니, 키 설계 자체가 캐싱 전략의 절반이다(캐싱 가이드).

무효화는 언제 자동으로 일어날까?

invalidateQueries를 호출하거나 refetchOnWindowFocus, refetchOnReconnect 같은 옵션이 걸린 이벤트가 발생할 때 일어난다. 이건 전역 캐시 콜백 기반으로 동작하기 때문에, useEffect로 데이터를 붙여놓고 수동으로 다시 부르는 방식과는 구조가 다르다. 실무에서 재요청이 과도하다고 느껴지면 retry 횟수와 refetchOnWindowFocus부터 점검하는 게 순서다. 다만 이 옵션들을 끄면 최신성이 떨어질 수 있으니, "얼마나 빨리 반영돼야 하는 데이터인가"를 먼저 정해야 한다.

React Query를 전역 상태 관리 라이브러리로 써도 될까?

여기서 흔한 오해가 생긴다. React Query가 캐시와 구독을 제공하니 Redux나 Zustand를 아예 대체할 수 있다고 보는 시각이다. 실제로는 다르다. React Query는 서버가 소유한 데이터의 동기화 문제를 풀도록 설계됐고, 아직 제출하지 않은 폼 값이나 모달 열림 상태처럼 서버에 대응 값이 없는 UI 상태는 애초에 캐시할 대상이 없다. 서버 상태 vs 클라이언트 상태를 나누는 기준은 여러 2026년 실무 가이드에서도 공통으로 언급된다.

실제 프로젝트에서는 데이터 종류를 어떻게 나눠 쓰나?

데이터 종류 예시 적합한 도구
서버 소유 데이터 상품 목록, 사용자 프로필, 주문 내역, 댓글 React Query
공유·재현 필요 상태 필터, 탭, 페이지네이션 URL state
여러 컴포넌트가 쓰는 UI 상태 사이드바, 테마, 전역 알림 전역 스토어(Zustand/Redux)
컴포넌트 내부 일시 상태 입력 중인 폼, 토글 local state

부적합한 예도 명확하다. 아직 제출 안 한 폼 초안을 React Query 캐시에 넣으면 서버 재검증 타이밍과 사용자 입력이 충돌한다. 반대로 이메일 중복 확인처럼 서버가 답을 갖고 있는 값을 local state로 관리하면 다른 화면과 동기화가 안 맞는 문제가 생긴다. 참고로 주간 다운로드 수 비교(TanStack Query 약 500만~640만, Zustand 약 400만)나 100개 쿼리 기준 메모리 사용량(SWR 약 3.8MB, React Query 약 6.1MB) 같은 수치는 2차 출처나 단일 블로그의 샘플값이라 일반화보다는 참고용으로만 보는 게 맞다.

핵심 정리

  • React Query는 서버 상태 캐시 도구, 전역 스토어는 클라이언트 상태 도구로 역할이 분리된다.
  • 기본값 staleTime=0, gcTime=5분은 즉시 반환 후 백그라운드 재요청 구조를 만든다.
  • 같은 쿼리 키를 쓰면 캐시와 in-flight 요청이 공유돼 중복 호출이 줄어든다.
  • 판단 기준은 서버 소유 여부, 컴포넌트 간 공유 필요성, 재동기화 필요성, 새로고침 후 유지 필요성 네 가지다.
  • 다운로드 수·메모리 비교 수치는 2차 출처·샘플 블로그 값이므로 절대 기준으로 삼지 않는 편이 안전하다.