JavaScript/React

React Router 와 TanStack Router 비교해보기

남희정 2025. 10. 26. 22:48

📌 목차

 

이직을 하고 적응 + 새로운 루틴을 만드느라 글을 오랜만에 쓴다.. (반성)

이전엔 Vite로 적용되는 React 기본 폴더 구조를 썼고 React Router의 코드 기반 라우팅을 사용해 봤던 경험이 많았다. 그러다 최근에 Next를 프로젝트에서 파일 시스템 구조에 맞춰서 자동으로 라우팅을 연결해 주는 File-based Routing을 사용해 본 경험으로 인해 편리함을 맛보아서 그런지 React에서도 비슷하게 사용하고 싶었다. 그러다 TanStack Router를 알게 되었다.

TanStack Router를 적용해보면서 기존에 써봤던 React router와의 차이를 비교해보려 한다.

단기간 사용으로 느낀 점을 다루다 보니 실제 TanStack의 잠재성보다 얄팍할 수 있다는 점,, 말씀드린다.


React Router | React의 전통적 라우팅 방식

수년간 React Routing의 필수 선택이었던 React Router는 간단하고 선언적인 접근 방식을 사용한다.

이는 2014년에 출시되어 개발자가 React 앱에서 탐색을 처리하는 방식을 바꾸어 놓았다.

 

React Router의 가장 큰 장점은 간단한 API 디자인입니다. 기존 앱 구조에 자연스럽게 맞춰지는 React 컴포넌트로 경로를 정의할 수 있습니다. 브라우저 기록 통합 기능을 통해 단일 페이지 앱의 모든 이점을 유지하면서도 기존 웹사이트처럼 매끄러운 탐색을 제공합니다.
- Stanley Ulili

 

React Router가 등장하기 전, 과거에는 브라우저의 history API를 직접 조작하거나, window.location.hash를 감시하면서 페이지 전환을 수동으로 관리했다. 이 방식은 간단했지만 복잡한 UI 구조에선 유지보수가 어려웠다.

 

라우팅을 React 컴포넌트처럼 다루자!

선언적 라우팅 Declarative Routing

React Router는 선언적으로 경로를 정의할 수 있다.

<Route path="/about" element={<About />} />

라우트 정의 자체가 React 컴포넌트 트리의 일부로 들어가며, 개발자는 UI를 구성하듯 자연스럽게 경로를 선언한다.

코드 기반 라우팅 Code-based Routing

React Router는 코드기반 라우팅 방식을 사용한다. 개발자가 모든 라우트를 직접 등록해야 한다.

import { createBrowserRouter, RouterProvider } from 'react-router'
import Home from './pages/Home'
import About from './pages/About'
import ProductList from './pages/products/ProductList'
import ProductDetail from './pages/products/ProductDetail'

const router = createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/about', element: <About /> },
  {
    path: '/products',
    element: <ProductsLayout />,
    children: [
      { path: '', element: <ProductList /> },
      { path: ':id', element: <ProductDetail /> },
    ],
  },
])

export default function Router() {
  return <RouterProvider router={router} />
}

직관적이고 보기 쉬운 것은 확실하다. JS로직 안에서 라우트 정의를 하니까 조건부로 생성하거나 비활성화할 수 있다.

const routes = [
  { path: '/', element: <Home /> },
  user.isAdmin && { path: '/admin', element: <AdminDashboard /> },
].filter(Boolean)

또한 권한 관리, 인증 등 복잡한 로직과 자연스럽게 결합이 가능하다.

const router = createBrowserRouter([
  {
    path: '/admin',
    element: user.role === 'admin' ? <Admin /> : <Navigate to="/login" />,
  },
  {
    path: '/experiment',
    element: featureFlags.newUI ? <NewUI /> : <OldUI />,
  },
])

코드 기반 라우팅은 파일 기반 라우팅보다 더 명시적이고 유연한 접근 방식을 제공한다. 동적 조건에 따라 라우트를 구성, 제한하여 전체 구조를 한 파일에서 한눈에 파악할 수 있다.

 

이전 프로젝트를 하며 불편함을 느낀 적은 없다. 다만 대규모의 프로젝트를 진행하게 된다면 어떻게 될까?

페이지 하나를 추가할 때마다

 

✔️ import 추가

✔️ 라우터 객체에 path + element 등록

✔️ 중첩 구조일 경우 하위에 children 배열 구성

 

서비스 규모가 커지면 커질수록 파일이 비대해지고 라우트 관리 자체가 유지보수 포인트가 되는 일이 발생한다.

TypeScript 환경

TypeScript를 지원하지만 loader, useLoaderData, params, searchParams 등의 타입을 직접 선언해주어야 한다.

import { useParams, useSearchParams } from 'react-router-dom'

interface ProductParams {
  productId: string
  categoryId?: string
}

interface ProductSearchParams {
  sort?: 'price' | 'rating' | 'name'
  filter?: string
  page?: number
}

const ProductDetail = () => {
  const params = useParams<ProductParams>()
  const [searchParams] = useSearchParams()

  const sort = searchParams.get('sort') as ProductSearchParams['sort'] // as 타입 단언
  const page = parseInt(searchParams.get('page') || '1', 10)
  const filter = searchParams.get('filter') || undefined

  const productId = params.productId! // non-null 타입 단언

  return (
    <div>
      <h1>Product {productId}</h1>
      <ProductFilters sort={sort} page={page} filter={filter} />
    </div>
  )
}

const navigate = useNavigate()
const handleProductSelection = (id: string) => {
  navigate(`/products/${id}?sort=price&page=1`)
}

이런 접근 방식은 잘 작동하지만, 라우트 구조가 복잡한 대규모 애플리케이션에서는 타입 안전성을 유지하려면 개발자의 관리가 필요하다.

여전히 좋은 선택, React Router

러닝커브가 낮고, 구현 속도가 빠르며 커뮤니티와 생태계가 매우 탄탄하게 형성되어 있다. 특히 단순하고 명시적인 라우팅 구조가 필요한 프로젝트, 관리자 대시보드, 사내 도구처럼 경로 구조가 고정적이라면 무엇보다 적합하다고 생각한다.

하지만 프로젝트 규모가 커지고 라우트 구조가 복잡해질수록 타입 일관성을 개발자가 직접 관리해야 하는 부담이 커진다.

 

TanStack router는 대규모 애플리케이션을 어떻게 대응할까?

TanStack Router | 타입 안전성과 엄청난 DX

TanStack Router는 타입 안전성과 현대적인 개발 방식을 중심으로 React 라우팅의 패러다임을 재정의했다.

TanStack Query 개발팀이 기존 라우터가 겪는 라우팅 문제, 특히 TypeScript 통합 문제를 해결하기 위해 탄생시켰다.

TypeScript 우선 설계

TanStack Router는 TypeScript-first 설계를 채택했다. 라우팅 구조에서 발생할 수 있는 타입 불일치 문제를 컴파일 시점에 미리 차단한다.

 

✔️ routeTree.gen.ts 파일에 경로, 매개변수, 검색 쿼리에 대한 타입 정의 자동 생성

✔️ 경로 파라미터와 검색 파라미터의 타입 안전성 보장

✔️ 컴파일 시점에 오류 포착, 런타임 에러를 방지

 

import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts/$postId')({
  component: PostPage,
})
import { Link } from '@tanstack/react-router'

export function Home() {
  return (
    <Link to="/posts/123" /> 
    // 여기서 만약 /post/로 할 경우 타입 에러
  )
}

예로 /posts/$postId 형태의 라우트가 존재하는데, 개발자가 실수로 /post/...형태로 적용하는 경우 바로 에러가 난다.

 

TanStack Router타입 에러 예시

import { useRouter } from '@tanstack/react-router'

function GoButton() {
  const router = useRouter()
  return (
    <button
      onClick={() =>
        router.navigate({
          to: '/products/$productId',     // 경로 오타/누락 시 컴파일 에러
          params: { productId: '123' },   // 필수 파라미터 누락 시 에러
        })
      }
    >
      Go
    </button>
  )
}

라우트 구조 전체를 타입으로 모델링하기 때문에 오류를 즉시 감지할 수 있다. (생성되는 routeTree.gen.ts 파일에 관해선 아래에서 더 살펴보자)

loader()

TanStack Router는 loader() 함수를 통해 라우트 렌더링 이전에 데이터를 미리 불러올 수 있다. React Router의 경우에도 v6.4 이후로 Data API가 추가되어 동일한 패턴을 지원한다. 하지만 가장 큰 차이는 타입 전파 Type inference이다.

TanStack Router는 loader()에서 반환된 데이터의 타입이 자동으로 추론되어 컴포넌트까지 전달된다.

import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/products/')({
  loader: async () => {
    return { products: await fetchProducts() }
  },
  component: ({ useLoaderData }) => {
    const { products } = useLoaderData() // 타입 자동 추론
    return <ProductList products={products} />
  },
})

파일 기반 라우팅 File-based Routing

Next.js처럼 폴더 구조를 기반으로 라우트를 자동 생성한다. /routes 폴더에 파일을 추가하는 것만으로 새로운 페이지가 생성된다. (이것 때문에 선택했다고 해도 과언이 아님)

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

export const Route = createRootRoute({
  component: () => (
    <div>
      <Navigation />
      <main>
        <Outlet />
      </main>
      <Footer />
    </div>
  ),
});
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/')({
  component: HomePage,
});

TanStack Query와의 자연스러운 통합

TanStack Router는 TanStack Query 팀에서 만든 만큼 굉장히 자연스럽게 통합된다.

라우터의 loader나 beforeLoad내에서 Query 캐싱, prefetch, 데이터 동기화 로직을 자연스럽게 연결할 수 있다.

import { createFileRoute } from '@tanstack/react-router'
import { PostDetail } from '../pages/PostDetail'
import { fetchPost } from '../lib/api'

export const Route = createFileRoute('/posts/$postId')({
  loader: async ({ params, context }) => {
    const { queryClient } = context
    return queryClient.ensureQueryData({
      queryKey: ['post', params.postId],
      queryFn: () => fetchPost(params.postId),
    })
  },
  component: PostDetail,
})
import { useParams } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { fetchPost } from '../lib/api'

export function PostDetail() {
  const { postId } = useParams()
  const { data: post, isLoading } = useQuery({
    queryKey: ['post', postId],
    queryFn: () => fetchPost(postId),
  })

  if (isLoading) return <p>Loading...</p>
  return <h1>{post?.title}</h1>
}

어떻게 타입 검증이 컴파일 시점에 이루어질까?

routeTree.gen.ts 파일에서 출발한다.

  1. @tanstack/router-pluginroutes/ 폴더를 감시해서 전체 라우트 트리를 분석하고, 타입 정보가 담긴 routeTree.gen.ts 를 생성한다.
  2. 파일을 추가/이동/이름 변경 시 즉시 다시 생성되어 항상 최신 상태를 유지한다.
  3. 앱에서 라우터를 생성할 때 이 타입 정보를 불러온다.
import { createRouter } from '@tanstack/react-router' 
import { routeTree } from './routeTree.gen'

export const router = createRouter({ routeTree })

 

이 과정 덕분에 TanStack Router는 잘못된 경로, 누락된 파라미터, 틀린 쿼리 구조를 컴파일 단계에서 완전히 차단하는 것이다.


글을 마치며

React Router만을 써보다 처음으로 TanStack Router를 적용해 보면서 마치 Express에서 Nest.js로 넘어갔을 때 느낀 변화처럼 개발자 경험에 초점을 맞춘 도구라는 인상을 강하게 받았다.

이제 TypeScript는 React 개발에서 떼어낼 수 없는 존재가 되었고, 라우팅 또한 그 흐름을 따르는 것은 자연스러운 방향이라 생각한다. 무엇보다 컴파일 단계에서 사이드 이펙트를 방지하고, 타입 레벨에서 안정성을 확보하는 철학이 마음에 들었다.

아직 정말 써 본 지 얼마 되지 않아서 깊이 있는 평가는 어렵지만 단기간 사용만으로도 타입 기반 라우팅의 강점을 꽤 체감했다.

 

정리하자면, React Router는 견고한 생태계와 적절한 업데이트로 충분히 경쟁력 있는 도구다. 다만 규모가 커질수록 유지보수 측면에서 고려하다 보면 TanStack Router는 훨씬 강력한 대안이 될 수 있다.

무엇보다 개발자의 관리 포인트를 줄여준 것이 최고의 니즈를 반영한 거 아닌가..?

TanStack 팀이 앞으로 또 어떤 진화를 보여줄지 그리고 TanStack Start 같은 풀스택 프레임워크로 어떻게 확장될지 기대된다.

 

아직은 서툰 기록이지만 조금 더 경험을 쌓은 뒤에 다시 이 주제를 다뤄보려 한다. 

오랜만의 글, 읽어주셔서 감사합니다!!!!!!

 

https://betterstack.com/community/comparisons/tanstack-router-vs-react-router/

https://doctorfeel.tistory.com/445#google_vignette

https://velog.io/@cjhlsb/React.js%EC%99%80-TanStack-Router%EB%A1%9C-Next.js%EC%97%90%EC%84%9C-%EB%B2%97%EC%96%B4%EB%82%98%EA%B8%B0

https://tanstack.com/router/latest/docs/framework/react/overview