코드 기반 라우팅
[!TIP] 코드 기반 라우팅은 대부분의 애플리케이션에 권장되지 않습니다. 대신 파일 기반 라우팅을 사용하는 것이 좋습니다.
⚠️ 시작하기 전에
- 파일 기반 라우팅을 사용한다면 이 가이드를 건너뜁니다.
- 그래도 코드 기반 라우팅을 사용하려면 먼저 라우팅 개념 가이드를 읽어야 합니다. 이 가이드에서 라우터의 핵심 개념도 다루기 때문입니다.
라우트 트리
코드 기반 라우팅도 파일 기반 라우팅과 마찬가지로 동일한 라우트 트리 개념을 사용해 라우트를 구성하고 매칭하며, 매칭된 라우트를 컴포넌트 트리로 조합합니다. 유일한 차이점은 라우트를 구성할 때 파일 시스템 대신 코드를 사용한다는 것입니다.
라우트 트리 가이드의 동일한 라우트 트리를 살펴보고 코드 기반 라우팅으로 변환해 보겠습니다.
다음은 파일 기반 버전입니다.
routes/
├── __root.tsx
├── index.tsx
├── about.tsx
├── posts/
│ ├── index.tsx
│ ├── $postId.tsx
├── posts.$postId.edit.tsx
├── settings/
│ ├── profile.tsx
│ ├── notifications.tsx
├── _pathlessLayout.tsx
├── _pathlessLayout/
│ ├── route-a.tsx
├── ├── route-b.tsx
├── files/
│ ├── $.tsx
다음은 요약한 코드 기반 버전입니다.
React
import { createRootRoute, createRoute } from '@tanstack/react-router'
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'about',
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
})
const postEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts/$postId/edit',
})
const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'settings',
})
const profileRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'profile',
})
const notificationsRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'notifications',
})
const pathlessLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'pathlessLayout',
})
const pathlessLayoutARoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-a',
})
const pathlessLayoutBRoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-b',
})
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'files/$',
})
Solid
import { createRootRoute, createRoute } from '@tanstack/solid-router'
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
})
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'about',
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
})
const postEditorRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts/$postId/edit',
})
const settingsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'settings',
})
const profileRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'profile',
})
const notificationsRoute = createRoute({
getParentRoute: () => settingsRoute,
path: 'notifications',
})
const pathlessLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'pathlessLayout',
})
const pathlessLayoutARoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-a',
})
const pathlessLayoutBRoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-b',
})
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'files/$',
})
라우트 구조
루트 라우트를 제외한 모든 라우트는 createRoute 함수로 구성합니다.
const route = createRoute({
getParentRoute: () => rootRoute,
path: '/posts',
component: PostsComponent,
})
getParentRoute 옵션은 만들고 있는 라우트의 부모 라우트를 반환하는 함수입니다.
❓❓❓ "잠시만요. 만드는 모든 라우트에 부모 라우트를 전달해야 하나요?"
그렇습니다! 부모 라우트를 전달하는 이유는 TanStack Router의 놀라운 타입 안전성과 전적으로 관련이 있습니다. 부모 라우트가 없으면 TypeScript는 라우트에 어떤 타입을 제공해야 하는지 알 수 없습니다!
[!IMPORTANT] 루트 라우트 또는 경로 없는 레이아웃 라우트가 아닌 모든 라우트에는
path옵션이 필요합니다. 이 경로를 URL pathname과 매칭해 라우트가 매칭되는지 판단합니다.
라우트에서 path 옵션을 구성할 때 앞뒤 슬래시는 무시됩니다("index" 라우트 경로 /는 제외). 슬래시를 포함해도 되지만 TanStack Router가 내부적으로 정규화합니다. 다음은 유효한 경로와 정규화되는 결과를 보여주는 표입니다.
| 경로 | 정규화된 경로 |
|---|---|
/ | / |
/about | about |
about/ | about |
about | about |
$ | $ |
/$ | $ |
/$/ | $ |
라우트 트리 수동 구성
코드로 라우트 트리를 만들 때 각 라우트의 부모 라우트를 정의하는 것만으로는 충분하지 않습니다. 각 라우트를 부모 라우트의 children 배열에 추가해 최종 라우트 트리도 구성해야 합니다. 파일 기반 라우팅과 달리 라우트 트리가 자동으로 만들어지지 않기 때문입니다.
/* prettier-ignore */
const routeTree = rootRoute.addChildren([
indexRoute,
aboutRoute,
postsRoute.addChildren([
postsIndexRoute,
postRoute,
]),
postEditorRoute,
settingsRoute.addChildren([
profileRoute,
notificationsRoute,
]),
pathlessLayoutRoute.addChildren([
pathlessLayoutARoute,
pathlessLayoutBRoute,
]),
filesRoute.addChildren([
fileRoute,
]),
])
/* prettier-ignore-end */
하지만 라우트 트리를 구성하기 전에 코드 기반 라우팅의 라우팅 개념이 어떻게 작동하는지 이해해야 합니다.
코드 기반 라우팅의 라우팅 개념
믿기 어려울 수 있지만 파일 기반 라우팅은 실제로 코드 기반 라우팅의 상위 집합이며, 파일 시스템과 그 위에 얹은 약간의 코드 생성 추상화를 사용해 위에서 본 구조를 자동으로 생성합니다.
라우팅 개념 가이드를 읽고 다음 주요 개념을 각각 잘 알고 있다고 가정합니다.
- 루트 라우트
- 기본 라우트
- 인덱스 라우트
- 동적 라우트 세그먼트
- 스플랫 / 캐치올 라우트
- 레이아웃 라우트
- 경로 없는 라우트
- 비중첩 라우트
이제 이러한 라우트 타입을 코드로 각각 만드는 방법을 살펴보겠습니다.
루트 라우트
코드 기반 라우팅에서 루트 라우트를 만드는 방법은 다행히 파일 기반 라우팅과 동일합니다. createRootRoute() 함수를 호출합니다.
하지만 파일 기반 라우팅과 달리 원하지 않는다면 루트 라우트를 export하지 않아도 됩니다. 전체 라우트 트리와 애플리케이션을 하나의 파일에 구성하는 것은 권장하지 않습니다(가능은 하며, 라우팅 개념을 간단히 보여주기 위해 예제에서는 이렇게 구성합니다).
React
// Standard root route
import { createRootRoute } from '@tanstack/react-router'
const rootRoute = createRootRoute()
// Root route with Context
import { createRootRouteWithContext } from '@tanstack/react-router'
import type { QueryClient } from '@tanstack/react-query'
export interface MyRouterContext {
queryClient: QueryClient
}
const rootRoute = createRootRouteWithContext<MyRouterContext>()
Solid
// Standard root route
import { createRootRoute } from '@tanstack/solid-router'
const rootRoute = createRootRoute()
// Root route with Context
import { createRootRouteWithContext } from '@tanstack/solid-router'
import type { QueryClient } from '@tanstack/solid-query'
export interface MyRouterContext {
queryClient: QueryClient
}
const rootRoute = createRootRouteWithContext<MyRouterContext>()
TanStack Router의 Context에 대해 자세히 알아보려면 Router Context 가이드를 참고합니다.
기본 라우트
기본 라우트를 만들려면 createRoute 함수에 일반 path 문자열을 제공하면 됩니다.
const aboutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'about',
})
간단합니다! aboutRoute는 URL /about과 매칭됩니다.
인덱스 라우트
index 라우트를 나타낼 때 index 파일 이름을 사용하는 파일 기반 라우팅과 달리, 코드 기반 라우팅에서는 슬래시 하나 /를 사용합니다. 예를 들어 위의 예제 라우트 트리에 있는 posts.index.tsx 파일은 코드 기반 라우팅에서 다음과 같이 나타냅니다.
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
// Notice the single slash `/` here
path: '/',
})
따라서 postsIndexRoute는 URL /posts/(또는 /posts)와 매칭됩니다.
동적 라우트 세그먼트
동적 라우트 세그먼트는 코드 기반 라우팅에서도 파일 기반 라우팅과 정확히 동일하게 작동합니다. 경로의 세그먼트 앞에 $를 붙이면 라우트의 loader 또는 component의 params 객체에 캡처됩니다.
const postIdRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
// In a loader
loader: ({ params }) => fetchPost(params.postId),
// Or in a component
component: PostComponent,
})
function PostComponent() {
const { postId } = postIdRoute.useParams()
return <div>Post ID: {postId}</div>
}
[!TIP] 컴포넌트가 코드 분할되어 있다면 getRouteApi 함수를 사용해 타입이 지정된
useParams()훅에 액세스하기 위해postIdRoute구성을 import하지 않아도 됩니다.
스플랫 / 캐치올 라우트
예상대로 스플랫/캐치올 라우트도 코드 기반 라우팅에서 파일 기반 라우팅과 동일하게 작동합니다. 경로의 세그먼트 앞에 $를 붙이면 _splat 키 아래의 params 객체에 캡처됩니다.
const filesRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'files',
})
const fileRoute = createRoute({
getParentRoute: () => filesRoute,
path: '$',
})
URL /documents/hello-world의 경우 params 객체는 다음과 같습니다.
{
'_splat': 'documents/hello-world'
}
레이아웃 라우트
레이아웃 라우트는 children을 레이아웃 컴포넌트로 감싸는 라우트입니다. 코드 기반 라우팅에서는 한 라우트 아래에 다른 라우트를 중첩해 간단히 레이아웃 라우트를 만들 수 있습니다.
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
component: PostsLayoutComponent, // The layout component
})
function PostsLayoutComponent() {
return (
<div>
<h1>Posts</h1>
<Outlet />
</div>
)
}
const postsIndexRoute = createRoute({
getParentRoute: () => postsRoute,
path: '/',
})
const postsCreateRoute = createRoute({
getParentRoute: () => postsRoute,
path: 'create',
})
const routeTree = rootRoute.addChildren([
// The postsRoute is the layout route
// Its children will be nested under the PostsLayoutComponent
postsRoute.addChildren([postsIndexRoute, postsCreateRoute]),
])
이제 postsIndexRoute와 postsCreateRoute는 모두 PostsLayoutComponent 내부에서 콘텐츠를 렌더링합니다.
// URL: /posts
<PostsLayoutComponent>
<PostsIndexComponent />
</PostsLayoutComponent>
// URL: /posts/create
<PostsLayoutComponent>
<PostsCreateComponent />
</PostsLayoutComponent>
경로 없는 레이아웃 라우트
파일 기반 라우팅에서는 경로 없는 레이아웃 라우트 앞에 _를 붙이지만, 코드 기반 라우팅에서는 path 옵션 대신 id가 있는 라우트일 뿐입니다. 코드 기반 라우팅은 파일 시스템으로 라우트를 구성하지 않으므로 경로가 없음을 나타내기 위해 라우트 앞에 _를 붙일 필요가 없습니다.
const pathlessLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'pathlessLayout',
component: PathlessLayoutComponent,
})
function PathlessLayoutComponent() {
return (
<div>
<h1>Pathless Layout</h1>
<Outlet />
</div>
)
}
const pathlessLayoutARoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-a',
})
const pathlessLayoutBRoute = createRoute({
getParentRoute: () => pathlessLayoutRoute,
path: 'route-b',
})
const routeTree = rootRoute.addChildren([
// The pathless layout route has no path, only an id
// So its children will be nested under the pathless layout route
pathlessLayoutRoute.addChildren([pathlessLayoutARoute, pathlessLayoutBRoute]),
])
이제 /route-a와 /route-b는 모두 PathlessLayoutComponent 내부에서 콘텐츠를 렌더링합니다.
// URL: /route-a
<PathlessLayoutComponent>
<RouteAComponent />
</PathlessLayoutComponent>
// URL: /route-b
<PathlessLayoutComponent>
<RouteBComponent />
</PathlessLayoutComponent>
비중첩 라우트
코드 기반 라우팅에서 비중첩 라우트를 만들 때는 경로 끝에 _를 사용할 필요가 없지만, 올바른 경로와 중첩 구조로 라우트와 라우트 트리를 구성해야 합니다. 게시물 편집기를 posts 라우트 아래에 중첩하지 않으려는 라우트 트리를 살펴보겠습니다.
/posts_/$postId/edit/posts$postId
이를 위해 게시물 편집기용 별도 라우트를 만들고, 라우트를 중첩하려는 위치(이 경우 루트)부터 전체 경로를 path 옵션에 포함해야 합니다.
// The posts editor route is nested under the root route
const postEditorRoute = createRoute({
getParentRoute: () => rootRoute,
// The path includes the entire path we need to match
path: 'posts/$postId/edit',
})
const postsRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
})
const postRoute = createRoute({
getParentRoute: () => postsRoute,
path: '$postId',
})
const routeTree = rootRoute.addChildren([
// The post editor route is nested under the root route
postEditorRoute,
postsRoute.addChildren([postRoute]),
])