본문으로 건너뛰기

타입 안전성

TanStack Router는 TypeScript 컴파일러와 런타임의 한계 안에서 최대한 타입 안전하도록 설계되었습니다. 이는 TypeScript로 작성되었을 뿐 아니라 제공된 타입을 완전히 추론하고 전체 라우팅 경험에 끈질기게 전달한다는 뜻입니다.

결국 개발자는 작성해야 할 타입이 줄어들고, 코드가 발전할수록 코드를 더 신뢰할 수 있게 됩니다.

라우트 정의

파일 기반 라우팅

라우트는 계층적이며 라우트 정의도 마찬가지입니다. 파일 기반 라우팅을 사용하면 타입 안전성의 많은 부분이 이미 자동으로 처리됩니다.

코드 기반 라우팅

Route 클래스를 직접 사용하는 경우 RoutegetParentRoute 옵션을 사용해 라우트에 올바른 타입을 지정하는 방법을 알아야 합니다. 자식 라우트는 부모 라우트 모두의 타입을 알아야 하기 때문입니다. 그렇지 않으면 3단계 위의 레이아웃경로 없는 레이아웃 라우트에서 파싱한 중요한 검색 매개변수가 JS의 무한 공백으로 사라집니다.

따라서 자식 라우트에 부모 라우트를 전달하는 것을 잊지 마세요!

const parentRoute = createRoute({
getParentRoute: () => parentRoute,
})

내보낸 훅, 컴포넌트 및 유틸리티

라우터의 타입이 Link, useNavigate, useParams 등의 최상위 내보내기와 작동하려면 TypeScript 모듈 경계를 통과해 라이브러리에 직접 등록되어야 합니다. 이를 위해 내보낸 Register 인터페이스에서 선언 병합을 사용합니다.

React

const router = createRouter({
// ...
})

declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}

Solid

const router = createRouter({
// ...
})

declare module '@tanstack/solid-router' {
interface Register {
router: typeof router
}
}

라우터를 모듈에 등록하면 이제 라우터의 정확한 타입과 함께 내보낸 훅, 컴포넌트 및 유틸리티를 사용할 수 있습니다.

컴포넌트 컨텍스트 문제 해결

컴포넌트 컨텍스트는 React 및 다른 프레임워크에서 컴포넌트에 의존성을 제공하는 훌륭한 도구입니다. 그러나 컴포넌트 계층을 이동하는 동안 컨텍스트의 타입이 변경되면 TypeScript가 변경 사항을 추론하는 방법을 알 수 없습니다. 이를 해결하기 위해 컨텍스트 기반 훅과 컴포넌트에는 사용 방법과 위치를 알려주는 힌트가 필요합니다.

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

function PostsComponent() {
// Each route has type-safe versions of most of the built-in hooks from TanStack Router
const params = Route.useParams()
const search = Route.useSearch()

// Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
// we must pass the `from` param to tell the hook our relative position in the route hierarchy.
const navigate = useNavigate({ from: Route.fullPath })
// ... etc
}

컨텍스트 힌트가 필요한 모든 훅과 컴포넌트에는 from 매개변수가 있으며, 현재 렌더링 중인 라우트의 ID 또는 경로를 전달할 수 있습니다.

🧠 팁: 컴포넌트가 코드 분할되어 있다면 getRouteApi 함수를 사용해 Route.fullPath를 전달하지 않고 타입이 지정된 useParams()useSearch() 훅에 접근할 수 있습니다.

라우트를 모르거나 공유 컴포넌트라면 어떻게 하나요?

from 속성은 선택 사항이므로 전달하지 않으면 라우터에서 사용 가능한 타입을 가장 잘 추측합니다. 일반적으로 라우터의 모든 라우트에 대한 모든 타입의 유니온을 얻게 됩니다.

잘못된 from 경로를 전달하면 어떻게 되나요?

TypeScript를 만족하지만 런타임에 실제로 렌더링 중인 라우트와 일치하지 않는 from을 전달하는 것도 기술적으로 가능합니다. 이 경우 from을 지원하는 각 훅과 컴포넌트가 예상과 실제로 렌더링 중인 라우트가 일치하지 않는지 감지하고 런타임 오류를 throw합니다.

라우트를 모르거나 공유 컴포넌트라서 from을 전달할 수 없으면 어떻게 하나요?

여러 라우트에서 공유되는 컴포넌트를 렌더링하거나 라우트 내부에 있지 않은 컴포넌트를 렌더링한다면 from 옵션 대신 strict: false를 전달할 수 있습니다. 이렇게 하면 런타임 오류를 숨길 뿐 아니라 호출할 수 있는 훅에 대해 완화되면서도 정확한 타입을 제공합니다. 공유 컴포넌트에서 useSearch를 호출하는 것이 좋은 예입니다.

function MyComponent() {
const search = useSearch({ strict: false })
}

이 경우 search 변수는 라우터의 모든 라우트에서 가능한 검색 매개변수의 유니온으로 타입이 지정됩니다.

라우터 컨텍스트

라우터 컨텍스트는 계층적 의존성 주입을 완성하는 매우 유용한 기능입니다. 라우터와 라우터가 렌더링하는 모든 라우트에 컨텍스트를 제공할 수 있습니다. 컨텍스트를 구성하면 TanStack Router가 라우트 계층에 따라 이를 병합하므로 각 라우트에서 모든 부모의 컨텍스트에 접근할 수 있습니다.

createRootRouteWithContext 팩토리는 인스턴스화된 타입으로 새 라우터를 생성합니다. 그러면 라우터에서 동일한 타입 계약을 충족해야 하며, 전체 라우트 트리에서 컨텍스트에 올바른 타입이 지정되도록 보장합니다.

const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
component: App,
})

const routeTree = rootRoute.addChildren([
// ... all child routes will have access to `whateverYouWant` in their context
])

const router = createRouter({
routeTree,
context: {
// This will be required to be passed now
whateverYouWant: true,
},
})

성능 권장 사항

애플리케이션이 커지면 TypeScript 검사 시간도 자연스럽게 증가합니다. 애플리케이션을 확장할 때 TS 검사 시간을 줄이려면 몇 가지 사항을 고려해야 합니다.

필요한 타입만 추론

클라이언트 측 데이터 캐시(TanStack Query 등)에서는 데이터를 프리페치하는 패턴이 유용합니다. 예를 들어 TanStack Query를 사용하면 loader에서 queryClient.ensureQueryData를 호출하는 라우트를 만들 수 있습니다.

export const Route = createFileRoute('/posts/$postId/deep')({
loader: ({ context: { queryClient }, params: { postId } }) =>
queryClient.ensureQueryData(postQueryOptions(postId)),
component: PostDeepComponent,
})

function PostDeepComponent() {
const params = Route.useParams()
const data = useSuspenseQuery(postQueryOptions(params.postId))

return <></>
}

이 방식은 문제가 없어 보이고 작은 라우트 트리에서는 TS 성능 문제를 느끼지 못할 수 있습니다. 그러나 이 경우 로더의 반환 타입이 라우트에서 전혀 사용되지 않더라도 TS가 이를 추론해야 합니다. 여러 라우트에서 이러한 방식으로 프리페치하고 로더 데이터가 복잡한 타입이라면 편집기 성능이 느려질 수 있습니다. 이 경우 변경은 간단하며 TypeScript가 Promise<void>를 추론하도록 하면 됩니다.

export const Route = createFileRoute('/posts/$postId/deep')({
loader: async ({ context: { queryClient }, params: { postId } }) => {
await queryClient.ensureQueryData(postQueryOptions(postId))
},
component: PostDeepComponent,
})

function PostDeepComponent() {
const params = Route.useParams()
const data = useSuspenseQuery(postQueryOptions(params.postId))

return <></>
}

이렇게 하면 로더 데이터가 추론되지 않고 라우트 트리 밖으로 추론이 이동해 useSuspenseQuery를 처음 사용하는 시점에 수행됩니다.

가능한 한 관련 라우트로 범위 좁히기

다음 Link 사용을 살펴보세요.

<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />

이 예시는 TS 성능에 좋지 않습니다. search가 모든 라우트의 모든 search 매개변수 유니온으로 해석되고 TS가 search prop에 전달하는 값을 이처럼 큰 유니온과 비교해 검사해야 하기 때문입니다. 애플리케이션이 커지면 이 검사 시간은 라우트 및 검색 매개변수 수에 비례해 증가합니다. 이 경우를 최적화하기 위해 최선을 다했지만(TypeScript는 일반적으로 이 작업을 한 번 수행하고 캐시함) 이 큰 유니온에 대한 초기 검사는 비용이 큽니다. 이는 paramsuseSearch, useParams, useNavigate 등의 다른 API에도 적용됩니다.

대신 from 또는 to를 사용해 관련 라우트로 범위를 좁혀야 합니다.

<Link from={Route.fullPath} to=".." search={{page: 0}} />
<Link from="/posts" to=".." search={{page: 0}} />

to 또는 from에 유니온을 전달해 관심 있는 라우트로 범위를 좁힐 수 있다는 점을 기억하세요.

const from: '/posts/$postId/deep' | '/posts/' = '/posts/'
<Link from={from} to='..' />

from에 브랜치를 전달해 search 또는 params가 해당 브랜치의 모든 하위 항목에서만 해석되도록 할 수도 있습니다.

const from = '/posts'
<Link from={from} to='..' />

/posts는 동일한 search 또는 params를 공유하는 많은 하위 항목을 가진 브랜치일 수 있습니다.

addChildren의 객체 구문 사용 고려

라우트에는 params, search, loaders 또는 context가 있는 것이 일반적이며, 이러한 값은 TS 추론에 부담을 주는 외부 의존성을 참조할 수도 있습니다. 이러한 애플리케이션에서는 객체를 사용해 라우트 트리를 만드는 편이 튜플보다 성능이 좋을 수 있습니다.

createChildren도 객체를 허용합니다. 복잡한 라우트와 외부 라이브러리를 포함한 큰 라우트 트리에서는 큰 튜플보다 객체를 TS가 훨씬 빠르게 타입 검사할 수 있습니다. 성능 향상 정도는 프로젝트, 외부 의존성, 해당 라이브러리의 타입 작성 방식에 따라 달라집니다.

const routeTree = rootRoute.addChildren({
postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
indexRoute,
})

이 구문은 더 장황하지만 TS 성능이 더 좋다는 점에 유의하세요. 파일 기반 라우팅에서는 라우트 트리가 자동으로 생성되므로 장황한 라우트 트리를 걱정할 필요가 없습니다.

범위를 좁히지 않은 내부 타입 피하기

노출된 타입을 재사용하고 싶을 수 있습니다. 예를 들어 다음과 같이 LinkProps를 사용하고 싶을 수 있습니다.

const props: LinkProps = {
to: '/posts/',
}

return (
<Link {...props}>
)

이는 TS 성능에 매우 좋지 않습니다. LinkProps에는 타입 인수가 없으므로 매우 큰 타입이 된다는 것이 문제입니다. 모든 search 매개변수의 유니온인 search와 모든 params의 유니온인 params가 포함됩니다. 이 객체를 Link와 병합할 때 이처럼 큰 타입의 구조적 비교를 수행합니다.

대신 as const satisfies를 사용해 정확한 타입을 추론하고 LinkProps를 직접 사용하지 않으면 큰 검사를 피할 수 있습니다.

const props = {
to: '/posts/',
} as const satisfies LinkProps

return (
<Link {...props}>
)

propsLinkProps 타입이 아니므로 타입이 훨씬 정확해 이 검사가 더 저렴합니다. LinkProps의 범위를 좁혀 타입 검사를 더 개선할 수도 있습니다.

const props = {
to: '/posts/',
} as const satisfies LinkProps<RegisteredRouter, string '/posts/'>

return (
<Link {...props}>
)

범위를 좁힌 LinkProps 타입과 비교해 검사하므로 더 빠릅니다.

이를 사용해 LinkProps 타입을 특정 타입으로 좁혀 prop이나 함수 매개변수로 사용할 수도 있습니다.

export const myLinkProps = [
{
to: '/posts',
},
{
to: '/posts/$postId',
params: { postId: 'postId' },
},
] as const satisfies ReadonlyArray<LinkProps>

export type MyLinkProps = (typeof myLinkProps)[number]

const MyComponent = (props: { linkProps: MyLinkProps }) => {
return <Link {...props.linkProps} />
}

MyLinkProps가 훨씬 정확한 타입이므로 컴포넌트에서 LinkProps를 직접 사용하는 것보다 빠릅니다.

또 다른 방법은 LinkProps를 사용하지 않고 제어의 역전을 제공해 특정 라우트로 범위가 좁혀진 Link 컴포넌트를 렌더링하는 것입니다. 렌더 props는 컴포넌트 사용자에게 제어를 역전하는 좋은 방법입니다.

export interface MyComponentProps {
readonly renderLink: () => React.ReactNode
}

const MyComponent = (props: MyComponentProps) => {
return <div>{props.renderLink()}</div>
}

const Page = () => {
return <MyComponent renderLink={() => <Link to="/absolute" />} />
}

이 예시는 내비게이션할 위치의 제어를 컴포넌트 사용자에게 역전했기 때문에 매우 빠릅니다. Link의 범위가 내비게이션하려는 정확한 라우트로 좁혀집니다.