전역 상태, 뭘 기준으로 갈라야 할까?
결론부터 적으면, 전역 상태 도구를 고르는 첫 질문은 "어디서 온 데이터인가"다. API·URL·서버 DB에서 온 데이터면 React Query 계열, 앱 안에서만 도는 UI 상태(모달 열림, 테마, 폼 입력)면 로컬 상태나 Context, 그 중간에 여러 화면이 공유해야 하는 순수 클라이언트 상태면 Zustand·Redux 같은 전역 스토어다. 이 구분은 TanStack Query 공식 문서와 React 공식 문서에서 반복해서 강조하는 원칙이다TanStack Query 공식 가이드.
- 데이터 출처(서버 vs 클라이언트)가 도구 선택의 1차 기준이다.
- 동기화·재검증이 필요하면 React Query, 즉시 반응이 필요하면 로컬 상태다.
- 서버 상태를 Redux·Zustand에 억지로 넣는 건 가능하지만 캐시 무효화·재요청 로직을 직접 다 짜야 해서 비효율적일 수 있다.
- SSR 환경에서는 QueryClient를 요청 단위로 새로 만들어야 사용자 간 데이터 오염을 막는다.
- 2026년 기준으로도 "로컬은 useState, 공유는 Context, 복잡한 클라이언트 전역은 Zustand/Redux, 서버 데이터는 React Query"라는 분리가 여전히 실무 기본값이다.
실무에서 처음 부딪히는 판단 지점은 신규 기능을 붙일 때 "이 상태를 스토어에 넣을까 쿼리 훅으로 뺄까"다. 나는 이 판단을 '네트워크 요청이 이 상태의 진실 공급원인가'로 먼저 거른다. 진실 공급원이 서버면 무조건 React Query 후보고, 아니면 그다음 질문(컴포넌트 트리 몇 단계까지 공유되는가)으로 넘어간다.
데이터가 서버에서 오는가, 앱 안에서만 도는가?
이 축이 갈리는 순간 도구가 거의 자동으로 정해진다. TanStack Query는 비동기 서버 데이터를 캐시·동기화·갱신하는 라이브러리로 설계됐고, 로컬 UI 상태나 폼 입력 같은 클라이언트 전용 상태를 대체하는 도구로 설명되지 않는다TanStack Query 문서. 실제로 게시글 목록, 사용자 프로필, 검색 결과처럼 "서버가 원본이고 클라이언트는 사본을 보여주는" 데이터는 캐시 만료(staleTime)와 재검증(refetch) 개념이 필요한데, 이 로직을 Redux reducer로 직접 짜면 로딩·에러·재시도 상태 관리 코드가 쿼리 훅 하나 쓰는 것보다 훨씬 길어진다.
반대로 모달 열림 여부, 사이드바 접힘 상태, 다크모드 토글, 인증 토큰처럼 서버에 물어볼 필요가 없는 상태는 React Query에 넣을 이유가 없다. 2025년 이후 정리된 실무 가이드들도 폼 입력이나 UI 토글에는 useState/useReducer를 쓰라고 반복해서 못박는다. 나는 이 기준으로 팀 코드리뷰에서 "이건 쿼리로 빼자" "이건 그냥 로컬로 두자"를 구분하는데, 첫 축만 명확해도 이후 논쟁의 절반이 줄어든다.
리렌더링 비용과 유지보수, 어디서 갈리나?
두 번째 축은 "이 상태를 몇 개 컴포넌트가 구독하는가"와 "얼마나 자주 바뀌는가"다. Context는 하위 트리에 데이터를 공유하는 React 공식 방법이지만, 값이 바뀌면 그 Context를 구독하는 모든 컴포넌트가 리렌더된다는 특성이 그대로 남는다React 공식 문서. 그래서 Context+useReducer 패턴에서는 state Context와 dispatch Context를 분리하는 팁이 널리 쓰인다. dispatch만 쓰는 소비자는 상태값이 바뀌어도 리렌더되지 않기 때문이다.
여기서 트레이드오프가 생긴다. Context 분리는 코드가 늘어나는 대가로 불필요한 리렌더를 줄이고, Zustand 같은 스토어는 선택자(selector) 기반 구독으로 이 문제를 라이브러리 차원에서 흡수한다. 나는 구독 컴포넌트가 5개 이하이고 값이 자주 안 바뀌면 Context로 시작하고, 알림 배지 카운트처럼 초 단위로 바뀌는 값이 트리 전역에 퍼져 있으면 스토어로 넘긴다. 이 판단을 미리 안 하면 나중에 "왜 이 페이지 전체가 매번 다시 그려지지"를 디버깅하는 데 반나절씩 쓰게 된다.
SSR 환경에서 QueryClient를 어떻게 다루나?
이 축은 서버 렌더링을 쓰는 프로젝트에서만 발생하지만 실무에서 자주 사고로 이어진다. TanStack Query 공식 SSR 문서는 서버 요청마다 QueryClient를 새로 만들라고 권고하는데, 이유는 사용자·요청 간 데이터 공유를 막기 위해서다TanStack Query SSR 가이드. QueryClient를 모듈 스코프에 싱글턴으로 만들어두면 서버에서 요청 A의 캐시가 요청 B로 새어 들어가는 상황이 생길 수 있다는 뜻이다.
또 하나 중요한 조건은 hydration이다. 서버와 클라이언트가 같은 dehydrated state를 써야 마크업이 일치한다고 문서가 강조한다. 서버에서 미리 가져오지 않은 쿼리는 클라이언트에서 인터랙션 이후에 가져올 수 있는데, 이건 페이지 하단 콘텐츠나 상호작용 뒤에만 보이는 콘텐츠에 적합한 패턴이지, 초기 렌더에 꼭 필요한 데이터에 쓰면 깜빡임(flash)이 생길 수 있다. 나는 이 부분에서 "이 데이터가 최초 페인트에 보여야 하는가"를 먼저 정하고 나서야 prefetch 범위를 정한다.
공식 문서와 실무 가이드는 실제로 뭐라고 선을 그었나?
근거를 정리하면 이렇다. TanStack Query 공식 문서는 React Query가 서버 상태 도구이고 Redux·Zustand는 클라이언트 상태 도구라는 구분을 반복적으로 명시한다. React 공식 문서는 Context를 "props drilling을 피하면서 하위 트리에 데이터를 공유하는 방법"으로 설명할 뿐, 서버 캐시 동기화 도구로 소개하지 않는다. 이 두 문서만 봐도 "전역 상태 = 하나의 도구"라는 전제가 애초에 틀렸다는 걸 알 수 있다.
버전 이슈도 짚을 필요가 있다. React Query v5 계열로 넘어오면서 cacheTime이 gcTime으로 이름이 바뀌었고 isLoading의 의미도 조정됐다. v4 기준 코드를 그대로 v5 문서 설명과 매칭하면 로딩 플래그 해석이 어긋날 수 있으니, 마이그레이션 시에는 버전 노트를 반드시 확인하는 게 안전하다. 벤치마크 수치 자체는 이번 정리에서 재현 가능한 원자료가 제한적이었지만, 요청 단위 QueryClient 생성과 dehydration 일치라는 조건은 공식 문서 수준에서 명확하게 확인된다.
이 구분에서 가장 흔히 놓치는 게 뭘까?
가장 흔한 함정은 "서버에서 받은 데이터를 전역 스토어에 캐싱까지 직접 구현하는 것"이다. Redux나 Zustand로 API 응답을 받아 저장하는 것 자체는 가능하지만, staleTime 관리·백그라운드 재검증·요청 중복 제거(dedup)·에러 재시도를 전부 손으로 짜야 한다. 이 코드는 처음엔 단순해 보여도 화면이 늘어날수록 캐시 무효화 조건이 꼬이기 쉽고, 결국 React Query가 기본 제공하는 기능을 재발명하는 셈이 된다.
반대 방향의 함정도 있다. 대규모 동기식 클라이언트 전용 상태(예: 여러 단계의 폼 위저드, 실시간 협업 에디터의 커서 위치)까지 전부 React Query 쿼리로 우겨넣으려는 시도다. 이런 상태는 서버에 물어볼 필요가 없는데도 쿼리 캐시에 넣으면 staleTime·refetch 옵션을 억지로 조정해야 하는 부자연스러운 코드가 나온다. 결국 "이 값의 원본이 서버인가 클라이언트인가"라는 첫 질문을 건너뛰면 두 방향 모두에서 같은 종류의 과잉 설계가 반복된다.
그래서 지금 이 프로젝트엔 뭘 고르나?
정리하면 판단 순서는 이렇다. 첫째, 데이터가 서버·API·URL에서 오고 재검증이 필요하면 React Query 계열을 기본값으로 둔다. 둘째, 앱 내부에서만 도는 상태고 구독 컴포넌트가 적으면 로컬 상태(useState/useReducer)로 충분하다. 셋째, 여러 화면이 공유하지만 서버에 물어볼 필요는 없는 상태(테마, 인증 토큰, 모달 상태)는 Context나 Zustand·Redux로 옮긴다. 넷째, SSR을 쓴다면 QueryClient는 요청 단위로 생성하고 hydration 일치 여부를 별도로 검증한다.
- 서버 상태(목록, 검색 결과, 프로필) → React Query, 캐시·재검증 로직을 직접 짜지 않는다.
- UI 로컬 상태(폼 입력, 토글) → useState/useReducer, React Query로 대체하지 않는다.
- 순수 클라이언트 전역 상태(테마, 인증, 사이드바) → Context 또는 Zustand/Redux, 구독 범위로 선택한다.
- SSR 프로젝트 → QueryClient는 요청마다 새로 생성, dehydrated state 일치 여부 확인.
핵심 정리
- 전역 상태 도구는 하나로 통일하는 게 목표가 아니라, 데이터 출처별로 분리하는 게 목표다.
- 서버 데이터는 React Query, 로컬 UI는 useState, 공유되는 순수 클라이언트 상태는 Context/Zustand·Redux로 나눈다.
- SSR에서는 QueryClient를 요청 단위로 생성하고 hydration 상태 일치를 별도로 검증해야 사용자 간 데이터 오염을 피할 수 있다.
- 서버 상태를 전역 스토어에 직접 캐싱하거나, 반대로 순수 클라이언트 상태를 쿼리 캐시에 우겨넣는 두 방향 모두 유지보수 비용을 키운다.
- 2026년 기준으로도 "로컬-공유-전역클라이언트-서버"라는 4단 분리가 실무 기본 틀로 유지되고 있다.