Docs
Guide
Type Safety

Type Safety

TanStack Router는 TypeScript 컴파일러와 런타임의 한계 내에서 최대한 타입 안전성을 제공하도록 설계되었습니다. 이는 TanStack Router가 TypeScript로 작성되었을 뿐만 아니라 제공된 타입을 완전히 추론하고 이를 라우팅 전체 경험에 걸쳐 지속적으로 전달한다는 것을 의미합니다.

결과적으로, 개발자가 작성해야 할 타입이 줄어들고, 애플리케이션이 진화하면서도 코드에 대한 신뢰도가 높아집니다.

Route Definitions

File-based Routing

라우트는 계층적이며, 정의도 마찬가지입니다. 파일 기반 라우팅을 사용하는 경우, 대부분의 타입 안전성이 이미 보장됩니다.

Code-based Routing

Route 클래스를 직접 사용하는 경우, RoutegetParentRoute 옵션을 사용하여 라우트가 올바르게 타입 지정되도록 보장해야 합니다. 이는 하위 라우트가 모든 상위 라우트의 타입을 알아야 하기 때문입니다. 그렇지 않으면, 상위 레이아웃 라우트에서 파싱한 중요한 검색 파라미터가 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.ensureQueryDataloader에서 호출할 수 있습니다.

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}>
)

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} />;
};

이 방법은 컴포넌트에서 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는 정확히 탐색하려는 라우트로 좁혀집니다.