TanStack Router 실무 가이드, 파일 기반 라우팅과 타입 안전하게 React 구조 잡기

작성일:2026.05.09|조회수:4

TanStack Router 실무 가이드, 파일 기반 라우팅과 타입 안전하게 React 구조 잡기

React 애플리케이션이 커질수록 라우팅은 단순한 화면 전환 문제가 아니라, URL 설계, 데이터 로딩, 권한 처리, 상태 동기화까지 함께 다뤄야 하는 구조의 문제가 된다. TanStack Router는 이 지점을 정면으로 다룬다. 파일 기반 라우팅을 중심에 두고, URL 파라미터와 검색 파라미터, loader 데이터까지 타입 안전하게 연결해서 애플리케이션의 뼈대를 더 예측 가능하게 만든다.

이 글은 TanStack Router를 처음 도입하거나, 이미 써 봤지만 실무 기준의 정리된 원칙이 필요한 개발자를 위한 가이드다. 설정 순서, 파일 구성 규칙, search params 처리 방식, loader의 책임, 흔한 실수까지 실제 프로젝트에서 바로 적용할 수 있는 기준으로 정리했다.

TanStack Router를 보는 관점

TanStack Router는 단순히 라우트 몇 개를 선언하는 라이브러리로 접근하면 장점을 제대로 살리기 어렵다. 이 도구의 핵심은 애플리케이션의 라우팅 계층을 타입 안전하게 설계하고, 데이터 흐름의 기준점을 라우트로 끌어올리는 데 있다.

프로젝트 설정에서 먼저 잡아야 할 기준

Vite 플러그인 순서

TanStack Router를 Vite에서 사용할 때 가장 먼저 확인할 부분은 플러그인 선언 순서다. tanstackRouter 플러그인은 코드 생성을 담당하므로 react()보다 앞에 와야 한다. 이 순서를 틀리면 생성 파일이 기대대로 만들어지지 않거나 개발 경험이 흔들릴 수 있다.

TS
// vite.config.ts
import { tanstackRouter } from '@tanstack/router-plugin/vite';

export default defineConfig({
  plugins: [
    tanstackRouter({
      autoCodeSplitting: true, // 라우트별 자동 청크 분리
    }),
    react(),
  ],
});

autoCodeSplitting을 켜 두면 라우트 단위로 청크를 나누는 기본 흐름을 자연스럽게 가져갈 수 있다. 규모가 있는 앱에서는 초기에 켜 두는 편이 보통 더 낫다.

Router 인스턴스 생성과 타입 등록

플러그인이 생성하는 routeTree.gen.ts는 애플리케이션 전체 라우트 구조의 기준점이다. 이 파일은 자동 생성 결과물이므로 직접 수정하면 안 된다. 실제로 손댈 파일은 라우트 정의 파일과 Router 생성 코드이다.

TS
// src/main.tsx
import { routeTree } from './routeTree.gen';

const router = createRouter({
  routeTree,
  defaultPreload: 'intent', // 마우스 호버 시 데이터를 프리로드하여 체감 성능 향상
});

// 타입 안전성을 위해 전역 모듈에 등록
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router;
  }
}

defaultPreload: 'intent'는 사용자가 이동할 가능성이 높은 시점에 데이터를 미리 불러와서 체감 성능을 높이는 데 도움이 된다. 또한 모듈 등록을 해 두어야 Link, useNavigate, 라우트 관련 API가 전체 라우트 트리 정보를 타입으로 연결할 수 있다.

파일 기반 라우트 구성 패턴

TanStack Router의 생산성은 파일 시스템 규칙을 얼마나 일관되게 지키느냐에 크게 좌우된다. 기준 디렉터리는 보통 src/routes이며, 각 파일은 반드시 export const Route = ... 형식으로 라우트 객체를 내보내야 생성기가 인식한다.

핵심 파일 규칙

루트 레이아웃 예시

먼저 전체 앱의 공통 뼈대를 __root.tsx에 둔다. 자식 라우트가 들어올 지점은 Outlet으로 명확히 표시한다.

TSX
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'

export const Route = createRootRoute({
  component: () => (
    <>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/posts">Posts</Link>
      </nav>
      <hr />
      <Outlet /> {/* 자식 라우트가 렌더링되는 지점 */}
    </>
  ),
})

이 구조의 장점은 단순하다. URL 계층과 레이아웃 계층이 자연스럽게 맞물리고, 앱 전체의 공통 UI를 한 군데에서 관리할 수 있다.

중첩 라우트 예시

부모 레이아웃과 자식 라우트는 URL 구조를 그대로 반영하는 편이 좋다. 이렇게 해야 라우트 파일만 봐도 화면 구성과 데이터 흐름을 빠르게 이해할 수 있다.

TSX
// src/routes/posts.tsx
import { Outlet, createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
  component: PostsLayout,
})

function PostsLayout() {
  return (
    <section>
      <h1>Posts</h1>
      <Outlet />
    </section>
  )
}

// src/routes/posts.$postId.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  loader: ({ params }) => fetchPost(params.postId),
  component: PostDetail,
})

여기서 중요한 점은 params.postId가 라우트 경로와 연결된 타입 정보를 기반으로 다뤄진다는 점이다. 동적 세그먼트를 문자열로 임의 처리하는 습관보다 훨씬 안전하다.

패스리스 레이아웃 예시

인증된 사용자 영역이나 앱 셸처럼 URL에는 노출되지 않지만 공통 레이아웃은 필요한 경우가 많다. 이런 구조에 패스리스 레이아웃이 잘 맞는다.

TSX
// src/routes/_app.tsx
import { Outlet, createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_app')({
  component: AppLayout,
})

function AppLayout() {
  return (
    <div className="app-shell">
      <Sidebar />
      <main>
        <Outlet />
      </main>
    </div>
  )
}

// src/routes/_app.dashboard.tsx
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/_app/dashboard')({
  component: DashboardPage,
})

이 패턴을 쓰면 URL은 깔끔하게 유지하면서도 공통 레이아웃 책임을 라우트 계층 안에 정리할 수 있다.

Route Colocation 규칙

라우트와 관련된 컴포넌트, 훅, 유틸을 라우트 근처에 같이 두고 싶을 때가 많다. 이때 - 접두사를 사용하면 플러그인이 해당 디렉터리를 라우트 파일로 해석하지 않고 건너뛴다.

TS
src/routes/
├── posts/
│   ├── index.tsx
│   ├── $postId.tsx
│   ├── -components/    # 라우트 생성에서 제외됨
│   │   └── PostCard.tsx
│   └── -hooks/
│       └── usePostActions.ts

이 규칙은 프로젝트가 커질수록 빛을 발한다. 라우트 단위로 파일을 모아 두되, 생성 규칙은 깨지지 않게 유지할 수 있기 때문이다.

Search Params와 데이터 로딩을 함께 설계하기

TanStack Router를 실무에서 안정적으로 쓰려면 search params를 단순 문자열 쿼리로 다루지 말고, 라우트 경계에서 검증된 애플리케이션 상태로 변환해야 한다. 이때 validateSearch, loaderDeps, loader를 역할별로 분리하는 패턴이 중요하다.

타입 안전한 Search Params

아래 예시는 Zod를 사용해 URL 쿼리 파라미터를 파싱하고, loader가 실제로 의존하는 값만 명시적으로 연결하는 방식이다.

TS
import { z } from 'zod';

const searchSchema = z.object({
  page: z.coerce.number().catch(1),
  filter: z.string().catch(''),
});

export const Route = createFileRoute('/products')({
  validateSearch: (search) => searchSchema.parse(search),
  loaderDeps: ({ search: { page, filter } }) => ({ page, filter }), // 의존성 명시
  loader: ({ deps }) => fetchProducts(deps), // deps 변경 시 loader 재실행
  component: ProductListComponent,
});

실무에서는 아래 세 가지를 기준으로 삼으면 흔들림이 적다.

이 패턴을 지키면 search params는 바뀌었는데 데이터는 갱신되지 않는 식의 버그를 크게 줄일 수 있다.

TanStack Router의 링크와 이동 API는 문자열 기반 라우팅보다 한 단계 더 안전한 추상화를 제공한다. 라우트 트리 등록이 제대로 되어 있다면, 존재하지 않는 경로나 누락된 파라미터를 더 이른 시점에 잡을 수 있다.

TSX
// 타입 안전한 링크
<Link
  to="/posts/$postId"
  params={{ postId: '123' }}
  search={{ tab: 'comments' }}
>
  View Comments
</Link>

// 프로그래매틱 이동
const navigate = useNavigate()
const updateFilter = (filter: string) => {
  navigate({ search: (prev) => ({ ...prev, filter }) })
}

특히 search params를 함수형 업데이트로 다루는 방식은 기존 상태를 보존하면서 필요한 값만 바꾸는 데 유용하다. 필터, 정렬, 페이지네이션 같은 UI에서 안정적으로 쓸 수 있다.

Next.js App Router와 비교해서 이해하기

TanStack Router를 처음 접하는 개발자 중에는 Next.js App Router 경험이 있는 경우가 많다. 두 도구가 완전히 같은 추상화를 제공하는 것은 아니지만, 대응 관계를 느슨하게 잡아 두면 개념 전환에 도움이 된다.

다만 이 비교는 어디까지나 개념적 대응이다. 실제 동작 모델과 책임 분리는 다를 수 있으니, 이름만 같다고 동일한 설계라고 보면 곤란하다.

Loader 실행 시점은 프로젝트 형태에 따라 달라진다

Loader는 언제 실행되는가, 이 질문은 생각보다 중요하다. 특히 클라이언트 전용 앱인지, SSR이나 풀스택 구성을 포함하는지에 따라 설계 기준이 달라진다.

즉, loader를 작성할 때는 단순히 데이터 fetch 함수로만 생각하지 말고, 현재 프로젝트의 실행 컨텍스트를 같이 고려해야 한다.

실무에서 자주 나오는 안티 패턴

TanStack Router는 규칙을 잘 지키면 강력하지만, 몇 가지 흔한 실수를 반복하면 장점이 빠르게 사라진다. 아래 항목은 도입 초기에 특히 자주 마주치는 문제들이다.

  1. 코드 기반 라우팅 남용 특수한 케이스가 아닌데도 코드 기반 라우팅을 넓게 쓰면 구조 파악이 어려워지고, 파일 기반 생성기의 장점을 잃게 된다.
  2. loaderDeps 누락 search params는 변했는데 loader 재실행 조건에 연결하지 않으면 화면과 데이터가 어긋나는 버그가 생기기 쉽다.
  3. Route 객체 export 누락 export const Route = ... 형식을 지키지 않으면 파일 기반 생성기가 해당 파일을 무시할 수 있다.
  4. routeTree.gen.ts 수동 수정 자동 생성 파일은 개발 서버 실행이나 빌드 과정에서 덮어써진다. 시간을 들여 수정해도 유지되지 않는다.

정리

TanStack Router의 핵심은 파일 기반 라우팅 자체보다, 라우팅을 데이터와 타입의 관점에서 다시 설계하게 만든다는 점에 있다. 라우트 파일 구조를 일관되게 유지하고, search params를 검증된 상태로 다루고, loader 의존성을 명시적으로 연결하면 라우팅 계층이 훨씬 예측 가능해진다. React 앱이 커질수록 이런 기준은 선택이 아니라 유지보수 비용을 줄이는 기본기 쪽에 가깝다.

더 읽어보기

댓글

댓글을 불러오는 중...