원문: https://frontendmasters.com/blog/introducing-tanstack-router/
TanStack Router는 매우 흥미로운 프로젝트입니다. 기본적으로 모든 기능을 갖춘 클라이언트 사이드 자바스크립트 애플리케이션 프레임워크입니다. 라우팅 트리의 모든 지점에서 중첩된 레이아웃과 효율적인 데이터 로딩 기능을 갖추며 고도화된 라우팅 및 탐색 시스템을 제공합니다. 무엇보다도 이 모든 기능을 타입 안정성을 보장하며 제공합니다.
더욱 흥미로운 점은 이 글을 쓰는 현재, Router에 서버 사이드 기능을 추가하여 풀스택 웹 애플리케이션을 구축할 수 있는 TanStack Start가 개발 중이라는 점입니다. Start는 여기서 다룰 TanStack Router에 서버 레이어를 적용하여 풀스택을 구축할 수 있도록 합니다. 아직 TanStack Router를 사용해 보지 않았다면 지금이 바로 Router에 대해 알아볼 수 있는 완벽한 시기입니다.
TanStack Router는 단순한 라우터 그 이상이며, 완전한 클라이언트 사이드 애플리케이션 프레임워크입니다. 따라서 글이 너무 길어지지 않도록 모든 내용을 다루지는 않겠습니다. 라우팅과 내비게이션으로 주제를 제한하도록 하겠습니다. Router의 타입 안정성을 고려하면 이 두 주제는 생각보다 큰 주제입니다.
TanStack Router 공식 문서와 빠른 시작 가이드가 있으며, 이 가이드에는 새로운 Router 프로젝트의 기본 구조를 생성해주는 유용한 도구도 포함되어 있습니다. 이 글에 사용된 저장소를 복제하여 따라하셔도 좋습니다.
Router의 기능과 작동 방식을 살펴보기 위해 Jira와 같은 작업 관리 시스템을 구축한다고 가정해 보겠습니다. 실제 Jira처럼 멋지게 보이거나 사용하기 좋게 만드는 데는 중점을 두지 않겠습니다. 우리의 목표는 유용한 웹 애플리케이션을 만드는 것이 아니라 Router가 무엇을 할 수 있는지 알아보는 것입니다.
라우팅, 레이아웃, 경로, 검색 파라미터는 물론 정적 타이핑도 함께 다룰 것입니다.
그럼 제일 첫 주제부터 시작하겠습니다.
아래는 Router에서 __root.tsx
라고 부르는 루트 레이아웃입니다. 자체 프로젝트를 생성해 글을 읽고 계시다면 이 파일은 routes
폴더 바로 아래에 있습니다.
import { createRootRoute, Link, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => {
return (
<>
<div>
<Link to="/">Home</Link>
<Link to="/tasks">Tasks</Link>
<Link to="/epics">Epics</Link>
</div>
<hr />
<div>
<Outlet />
</div>
</>
);
},
});
createRootRoute
함수는 이름 그대로 작동합니다. <Link/>
컴포넌트도 설명이 필요없습니다(링크를 만듭니다). Router는 현재 활성화된 링크에 active
클래스를 추가하여 그에 따라 스타일을 쉽게 지정할 수 있습니다(적절한 aria-current=“page”
속성/값도 추가됩니다). 마지막으로 <Outlet />
컴포넌트가 흥미로운데, 이 레이아웃의 '콘텐츠'를 렌더링할 위치를 Router에 알려줍니다.
npm run dev
로 앱을 실행할 수 있습니다. 터미널에서 실행 중인 localhost
의 포트를 확인해 보세요.
여기서 중요한 점은 dev
명령어가 경로의 변경 사항을 모니터링하고, 이를 반영하여 routeTree.gen.ts
파일을 자동으로 업데이트한다는 것입니다. 이는 경로에 대한 메타데이터를 동기화하여 정적 타입을 빌드할 수 있게 하여 경로 작업을 안전하게 할 수 있도록 도와줍니다. 데모 저장소에서 처음부터 빌드하는 경우, Link 태그에 아직 존재하지 않는 URL이 있기 때문에 일부 타입스크립트 오류가 나타나는 것을 확인하실 수 있습니다. 그렇습니다. TanStack Router는 라우팅 레벨에 타입스크립트를 깊게 통합하여, 심지어 Link 태그가 유효한 곳을 가리키고 있는지까지 검증합니다.
다시 한번 말씀드리지만, 아래 에러는 에디터 플러그인 때문이 아닙니다. 타입스크립트 통합 자체에서 오류가 발생하기 때문에 CI/CD 시스템에서도 동일하게 에러가 발생하게 됩니다.
src/routes/\_\_root.tsx:8:17 - error TS2322: Type '"/"' is not assignable to type '"." | ".." | undefined'.
<link to="/" className="[&.active]:font-bold" />
루트 페이지를 추가하는 것부터 시작해봅시다. Router에서는 라우트 트리의 어디에 있든(곧 설명하겠습니다), 루트 /
를 나타내기 위해 index.tsx
파일을 사용합니다. dev
명령어가 실행중이라고 가정하고 index.tsx
를 만들어보겠습니다. 다음과 같은 코드가 스캐폴딩 되는 것을 확인하실 수 있습니다.
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: () => <div>Hello /!</div>,
});
Next나 SvelteKit 같은 메타프레임워크에 익숙해져 있다면 그 보다는 조금 더 많은 보일러 플레이트가 필요합니다. 이러한 프레임워크에서는 리액트 컴포넌트를 export default
하거나 일반 Svelte 컴포넌트를 내보내기만 하면 바로 작동합니다. 하지만 TanStack Router에서는 createFileRoute
라는 함수를 호출하고 현재 경로를 전달해줘야 합니다.
Router의 타입 안정성을 위해 경로가 필요하지만 직접 관리할 필요는 없으니 걱정하지 마세요. dev 프로세스는 새 파일에 대해 이와 같은 코드를 스캐폴드할 뿐만 아니라 해당 경로 값을 동기화하여 유지합니다. 해당 경로를 다른 경로로 변경하고 파일을 저장하면 경로가 바로 변경됩니다. 또는 junk
라는 폴더를 만들어서 그 폴더로 드래그하면 경로가 “/junk/”
로 변경됩니다.
다음 콘텐츠를 추가해 봅시다(junk 폴더의 파일을 원래 위치로 되돌려 주세요).
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/")({
component: Index,
});
function Index() {
return (
<div>
<h3>Top level index page</h3>
</div>
);
}
최상위 인덱스 페이지에 있다는 것을 알려주는 것 뿐인 단순한 컴포넌트 입니다.
이제 실제 라우트를 만들어 봅시다. 루트 레이아웃에서는 작업(tasks)과 에픽(epics)을 처리하기 위한 경로를 만들고 싶다고 표시했습니다. 기본적으로 Router는 파일 기반 라우팅(file-based routing)을 사용하지만, 라우팅을 할 수 있는 두 가지 방법을 제공하며, 이 두 가지 방법은 혼합해서 사용할 수 있습니다(두 가지 방법을 모두 살펴보겠습니다). 파일을 탐색 하고자 하는 경로와 일치하는 폴더 안에 쌓을 수 있습니다. 또는 '플랫 라우트(flat routes)'를 사용하고 각 파일 이름에 경로 계층 구조를 점으로 구분하여 표시할 수 있습니다. 전자만 유용하다고 생각할 수도 있지만, 곧 후자의 장점도 확인해 보겠습니다.
재미 삼아 플랫 라우트부터 시작해 보겠습니다. tasks.index.tsx
파일을 만들어 보겠습니다. 이는 가상의 tasks
폴더 안에 index.tsx
를 만드는 것과 동일합니다. 콘텐츠를 표시하기 위해 몇 가지 기본 마크업을 추가하겠습니다(실제 할일 앱을 만드는 것이 아니라 Router의 작동 방식을 살펴보기 위한 것입니다).
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/tasks/")({
component: Index,
});
function Index() {
const tasks = [
{ id: "1", title: "Task 1" },
{ id: "2", title: "Task 2" },
{ id: "3", title: "Task 3" },
];
return (
<div>
<h3>Tasks page!</h3>
<div>
{tasks.map((t, idx) => (
<div key={idx}>
<div>{t.title}</div>
<Link to="/tasks/$taskId" params={{ taskId: t.id }}>
View
</Link>
<Link to="/tasks/$taskId/edit" params={{ taskId: t.id }}>
Edit
</Link>
</div>
))}
</div>
</div>
);
}
계속하기 전에 모든 tasks 경로에 대한 레이아웃 파일을 추가하여 /tasks
아래에 라우팅되는 모든 페이지에 표시될 몇 가지 공통 콘텐츠를 포함시켜 보겠습니다. tasks
폴더가 있다면 그 안에 route.tsx
파일을 넣으면 됩니다. 여기서는 대신 tasks.route.tsx
파일을 추가하겠습니다. 플랫 파일을 사용하므로 단순하게 tasks.tsx
로 지정해도 괜찮습니다. 하지만 저는 잠시 후 살펴볼 디렉터리 기반 파일과 일관성을 유지하는 것이 좋다고 생각하기 때문에 tasks.route.tsx
를 선호합니다.
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/tasks")({
component: () => (
<div>
Tasks layout <Outlet />
</div>
),
});
항상 그렇듯이 <Outlet />
을 잊지 마세요. <Outlet />
이 없으면 해당 경로의 실제 콘텐츠가 렌더링되지 않습니다.
다시 말해, xyz.route.tsx
는 전체 라우트에 렌더링하는 컴포넌트입니다. 본질적으로는 레이아웃이지만 Router는 이를 라우트라고 부릅니다. 그리고 xyz.index.tsx
는 xyz
의 개별 경로에 대한 파일입니다.
그 결과로 아래와 같이 렌더링됩니다. 살펴볼 내용은 많지 않지만 한 가지 흥미로운 변경 사항을 적용하기 전에 간단히 살펴보겠습니다.
제일 상위에는 루트 레이아웃의 내비게이션 링크를 확인할 수 있습니다. 그 아래에는 tasks 라우트 파일의 Tasks layout
이 표시됩니다(실제로는 이부분에 레이아웃이 들어갑니다). 그 아래에는 tasks 페이지의 콘텐츠가 있습니다.
tasks 인덱스 파일의 <Link>
태그는 우리가 어디로 향하고 있는지 알려주지만, 작업을 확인하고 편집할 수 있는 경로를 만들어 봅시다. /tasks/123
및 /tasks/123/edit
경로를 만들겠습니다. 여기서 123은 taskId
를 나타냅니다.
TanStack Router는 경로 내부의 변수를 경로 파라미터로 나타내며, 달러 기호로 시작하는 경로 세그먼트로 표시됩니다. 따라서 tasks.$taskId.index.tsx
와 tasks.$taskId.edit.tsx
를 추가하면 됩니다. 이에 따라 전자는 /tasks/123
로 라우팅되고 후자는 /tasks/123/edit
로 라우팅됩니다. tasks.$taskId.index.tsx
를 살펴보고 실제로 전달된 경로 파라미터를 어떻게 가져오는지 알아보겠습니다.
import { createFileRoute, Link } from "@tanstack/react-router";
export const Route = createFileRoute("/tasks/$taskId/")({
component: () => {
const { taskId } = Route.useParams();
return (
<div>
<div>
<Link to="/tasks">Back</Link>
</div>
<div>View task {taskId}</div>
</div>
);
},
});
Route 객체에 존재하는 Route.useParams()
객체는 파라미터를 반환합니다. 모든 라우팅 프레임워크에는 이와 비슷한 것이 있기 때문에 이 부분은 크게 흥미로운 부분은 아닙니다. 다른 프레임워크와 차별되는 포인트는 Route에서는 정적으로 타입이 지정되어 있다는 점입니다. Router는 해당 경로에 대해 어떤 파라미터가 존재하는지 바로 알 수 있습니다(잠시 후에 살펴보겠지만 경로 상위의 파라미터도 포함합니다). 즉, 자동 완성을 지원할 뿐만 아니라 잘못된 경로 파라미터를 입력했울 경우 타입스크립트 에러가 발생합니다.
라우트를 탐색하는 데 사용한 Link 태그에서도 이러한 사실을 확인할 수 있습니다.
<Link to="/tasks/$taskId" params={{ taskId: t.id }}>
여기서 파라미터를 생략했거나 taskId 이외의 다른 것을 지정했다면 오류가 발생합니다.
Router의 고급 라우팅 규칙에 대해 조금 더 자세히 알아보고 Router가 지원하는 몇 가지 멋진 기능에 대해 알아보겠습니다. 이 기능들은 일반적으로는 사용하지 않는 고급 기능이지만, 이런 기능이 있다는 것을 안다면 분명 도움이 될 것입니다.
edit task 라우트는 경로가 다르다는 점과 "View" 대신 “Edit”이라는 텍스트를 넣었다는 점을 제외하면 본질적으로 동일합니다. 이 경로를 사용하여 아직 보지 못한 TanStack Router 기능을 살펴보겠습니다.
개념적으로는 URL 경로와 컴포넌트 트리라는 두 가지 계층 구조가 있습니다. 지금까지는 이 두 가지가 1:1로 정렬되어 있습니다.
URL 경로는 다음과 같습니다.
/tasks/123/edit
렌더링은 다음 순서로 발생합니다.
root route -> tasks route layout -> edit task path
URL 계층 구조와 컴포넌트 계층 구조가 완벽하게 정렬되어 있습니다. 하지만 꼭 그럴 필요는 없습니다.
재미를 위해 edit task 라우트에서 기본 tasks 레이아웃 파일을 제거하는 방법을 살펴봅시다. 즉, /tasks/123/edit
URL이 동일한 내용을 렌더링하되 tasks.route.tsx
라우팅 파일은 렌더링하지 않기를 원합니다. 이렇게 하려면 tasks.$taskId.edit.tsx
의 이름을 tasks_.$taskId.edit.tsx
로 바꾸기만 하면 됩니다.
tasks
가 tasks_
가 되었습니다. edit은 tasks
안에 위치하므로 Router는 URL을 기반으로 edit.tsx
파일을 찾을 수 있습니다. 하지만 이름을 tasks_
로 지정했기 때문에 tasks
가 URL에 있더라도 렌더링된 컴포넌트 트리에서 해당 컴포넌트를 제거합니다. 이제 edit task 라우트를 렌더링하면 다음과 같이 표시됩니다.
Tasks layout
이 사라진 것을 확인할 수 있습니다.
만약 반대로 하고 싶다면 어떻게 해야 할까요? 원하는 컴포넌트 계층 구조가 있는데, edit task 페이지에서 해당 레이아웃을 렌더링하고 싶지만 URL에는 영향을 미치지 않기를 원한다면 어떻게 해야 할까요? 밑줄()을 반대쪽에 붙히면 됩니다. task edit 페이지를 렌더링하지만 tasks 레이아웃 라우트를 컴포넌트 계층 구조에 넣지 않는 `tasks.$taskId.edit.tsx가 있습니다. 작업 편집에만 사용하려는 특별한 레이아웃이 있다고 가정해 봅시다.
_taskEdit.tsx`를 만들어 보겠습니다.
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/_taskEdit")({
component: () => (
<div>
Special Task Edit Layout <Outlet />
</div>
),
});
그런 다음 task edit 파일을 _taskEdit.tasks_.$taskId.edit.tsx
로 변경합니다. 이제 /tasks/1/edit
에 접근하면 커스텀 레이아웃이 있는 task edit 페이지가 표시됩니다(URL에는 영향을 미치지 않습니다).
다시 한 번 말하지만, 이것은 고급 기능입니다. 보통은 간단하고 예측 가능한 라우팅 규칙으로도 충분합니다. 하지만 이러한 고급 기능이 있다는 것을 아는 것은 분명 도움이 될 것입니다.
파일 이름에 점(.)을 사용해 계층구조를 넣는 대신, 디렉터리 구조를 이용할 수도 있습니다. 저는 보통 디렉터리를 선호하지만, 두 방식을 혼합하여 사용할 수 있습니다. 때로는 $pathParam.index.tsx
와 $pathParam.edit.tsx
와 같은 파일 쌍을 플랫 파일 이름으로 사용하는 것이 디렉토리 안에서 자연스러울 때도 있습니다. 일반적인 규칙은 그대로 적용되므로, 자신에게 가장 적합한 방식을 선택하면 됩니다.
디렉터리를 기준으로 앞서 말한 모든 것을 다시 설명하지는 않겠습니다. 완성된 결과물(GitHub에서 확인할 수 있습니다)만 살펴보겠습니다. epic들을 나열하는 epics
경로가 있습니다. 각 경로에서 epic을 편집하거나 볼 수 있습니다. epic을 볼 때 epic 안의 (정적인) 마일스톤 목록도 표시되며, 이 마일스톤 목록도 보거나 편집할 수 있습니다. 이전과 마찬가지로 마일스톤을 편집할 때는 마일스톤 route 레이아웃을 제거해보겠습니다.
이제 epics.index.tsx
와 epics.route.tsx
대신 epics/index.tsx와 epics/route.tsx가 있습니다. 다시 말하지만, 파일 이름의 점을 슬래시(및 디렉터리)로 바꾸었을 뿐 동일한 규칙입니다.
계속 진행하기 전에 잠시 $milestoneId.index.tsx
라우트를 살펴봅시다. 경로에 $milestoneId
가 있으므로 해당 경로 파라미터를 찾을 수 있습니다. 하지만 라우트 트리에서 더 위로 올라가 봅시다. 두 레이어 위에 $epicId
파라미터도 있습니다. Router는 이를 바로 알아차리고 두 파라미터가 모두 존재하는 것으로 타입을 설정합니다.
이 글의 화룡점정은 제 생각에 웹 개발에서 가장 성가신 부분 중 하나인 검색 파라미터(쿼리스트링이라고도 합니다)를 다루는 것입니다. 이는 기본적으로 URL에서 ?
뒤에 오는 것들입니다(/tasks?search=foo&status=open
). 이를 처리하는 데 기본이 되는 URLSearchParams
는 사용하기 까다로울 수 있으며, 다른 프레임워크에서도 일반적으로 타입이 지정되지 않은 속성을 제공하고 새롭고 업데이트된 쿼리 문자열 값으로 새 URL을 구성하는 데 최소한의 도움만 제공하는 등 그다지 좋은 성능을 제공하지 못합니다.
TanStack Router는 검색 파라미터를 관리할 수 있는 편리하면서도 모든 기능을 갖춘 메커니즘을 제공하며, 이 또한 타입 안정성을 보장합니다. 더 자세히 살펴보겠습니다. 여기서는 개략적으로 살펴볼 예정이므로 전체 문서는 여기에서 참고해주세요.
/epics/$epicId/milestones
경로에 대한 검색 파라미터 지원을 추가하겠습니다. 사용자가 특정 에픽에서 마일스톤을 검색할 수 있도록 검색 파라미터에 다양한 값을 허용할 것입니다. 이제 우리는 createFileRoute
함수를 수없이 많이 보았습니다. 보통은 컴포넌트
를 전달하기만 하면 됩니다.
export const Route = createFileRoute("/epics/$epicId/milestones/")({
component: ({}) => {
// ...
```
createFileRoute
는 많은 함수를 지원합니다. 검색 파라미터의 경우 validateSearch
가 필요합니다. Router에 이 라우트가 어떤 검색 파라미터를 지원하고 현재 URL에서 어떻게 유효성을 검사하는지 알려줄 수 있는 기회입니다. 사용자는 설정한 타입스크립트 유형에 관계없이 원하는 것을 자유롭게 URL에 입력할 수 있습니다. 유효하지 않을 수 있는 값을 유효한 값으로 변환하는 것은 여러분의 몫입니다.
먼저 검색 파라미터의 타입을 정의해 보겠습니다.
type SearchParams = {
page: number;
search: string;
tags: string[];
};
이제 validateSearch
메서드를 구현해 보겠습니다. 이 메서드는 URL에 있는 모든 것을 나타내는 Record<string, unknown>
을 입력받고, 이로부터 타입과 일치하는 것을 반환합니다. 한번 살펴보겠습니다.
export const Route = createFileRoute("/epics/$epicId/milestones/")({
validateSearch(search: Record<string, unknown>): SearchParams {
return {
page: Number(search.page ?? "1") ?? 1,
search: (search.search as string) || "",
tags: Array.isArray(search.tags) ? search.tags : [],
};
},
component: ({}) => {
```
URLSearchParams
와 달리 문자열 값으로만 제한되지 않는다는 점을 확인하실 수 있습니다. 객체나 배열을 넣을 수 있으며, TanStack이 이를 직렬화하고 직렬화 해제하는 작업을 대신 수행합니다. 뿐만 아니라 사용자 정의 직렬화 메커니즘을 지정할 수도 있습니다.
특히 프로덕션 애플리케이션의 경우 Zod와 같은 보다 엄격한 유효성 검사 메커니즘을 사용하고 싶을 수 있습니다. 따라서 Router에서는 Zod를 포함해 바로 사용할 수 있는 여러 가지 어댑터를 제공합니다. 여기에서 검색 파라미터에 대한 문서를 확인하세요.
검색 파라미터 없이 아래 경로를 수동으로 접근하고 어떤 일이 발생하는지 살펴봅시다.
http://localhost:5173/epics/1/milestones
Router는 아래 경로로 변경합니다(리디렉션 하는 것은 아닙니다).
http://localhost:5173/epics/1/milestones?page=1&search=&tags=%5B%5D
TanStack은 유효성 검사 기능을 실행한 다음 URL을 올바르고 유효한 검색 파라미터로 대체했습니다. URL이 이렇게 “못생기게” 표시되는 방식이 마음에 들지 않으시면 계속 지켜봐 주세요. 해결 방법이 있습니다. 하지만 먼저 우리가 알고 있는 것으로 작업해 봅시다.
우리는 Route.useParams
메서드를 여러 번 사용해 왔습니다. 검색 파라미터에 대해 동일한 작업을 수행하는 Route.useSearch
도 있습니다. 하지만 조금 다르게 해보겠습니다. 이전에는 모든 것을 동일한 경로 파일에 넣었기 때문에 동일한 렉시컬 스코프에서 Route 객체를 직접 참조할 수 있었습니다. 이제 별도의 컴포넌트를 사용하여 검색 파라미터를 읽고 업데이트 해보겠습니다.
MilestoneSearch.tsx
컴포넌트를 추가했습니다. 라우트 파일에서 Route
객체를 가져오면 된다고 생각할 수도 있습니다. 하지만 그건 위험합니다. 순환 종속성을 생성하여 번들러에 따라 작동할 수도 있고 작동하지 않을 수도 있는 코드가 만들어질 수 있습니다. 만약 "작동"하더라도 숨겨진 문제가 숨어 있을 수 있습니다.
다행히 Router는 이 문제를 처리하기 위해 @tanstack/react-router
에서 getRouteApi
라는 직접 API를 제공합니다. Router에 (정적으로 입력된) 경로를 전달하면 올바른 경로 객체를 반환합니다.
const route = getRouteApi("/epics/$epicId/milestones/");
이제 해당 route 객체에서 useSearch
를 호출하여 정적으로 입력된 결과를 가져올 수 있습니다.
폼 요소와 클릭 핸들러로 이러한 검색 매개변수의 새 값을 동기화하고 수집하는 부분은 여기서 다루지 않겠습니다. 새로운 값이 있다고 가정하고 어떻게 설정하는지 살펴봅시다. 이를 위해 useNavigate
훅을 사용할 수 있습니다.
const navigate = useNavigate({
from: "/epics/$epicId/milestones/",
});
이를 호출하여 어디에서 탐색 중인지 알려줍니다. 이 결과물을 사용하여 어디로 가고 싶은지(현재와 같은 위치)를 알려주고 새 검색 파라미터를 반환할 수 있는 search
함수가 주어집니다. 당연히 타입스크립트는 우리가 무언가를 놓치면 에러가 있다고 소리를 지를 것입니다. 편의를 위해 Router는 이 search 함수에 현재 값을 전달하므로 무언가를 쉽게 추가하거나 재정의할 수 있습니다. 따라서 페이지 업을 하려면 다음과 같이 하면 됩니다.
navigate({
to: ".",
search: (prev) => {
return { ...prev, page: prev.page + 1 };
},
});
물론 이 함수에는 params
프로퍼티가 있고 경로 파라미터가 있는 라우트로 이동하는 경우 꼭 명시해야 합니다(그렇지 않으면 항상 그렇듯이 타입스크립트가 사용자에게 에러가 있다고 소리를 지를 것입니다). 같은 장소로 가고 있기 때문에(useNavigate
의 from
으로 표시된 값과 to: "."
로 확인 할 수 있습니다) 여기서는 $epicId
경로 파라미터가 필요하지 않습니다. Router는 이미 있는 것을 그대로 유지한다는 것을 알고 있습니다.
검색 값과 태그를 설정하고 싶다면 아래와 같이 할 수 있습니다.
const newSearch = "Hello World";
const tags = ["tag 1", "tag 2"];
navigate({
to: ".",
search: (prev) => {
return { page: 1, search: newSearch, tags };
},
});
이렇게 하면 URL이 다음과 같이 표시됩니다.
/epics/1/milestones?page=1&search=Hello%20World&tags=%5B"tag%201"%2C"tag%202"%5D
다시 말하지만, 검색과 문자열 배열은 우리를 위해 직렬화되었습니다.
검색 파라미터가 있는 페이지로 연결하려면 Link 태그에 해당 검색 파라미터를 지정해주어야 합니다.
<Link
to="/epics/$epicId/milestones"
params={{ epicId }}
search={{ search: "", page: 1, tags: [] }}
>
View milestones
</Link>
그리고 언제나 그렇듯이 타입스크립트는 우리가 무언가를 빼놓으면 소리를 지를 것입니다. 강력한 타이핑은 좋은 것입니다.
앞서 살펴본 바와 같이 아래의 URL로 이동하면
http://localhost:5173/epics/1/milestones
URL은 아래와 같이 변경됩니다.
http://localhost:5173/epics/1/milestones?page=1&search=&tags=%5B%5D
페이지에는 항상 페이지, 검색 및 태그 값이 있다고 Router에 명시했기 때문에 이러한 모든 쿼리 파라미터를 갖게 됩니다. 최소한의 깔끔한 URL을 원하고 이러한 변환이 일어나지 않도록 하려면 몇 가지 옵션이 있습니다. 이 모든 값을 선택 사항으로 만들 수 있습니다. 자바스크립트 또는 타입스크립트에서는 값을 정의되지 않은 상태로 넘기면 값은 undefined
가 됩니다. 따라서 타입을 다음과 같이 변경할 수 있습니다.
type SearchParams = {
page: number | undefined;
search: string | undefined;
tags: string[] | undefined;
};
혹은 이렇게도 표현할 수 있습니다.
type SearchParams = Partial<{
page: number;
search: string;
tags: string[];
}>;
그런 다음 기본값 대신 undefined를 넣는 추가 작업을 수행합니다.
validateSearch(search: Record<string, unknown>): SearchParams {
const page = Number(search.page ?? "1") ?? 1;
const searchVal = (search.search as string) || "";
const tags = Array.isArray(search.tags) ? search.tags : [];
return {
page: page === 1 ? undefined : page,
search: searchVal || undefined,
tags: tags.length ? tags : undefined,
};
},
이제 이러한 값이 정의되지 않았을 수 있으므로 이러한 값을 사용할 때 복잡해집니다. 이제 멋지고 간단한 상위 페이지 호출은 다음과 같이 작성할 수 있습니다.
navigate({
to: ".",
search: (prev) => {
return { ...prev, page: (prev.page || 1) + 1 };
},
});
긍정적인 측면은 이제 URL에서 기본값이 있는 검색 파라미터가 생략되며, 이 페이지로 연결되는 <Link>
태그는 모두 선택 사항이므로 검색 값을 지정할 필요가 없다는 점입니다.
Router는 이 작업을 수행하는 다른 방법도 제공합니다. 현재 validateSearch
는 URL에 무엇이든 포함할 수 있으므로 유형이 지정되지 않은 Record<string, unknown
>만 허용합니다. 검색 파라미터의 “true” 타입은 이 함수에서 반환하는 값입니다. 반환 타입을 조정하는 것이 우리가 변경해 온 방식입니다.
하지만 Router에서는 다른 모드를 제공합니다. 전달된 검색 파라미터의 구조와, 유효성이 검사되고 최종화된 애플리케이션 코드에서 사용할 검색 파라미터의 타입을 모두 명시할 수 있습니다.
먼저 이러한 검색 파라미터에 대해 두 가지 타입을 지정해 보겠습니다.
type SearchParams = {
page: number;
search: string;
tags: string[];
};
type SearchParamsInput = Partial<{
page: number;
search: string;
tags: string[];
}>;
그 후 SearchSchemaInput
을 가져오겠습니다.
import { SearchSchemaInput } from "@tanstack/react-router";
SearchSchemaInput
은 Router에서 우리가 전달받을 검색 파라미터와 생성할 검색 파라미터의 차이를 전달하는 방법입니다. 다음과 같이 원하는 입력 타입과 이 유형을 교차하면 됩니다.
validateSearch(search: SearchParamsInput & SearchSchemaInput): SearchParams {
이제 실제 값을 생성하기 위해 이전에 수행했던 것과 동일한 원본 유효성 검사를 수행하면 됩니다. 이제 <Link>
태그가 있는 페이지로 이동하여 검색 파라미터를 하나도 지정하지 않아도 이전처럼 강력하게 타입 지정된 검색 파라미터 값을 생성하여 URL을 수정하지 않으면서 이를 허용할 수 있습니다.
URL을 업데이트할 때 단순히 이전 값과 설정 중인 값을 무조건적으로 모두 포함시킬 수는 없습니다. 이제 해당 파라미터에 값이 있으므로 URL에 업데이트됩니다. GitHub 리포지토리에는 이 두 번째 접근 방식은 feature/optional-search-params-v2 브랜치에서 확인할 수 있습니다.
사용 사례에 따라 가장 적합한 방법을 실험해보고 선택하세요.
TanStack Router는 매우 흥미로운 프로젝트입니다. 훌륭하게 만들어진 유연한 클라이언트 사이드 프레임워크로, 가까운 시일 내에 환상적인 서버 사이드 통합도 기대하고 있습니다.
여기서는 겨우 표면적인 부분만 소개했습니다. 타입 안정성을 갖춘 탐색, 레이아웃, 경로 파라미터 및 검색 파라미터의 기본 사항을 다루었지만, 특히 데이터 로딩과 곧 출시될 서버 통합과 관련하여 알아야 할 것이 훨씬 더 많습니다.
dsadadada