콘텐츠로 이동

ADR-0028: Frontend Routing & Data: TanStack

Umbra 의 React SPA 에서 라우팅은 TanStack Router (코드 기반), 서버 상태는 TanStack Query 로 관리한다.

Status

Accepted

  • Decided at — 2026-04-13
  • Decided by — Pablo

Context

Vite + React SPA 를 선택한 후(ADR-0008) 라우팅 라이브러리와 데이터 페칭 전략을 정해야 한다. Dashboard 는 인증 후 사용되며 다음 특성을 가진다.

  • 여러 도메인 화면 (구독, 복구, 설정, 길드 선택) 간 라우팅
  • 각 route 진입 시 서버 데이터 prefetch
  • Mutation 후 관련 데이터 자동 갱신
  • 타입 안전성 요구 (Go 백엔드와 OpenAPI 로 타입 동기화)

라우팅 후보: React Router, TanStack Router, Wouter 데이터 페칭 후보: TanStack Query, SWR, 자체 fetch

Decision

두 TanStack 라이브러리를 조합으로 채택한다.

  • Routing — TanStack Router, 코드 기반 모드 (파일 기반 아님)
  • Server state — TanStack Query
  • Client state — Zustand (별도 ADR 없음, 보조적 선택)
  • Form — React Hook Form + Zod (별도 ADR 없음)

TanStack Router (code-based)

파일 기반이 아닌 코드 기반으로 라우트를 선언한다.

const restoreRoute = createRoute({
  getParentRoute: () => dashboardRoute,
  path: 'restore',
  component: RestorePage,
})

TanStack Query

서버 데이터는 전적으로 TanStack Query 가 관리한다. Zustand 에 서버 상태를 저장하지 않는다.

const { data } = useQuery({
  queryKey: ['billing', 'subscription', guildId],
  queryFn: () => apiFetch(`/guilds/${guildId}/subscription`),
})

선택 근거:

  • 같은 팀 개발 — TanStack Router 와 Query 가 같은 팀에서 개발되어 통합이 자연스럽다. Router loader 가 Query 를 prefetch 하는 패턴이 1급 지원됨.
  • 타입 안전성 — Router 는 route params, search params 모두 타입 추론. Query 는 queryFn 반환 타입이 data 에 그대로 반영.
  • 코드 기반의 장점 — 파일 위치와 라우트 구조 분리 가능. Feature-based 폴더 조직 (ADR 없음, 구조 결정사항) 과 자연 결합. 자동 생성 파일(routeTree.gen.ts) 부담 0.
  • Query 는 사실상 표준 — SaaS 대시보드의 cache, invalidation, optimistic update 관리에 검증된 라이브러리.

Consequences

Positive

  • Route params / search params 의 타입 안전성으로 URL 조작 버그 감소
  • 서버 상태 관리가 TanStack Query 한 곳에 집중 (Zustand 에 섞이지 않음)
  • Route 전환 시 데이터 prefetch 로 UX 개선
  • Mutation 후 자동 invalidation 으로 stale data 방지

Negative

  • 두 라이브러리 모두 학습 곡선 존재 (진욱 입장)
  • TanStack Router 는 React Router 대비 레퍼런스가 적음
  • 코드 기반 라우팅은 파일 기반 대비 보일러플레이트 약간 증가

Neutral

  • 파일 기반 자동 생성(routeTree.gen.ts)을 쓰지 않으므로 build step 단순
  • Zustand 는 UI 상태만 담당 (토글, 모달 open 상태 등) → 책임 분리 명확

Alternatives considered

Alternative 1: React Router v7

Pros

  • 생태계 최대 규모, 레퍼런스 풍부
  • SPA 라우팅의 de-facto 표준

Cons

  • 타입 추론이 TanStack Router 대비 약함
  • loader 패턴이 Remix 와 결합되어 SPA only 사용 시 어색
  • 최근 리브랜딩으로 버전별 호환성 혼란

Why rejected — TanStack Router 의 타입 안전성이 우위. React Router 의 생태계 장점이 Umbra 의 도메인 수(8~9개 route) 규모에서는 결정적이지 않음.

Alternative 2: TanStack Router (file-based)

Pros

  • 파일 구조 = 라우트 트리 = 명시적
  • React Router, Next.js 사용자에 친숙

Cons

  • 자동 생성 파일(routeTree.gen.ts) 이 commit 되거나 gitignore 되어야 함 (팀 컨벤션 필요)
  • Feature-based 폴더 조직과 충돌 (route 기반 강제)

Why rejected — 코드 기반이 Umbra 의 Feature-based 조직과 더 자연스럽게 결합. 자동 생성 파일 관리 부담 회피.

Alternative 3: SWR

Pros

  • 경량, Vercel 팀 개발
  • 단순한 API

Cons

  • TanStack Query 대비 mutation, optimistic update 기능 덜 성숙
  • TanStack Router 와 통합되지 않음

Why rejected — TanStack Query 와의 기능 격차가 있고, Router 와 같은 팀 개발이 아니라 통합 이점 없음.

Alternative 4: Wouter

Pros

  • 매우 경량 (2KB)

Cons

  • 타입 안전성 약함
  • 기능 제한적

Why rejected — 결제 SaaS 대시보드의 기능 요구를 충족 못함.

Alternative 5: fetch 직접 + 자체 캐시

Pros

  • 의존성 0

Cons

  • 캐시, invalidation, retry 를 전부 자체 구현
  • 보일러플레이트 폭증

Why rejected — MVP 일정에 오버엔지니어링. TanStack Query 가 이 모든 문제를 해결.

Compliance

  • apps/web/src/router.ts 에 코드 기반 라우트 트리 정의
  • Query key 는 계층 구조 (['billing', 'subscription', guildId]) 로 일관성 유지
  • Server state 를 Zustand 에 저장 금지 (code review 에서 차단)
  • 라우트 loader 는 Query prefetch 로 사용, 자체 데이터 fetch 는 피함

Revisit triggers

  • TanStack Router 가 유지보수 정체 시 React Router 로 재평가
  • 서버 상태 관리 요구가 Redux Toolkit Query 수준의 복잡도로 확장되면 재검토
  • tRPC-like 엔드투엔드 타입 안전성이 필요하면 새 스택 논의

References

  • ADR-0008 — Frontend 스택
  • ADR-0030 — API 타입 동기화 (Query 와 결합)