Type Safety
TanStack Router는 TypeScript 컴파일러와 런타임의 한계 내에서 최대한 타입 안전성을 제공하도록 설계되었습니다. 이는 TanStack Router가 TypeScript로 작성되었을 뿐만 아니라 제공된 타입을 완전히 추론하고 이를 라우팅 전체 경험에 걸쳐 지속적으로 전달한다는 것을 의미합니다.
결과적으로, 개발자가 작성해야 할 타입이 줄어들고, 애플리케이션이 진화하면서도 코드에 대한 신뢰도가 높아집니다.
Route Definitions
File-based Routing
라우트는 계층적이며, 정의도 마찬가지입니다. 파일 기반 라우팅을 사용하는 경우, 대부분의 타입 안전성이 이미 보장됩니다.
Code-based Routing
Route
클래스를 직접 사용하는 경우, Route
의 getParentRoute
옵션을 사용하여 라우트가 올바르게 타입 지정되도록 보장해야 합니다. 이는 하위 라우트가 모든 상위 라우트의 타입을 알아야 하기 때문입니다. 그렇지 않으면, 상위 레이아웃 라우트에서 파싱한 중요한 검색 파라미터가 JavaScript void로 사라질 수 있습니다.
따라서, 하위 라우트에 상위 라우트를 전달하는 것을 잊지 마세요!
const parentRoute = createRoute({
getParentRoute: () => parentRoute,
});
Exported Hooks, Components, and Utilities
라우터의 타입이 Link
, useNavigate
, useParams
등과 같은 상위 레벨 내보내기와 작동하려면, TypeScript 모듈 경계를 넘어 라이브러리에 등록되어야 합니다. 이를 위해 내보낸 Register
인터페이스에서 선언 병합을 사용합니다.
const router = createRouter({
// ...
});
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
모듈에 라우터를 등록하면, 내보낸 훅, 컴포넌트 및 유틸리티를 라우터의 정확한 타입과 함께 사용할 수 있습니다.
Fixing the Component Context Problem
컴포넌트 컨텍스트는 React 및 다른 프레임워크에서 의존성을 컴포넌트에 제공하기 위한 훌륭한 도구입니다. 그러나 컨텍스트가 컴포넌트 계층을 따라 이동하면서 타입이 변경되면, TypeScript는 이러한 변경 사항을 추론할 수 없습니다. 이를 해결하기 위해 컨텍스트 기반의 훅과 컴포넌트는 어디에서 어떻게 사용되는지에 대한 힌트를 제공해야 합니다.
export const Route = createFileRoute("/posts")({
component: PostsComponent,
});
function PostsComponent() {
// 각 라우트는 TanStack Router의 대부분의 기본 제공 훅에 대해 타입 안전 버전을 제공합니다.
const params = Route.useParams();
const search = Route.useSearch();
// 일부 훅은 전체 라우터의 컨텍스트가 필요합니다. 여기서 타입 안전성을 유지하려면
// `from` 매개변수를 전달하여 라우트 계층에서의 상대적 위치를 알려야 합니다.
const navigate = useNavigate({ from: Route.fullPath });
// ... 기타
}
컨텍스트 힌트가 필요한 모든 훅과 컴포넌트에는 렌더링 중인 라우트의 ID 또는 경로를 전달할 수 있는 from
매개변수가 있습니다.
🧠 빠른 팁: 컴포넌트가 코드 분할된 경우, getRouteApi 함수를 사용하여
Route.fullPath
를 전달하지 않고도 타입이 지정된useParams()
및useSearch()
훅에 액세스할 수 있습니다.
What if I don't know the route? What if it's a shared component?
from
속성은 선택 사항입니다. 이를 전달하지 않으면 라우터는 사용 가능한 타입에 대해 최선의 추측을 제공합니다. 일반적으로 이는 라우터의 모든 라우트 타입의 유니온을 반환합니다.
What if I pass the wrong from
path?
TypeScript를 만족시키는 from
을 전달했지만, 런타임에서 실제 렌더링 중인 라우트와 일치하지 않을 가능성도 있습니다. 이 경우, from
을 지원하는 각 훅과 컴포넌트는 예상이 실제 라우트와 일치하지 않을 경우 이를 감지하고 런타임 오류를 발생시킵니다.
What if I don't know the route, or it's a shared component, and I can't pass from
?
여러 라우트에서 공유되는 컴포넌트를 렌더링하거나 라우트 외부에서 컴포넌트를 렌더링하는 경우, from
옵션 대신 strict: false
를 전달할 수 있습니다. 이 옵션은 런타임 오류를 무시할 뿐만 아니라 호출하려는 훅에 대해 유연하지만 정확한 타입을 제공합니다.
function MyComponent() {
const search = useSearch({ strict: false });
}
이 경우 search
변수는 라우터의 모든 라우트에서 가능한 검색 파라미터의 유니온으로 타입 지정됩니다.
Router Context
라우터 컨텍스트는 계층적 의존성 주입의 궁극적인 도구로 매우 유용합니다. 라우터와 렌더링되는 모든 라우트에 컨텍스트를 제공할 수 있습니다. 이 컨텍스트를 구축하면서 TanStack Router는 이를 라우트 계층 구조와 병합하여 각 라우트가 모든 상위 컨텍스트에 액세스할 수 있도록 합니다.
createRootRouteWithContext
팩토리는 새 라우터를 인스턴스화된 타입으로 생성하며, 동일한 타입 계약을 라우터에 충족하도록 요구하며, 전체 라우트 트리에서 컨텍스트가 올바르게 타입 지정되었는지 확인합니다.
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
component: App,
});
const routeTree = rootRoute.addChildren([
// ... 모든 하위 라우트는 컨텍스트에서 `whateverYouWant`에 액세스할 수 있습니다.
]);
const router = createRouter({
routeTree,
context: {
// 이제 이 값을 전달해야 합니다.
whateverYouWant: true,
},
});
Performance Recommendations
애플리케이션이 확장됨에 따라 TypeScript 검사 시간이 자연스럽게 증가합니다. 다음은 TypeScript 검사 시간을 줄이기 위해 고려해야 할 사항들입니다.
Only infer types you need
클라이언트 측 데이터 캐시(TanStack Query 등)를 사용해 데이터를 미리 가져오는 것이 좋은 패턴입니다. 예를 들어, TanStack Query에서는 라우트에서 queryClient.ensureQueryData
를 loader
에서 호출할 수 있습니다.
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 <></>;
}
이 방법은 작은 라우트 트리에서 문제가 없어 보일 수 있으며 TypeScript 성능 문제를 알아차리지 못할 수도 있습니다. 하지만 이 경우 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
를 처음 사용할 때 타입 추론이 라우트 트리 외부로 이동됩니다.
Narrow to relevant routes as much as you possibly can
다음과 같은 Link
사용 예를 고려해 보세요.
<Link to=".." search={{ page: 0 }} />
<Link to="." search={{ page: 0 }} />
이 예제는 TS 성능에 좋지 않습니다. 이는 search
가 모든 라우트의 search
파라미터의 유니온으로 해결되기 때문입니다. TS는 search
prop에 전달된 내용을 이 큰 유니온과 비교해야 합니다. 애플리케이션이 확장됨에 따라, 이 검사 시간은 라우트 및 검색 파라미터의 수에 비례하여 선형적으로 증가합니다. 이 문제는 params
와 같은 다른 API (useSearch
, useParams
, useNavigate
등)에도 적용됩니다.
대신 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
를 공유하는 여러 하위 라우트를 포함할 수 있는 브랜치일 수 있습니다.
Consider using the object syntax of addChildren
라우트는 일반적으로 params
, search
, loaders
, 또는 외부 종속성을 참조할 수 있는 context
를 포함합니다. 이러한 경우 객체 구문을 사용하여 라우트 트리를 생성하면 튜플보다 TS 성능이 더 좋아질 수 있습니다.
createChildren
도 객체를 허용합니다. 복잡한 라우트와 외부 라이브러리를 포함하는 큰 라우트 트리의 경우, 객체는 TS가 타입을 검사하는 데 있어 큰 튜플보다 훨씬 더 빠를 수 있습니다. 성능 향상은 프로젝트와 사용하는 외부 종속성, 그리고 해당 라이브러리 타입 작성 방식에 따라 다릅니다.
const routeTree = rootRoute.addChildren({
postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
indexRoute,
});
이 구문은 더 장황하지만 TS 성능이 더 좋습니다. 파일 기반 라우팅을 사용하는 경우, 라우트 트리는 자동으로 생성되므로 장황한 라우트 트리는 문제가 되지 않습니다.
Avoid internal types without narrowing
내보낸 타입을 재사용하고 싶을 수 있습니다. 예를 들어, 다음과 같이 LinkProps
를 사용하는 것이 유혹적일 수 있습니다.
const props: LinkProps = {
to: '/posts/',
}
return (
<Link {...props}>
)
이 방법은 TS 성능에 매우 나쁩니다. 문제는 LinkProps
에 타입 매개변수가 없으며, 따라서 매우 큰 타입이라는 것입니다. 여기에는 모든 search
파라미터의 유니온, 모든 params
의 유니온이 포함됩니다. 이 객체를 Link
와 병합하면 이 큰 타입의 구조적 비교가 이루어집니다.
대신, as const satisfies
를 사용하여 정밀한 타입을 추론하고, 직접적으로 LinkProps
를 사용하지 않아야 합니다.
const props = {
to: '/posts/',
} as const satisfies LinkProps
return (
<Link {...props}>
)
props
가 LinkProps
타입이 아니므로 이 검사는 더 저렴하며, 타입이 훨씬 더 정밀하기 때문입니다. 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} />;
};
이 방법은 컴포넌트에서 LinkProps
를 직접 사용하는 것보다 빠릅니다. MyLinkProps
가 훨씬 더 정밀한 타입이기 때문입니다.
또 다른 해결책은 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
는 정확히 탐색하려는 라우트로 좁혀집니다.