Docs
Getting Started
Decisions on DX

TanStack Router를 처음 사용하는 사람들은 종종 다음과 같은 질문을 합니다:

왜 이렇게 해야 하나요?

왜 이렇게 구현되었나요? 다른 방식이 더 나아 보이는데요?

원래 이런 방식으로 작업했는데, 왜 변경해야 하나요?

이 모든 질문은 매우 타당합니다. 대부분의 사람들은 비슷한 API, 비슷한 개념, 그리고 비슷한 방식으로 작동하는 라우팅 라이브러리를 사용해왔습니다.

하지만 TanStack Router는 다릅니다. 일반적인 라우팅 라이브러리가 아닙니다. 일반적인 상태 관리 라이브러리도 아닙니다. 사실, 어떤 면에서도 평범하지 않습니다.

TanStack Router's origin story

TanStack Router의 기원은 Nozzle.io (opens in a new tab)에서 시작되었습니다. 이들은 URL 검색 매개변수를 최우선으로 고려하면서도 복잡한 대시보드를 지원하기 위한 타입 안정성을 제공하는 클라이언트 측 라우팅 솔루션이 필요했습니다.

따라서 TanStack Router의 설계는 처음부터 끝까지 모든 세부 사항이 꼼꼼히 검토되어, 타입 안정성과 개발자 경험이 최고 수준이 되도록 만들어졌습니다.

How does TanStack Router achieve this?

TypeScript! TypeScript! TypeScript!

TanStack Router의 모든 측면은 가능한 한 타입 안정성을 제공하도록 설계되었습니다. 이를 위해 TypeScript의 타입 시스템을 최대한 활용하며, 고급 타입, 타입 추론, 기타 기능을 사용하여 개발자 경험을 부드럽게 만듭니다.

이를 달성하기 위해 라우팅 세계의 표준에서 벗어난 몇 가지 결정을 내렸습니다.

  1. 라우터 구성의 기본 템플릿?: TypeScript가 라우트의 타입을 최대한 추론할 수 있도록 라우트를 정의해야 합니다.
  2. 라우터에 TypeScript 모듈 선언 사용?: TypeScript의 모듈 선언을 사용하여 Router 인스턴스를 애플리케이션 나머지 부분에 전달해야 합니다.
  3. 파일 기반 라우팅을 선호하는 이유?: 파일 기반 라우팅이 라우트를 정의하는 데 선호되는 방식입니다.

요약: TanStack Router의 모든 개발자 경험 설계 결정은 라우트 구성을 제어, 유연성, 유지 관리 가능성을 손상시키지 않으면서 최고의 타입 안정성 경험을 제공하기 위해 만들어졌습니다.

1. Why is the Router's configuration done this way?

TypeScript의 추론 기능을 최대한 활용하고 싶다면, 제네릭이 가장 큰 도움이 된다는 것을 금방 알게 될 것입니다. 따라서 TanStack Router는 모든 곳에서 제네릭을 사용하여 라우트의 타입을 최대한 추론합니다.

이를 위해 TypeScript가 라우트의 타입을 최대한 추론할 수 있는 방식으로 라우트를 정의해야 합니다.

JSX를 사용하여 라우트를 정의할 수 있나요?

JSX를 사용하여 라우트를 정의하는 것은 불가능합니다. TypeScript는 라우터의 라우트 구성 타입을 추론할 수 없기 때문입니다.

// ⛔️ 사용할 수 없습니다.
function App() {
  return (
    <Router>
      <Route path="/posts" component={PostsPage} />
      <Route path="/posts/$postId" component={PostIdPage} />
      {/* ... */}
    </Router>
    // ^? TypeScript는 이 구성에서 라우트를 추론할 수 없습니다.
  );
}

이 방식은 <Link> 컴포넌트의 to 속성을 수동으로 입력해야 하며, 런타임까지 오류를 잡을 수 없게 되어 실용적이지 않습니다.

라우트를 중첩된 객체 트리로 정의할 수 있나요?

// ⛔️ 이 파일은 점점 커지고 관리가 어려워집니다.
const router = createRouter({
  routes: {
    posts: {
      component: PostsPage, // /posts
      children: {
        $postId: {
          component: PostIdPage, // /posts/$postId
        },
      },
    },
    // ...
  },
});

처음에는 전체 라우트 계층을 한눈에 시각화할 수 있으므로 좋은 아이디어처럼 보입니다. 하지만 이 접근 방식에는 대규모 애플리케이션에 적합하지 않은 몇 가지 큰 단점이 있습니다:

  • 확장성이 떨어집니다: 애플리케이션이 커지면서 트리가 커지고 관리가 어려워집니다. 또한 모든 것이 하나의 파일에 정의되어 유지 관리가 어려워질 수 있습니다.
  • 코드 분할에 적합하지 않습니다: 각 컴포넌트를 수동으로 코드 분할하고 이를 라우트의 component 속성에 전달해야 하므로 라우트 구성이 더욱 복잡해집니다.

이 문제는 중첩 컨텍스트, 로더, 검색 매개변수 검증 등 라우터의 더 많은 기능을 사용하기 시작하면 더 심각해집니다.

그렇다면 라우트를 정의하는 가장 좋은 방법은 무엇인가요?

라우트 트리 외부에서 라우트 구성을 추상화한 다음, 단일 일관된 라우트 트리로 결합하여 createRouter 함수에 전달하는 방법이 최선으로 나타났습니다.

[!TIP] 코드 기반 라우팅이 너무 번거롭다면 파일 기반 라우팅이 선호되는 이유를 확인하세요.

2. Declaring the Router instance for type inference

Router를 선언해야 하나요?

이 선언 과정이 너무 복잡합니다...

라우트를 트리로 구성하고 이를 createRouter를 사용하여 Router 인스턴스로 전달한 후, 이 정보를 애플리케이션 나머지 부분에 전달해야 합니다.

우리가 고려한 두 가지 접근 방식은 다음과 같습니다:

  1. Imports: Router 인스턴스를 생성한 파일에서 import하여 직접 컴포넌트에서 사용하는 방법.

  2. Module declaration: TypeScript의 모듈 선언을 사용하여 Router 인스턴스를 애플리케이션 어디에서든 타입 추론에 사용할 수 있도록 모듈로 선언하는 방법.

우리는 모듈 선언 방식을 선택했으며, 이는 가장 확장 가능하고 유지 관리가 용이한 접근 방식으로 나타났습니다.

3. Why is file-based routing the preferred way to define routes?

왜 문서에서 파일 기반 라우팅을 권장하나요?

TanStack Router는 복잡한 애플리케이션을 위해 설계되었으며, 높은 수준의 타입 안정성과 유지 관리 가능성을 요구합니다. 이를 달성하기 위해 라우터의 구성이 정확하게 설계되었습니다.

파일 기반 라우팅은 이러한 문제를 해결하며, 애플리케이션이 성장함에 따라 관리 및 유지가 용이한 예측 가능한 방식으로 라우트를 정의할 수 있게 합니다.

더 자세한 내용은 파일 기반 라우팅코드 분할 가이드를 확인하세요.