[Library] RTK Query VS. Tanstack Query
1️⃣ 이 주제로 글을 쓰는 이유
회사에서 웹과 앱을 각각 다른 스택으로 운영하고 있었고, 서버 상태 관리는 웹은 TanStack Query, 앱은 RTK Query를 사용하고 있었다.
이 상태에서 모노레포 도입을 고려하게 되었고, 공통 API / 인증 로직을 어떻게 가져가야 할지가 문제였다.
이로 인해 하나의 라이브러리로 통합하려는 고민과 어떤 선택이 장기적으로 더 나은지 고민하게 되었다.
2️⃣ RTK Query vs Tanstack Query
이번 선택에서 중요하게 본 기준은 다음과 같았다.
- 웹 / 앱 구조 차이에 대한 유연성
- 서버 상태와 UI 상태의 분리 정도
- 기존 코드베이스와의 정합성
- 모노레포 환경에서의 코드 공유 용이성
각 라이브러리 정의
RTK Query
- Redux와 깊게 통합되어 전역 상태 관리를 하면서 데이터를 가져오고 동기화함
- createApi를 통해 API Slice를 정의하며, 이 slice는 자동으로 데이터 요청/조작용 훅 생성
- 훅 : ex. useLoginMutation, useFetchPowerplantQuery
Tanstack Query
- Tanstack Query는 상태 관리 라이브러리와 독립적으로 동작하며, 컴포넌트 수준에서 캐싱과 서버 상태 관리를 처리함
- queryKey, select, invalidateQueries 등을 제공함
- axios와 같은 요청 라이브러리를 직접 사용하여 요청 로직을 구성함
| RTK Query | Tanstack Query | |
|---|---|---|
| 상태 관리 | Redux가 제공하는 기능 중 하나. | |
| Redux가 적용된 프로젝트에 추가하기 쉬움 | 상태를 저장하는 저장소가 따로 존재하지 X, | |
| Redux가 아닌 여러 상태 관리 라이브러리와 호환하여 사용 가능 | ||
| API 호출법 | createAPI 메서드 사용 | useQuery, useMutation 메서드 사용 |
| 데이터 정규화 | createAPI에서 데이터를 정규화하여 상태 관리 최적화 | 데이터를 자동으로 정규화하지는 않으나 |
| API의 요청 중복 방지 및 캐싱을 통해 성능 향상 | ||
| 캐싱 및 태그 관리 | providesTags와 invalidatesTags를 통해 데이터 변경 시 쿼리 데이터를 자동으로 갱신함 | |
| 철학 | 중앙 집중식 엔드포인트 정의 | 컴포넌트 단위의 선언적 페칭 |
| Github Stars | 11.2k | 48.6k |
| 번들 사이즈 | 13.4KB | 13.3KB |
📜1️⃣ RTK Query로 통합하는 경우
개요
- 서버 상태 관리를 RTK Query + Redux Store 기준으로 통합
- 앱은 기존 구조 유지
- 웹은 기존 Tanstack Query 기반 API 로직을 RTK Query로 전환
적합한 경우
- Redux를 팀 표준 상태 관리로 강하게 가져가려는 경우
- 앱 중심 구조이며 웹은 상대적으로 단순한 경우
- API 구조 변경 가능성이 낮은 내부 관리용 서비스
장점
- Redux 기반 전역 상태 관리에 익숙할 경우, 효율 증가
- 앱 쪽 추가 작업이 거의 없음
단점
- 웹에서 이미 사용 중인 Tanstack 생태계와 철학적 불일치
- Tanstack Table / Virtual과 데이터 연결 구조가 어색해질 수 있음
- 웹의 기존 API 코드 마이그레이션 비용이 큼
- Redux 의존도가 웹까지 확대됨
- 서버 상태와 UI 상태가 Redux에 함께 쌓이며 장기적으로 store의 복잡도 증가 우려됨
- 모노레포 환경에서
- store 구조 차이로 공통 API 로직 재사용이 어려워질 가능성
📜2️⃣ TanStack Query로 통합하는 경우
개요
- 서버 상태 관리를 Tanstack Query + Zustand 기준으로 통합
- 웹은 기존 구조 유지
- 앱은 RTK Query 기반 API 로직을 Tanstack Query로 전환
적합한 경우
- 테이블 / 리스트 / 대용량 데이터 중심 서비스
- 웹과 앱을 동등하게 혹은 웹 중심 구조일 경우
- 장기적인 확장성과 유연성을 중시하는 경우
장점
- 웹에서 이미 사용 중인 Tanstack Table / Virtual과 라이브러리 생태계 일치 → 통일성 증가, 데이터 연결 자연스럽게 연계 가능
- 모노레포에서
- API 함수 / query key / 훅을 패키지 단위로 공유하기 쉬움
- 앱 번들 크기 및 구조 단순화
단점
- 앱에서 RTK Query 제거 및 마이그레이션 비용 발생
- Redux 기반 패턴에 익숙할 경우 초기 적응 비용 존재
3️⃣ 웹, 앱 인증 및 토큰 로직 통합 플랜
1. 방식 설정
1. 웹과 앱을 같은 로그인 상태로 묶고 싶은가?
- 웹에서 로그아웃 → 앱에서도 자동 로그아웃
- 하나의 세션으로 관리
2. 웹과 앱을 독립적인 로그인 세션으로 가져가고 싶은가?
- 웹과 앱이 다른 쿠키를 쓰도록 분리시킴
⇒ 2번 방안으로 확정
2. 통합 규칙
-
쿼리 라이브러리, 전역 상태 관리 라이브러리 통일
: RTK Query + Redux VS. Tanstack Query + Zustand
-
웹에서 사용하는 토큰 관리 방식
- zustand : accessToken, sessionReady
- axios interceptor : 토큰 부착 + refresh
- custom hook
-
앱에서 사용하는 토큰 관리 방식
login mutation ↓ onQueryStarted ↓ setAccessToken(accessToken)
-
-
Middleware는 유지
3. 개발 플랜
- accessToken 저장소 통일하기
- 웹의 인증 API 단계 통일하기
- 인증 API : login, refresh, logout
- 세션 초기화 로직을 웹 + 앱 하나로 통합
4️⃣ 결론
RTK Query로의 통합은 단기적으로 앱 구조와의 정합성이 높으나,
이미 웹이 TanStack 생태계를 중심으로 확장되고 있는 상황에서는
웹의 구조적 변경 비용과 장기 유지보수 리스크가 존재한다.반면 TanStack Query 기준 통합은 앱의 마이그레이션 비용이 발생하지만, 웹과 앱 모두에서 서버 상태 관리 패턴을 통일할 수 있으며 모노레포 운영과 장기 확장성 측면에서 더 안정적인 선택지라고 판단했다.
Tanstack Query를 선택하게 된 이유
개인적으로 Tanstack Query가 선택하는 것이 더 좋다고 생각하였고, 이에 대한 나의 의견을 작성해보았다.
-
현재 웹에서 Tanstack Query뿐만 아니라 Tanstack Virtual, Tanstack Table을 사용 중
- API 호출만 RTK Query를 사용할 경우, 프론트엔드 전반의 일관성이 떨어짐
- TanStack Table의 필터/정렬 상태를 Tanstack Query의 키값으로 연결해 서버 데이터를 페칭하는 구조를 모노레포 전체에서 재사용하기 유리해짐
-
Zustand 용량 <<< Redux Toolkit 용량
- 앱 환경에서는 번들 크기와 메모리 관리가 중요함
- 상대적으로 경량화된 라이브러리인 zustand를 사용하는 게 앱의 특성에 있어 적합함
라이브러리 redux-toolkit zustand 용량 5.51MB 327KB - 앱의 기존 Redux 로직도 Zustand를 통해 점진적으로 단순화하면 전체적인 코드 베이스가 가벼워질 것으로 예상됨
-
모노레포에서의 코드 공유
- 모노레포의 핵심 :
packages/api와 같은 공통 로직을 분리하는 것 - Tanstack Query : API 요청 함수와 커스텀 훅을 순수하게 정의 → 웹과 앱에서 그대로 import하여 유연하게 사용 가능
- RTK Query : createApi로 정의된 서비스 객체가 Redux store 구성 방식에 강하게 결합 → 플랫폼별로 store 구조가 다를 경우 공유 로직 작성이 까다로움
- 모노레포의 핵심 :
5️⃣ 참고
6️⃣ 번외
➕ 시나리오 : Redux/Toolkit + Tanstack Query
개요
- 서버 상태는 Tanstack Query로, 클라이언트/전역 상태는 RTK를 사용함
- 서버 데이터 : Tanstack Query를 통해 가져옴
- UI 상태 / 전역 상태 (폼, 모달, 필터, 탭) : RTK에서 slice로 관리
- Redux를 “전역 UI 상태 전용”으로 최소화
- 서버 상태를 Redux로까지 끌고 오는 경우는 불필요한 boilerplate + 동기화 버그 발생 가능
- TanStack Query가 이미 컴포넌트 레벨 캐시를 제공하기 때문에, 대부분 UI 상태만 Redux로 유지
// Redux: filter 상태
const filter = useSelector((state) => state.ui.filter);
// TanStack Query: 서버 데이터
const { data: users } = useQuery(['users'], fetchUsers);
// 결합
const filteredUsers = users?.filter(u => u.role === filter);
장점
- 서버 상태와 클라이언트 상태의 역할이 명확함
- 각 라이브러리가 잘하는 역할에만 집중 → 구조 이해가 쉬움
- 대규모 서비스에서 확장성 좋음
- UI 상태는 전역으로 공유하면서도 서버 상태는 컴포넌트 단위로 독립적으로 관리 가능
- 팀 단위 협업 시 책임 범위가 명확해짐
- 불필요한 Redux boilerplate 감소
- async thunk, loading/error 상태 관리 코드가 줄어듦
- Redux는 순수 상태 관리에만 사용되어 코드가 가벼워짐
- 참고 블로그 : https://mkdiriandev.tistory.com/4
단점
- 상태 관리 도구가 2개라 러닝 커브가 있음
- Redux Toolkit + TanStack Query 모두에 대한 이해 필요
- Redux를 쓰는 이유가 약해질 수 있음
- 전역 UI 상태가 많지 않은 서비스라면 굳이 Redux 쓸 필요 X
- 역할 분리가 무너지면 오히려 혼란
- Redux와 Query를 사용하는 기준이 없으면 상태가 중복되거나 책임이 섞일 위험 있음
주의할 점
-
Tanstack Query로 fetch한 데이터를 Redux store에 복사하지 말 것
→ 중복 캐시 + 상태 동기화 문제 발생 가능
-
명확한 역할 분리
➕ tanstack query + zustand 에서 전역 store 파일 하나로 공유하여 사용할 수 있을까?
import { create } from "zustand";
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
// 컴포넌트 어디서든
const count = useStore((state) => state.count);
장점
-
boilerplate 없이 전역 상태 생성
-
Redux처럼 Provider/Context/Reducer/Action 구조가 필요 없고, 단일 훅으로 상태와 수정 함수를 정의해서 사용 가능
→ useStore()를 호출하는 모든 컴포넌트는 같은 전역 store에 접근!
-
-
selector 기반 구독으로 성능 최적화
- Zustand는 selector를 지원하므로, 필요한 상태만 구독하여 불필요한 리렌더 감소시킴
➕ 쿼리 + 전역 상태 조합 비교
📍 작고 빠른 SPA / 팀 소규모 / 단순 UI 로직
👉 <strong>TanStack Query + Zustand</strong>
📍 중대형 앱 / 구조화된 전역 상태 필요
👉 <strong>TanStack Query + Redux Toolkit</strong>
📍 대형 조직 / Redux 중심 아키텍처
👉 <strong>RTK Query + Redux Toolkit</strong>
