Tanstack Route 동적(dynamic) 라우팅 사용법 및 가이드

Tanstack Route의 동적(dynamic) 라우팅 사용법과 가이드를 알아봅니다.


소개

Tanstack Route는 동적(dynamic) 라우팅을 지원합니다.

이전 글에서는 Tanstack Route의 기본 사용법에 대해 알아보았습니다.

이번 글에서는 동적(dynamic) 라우팅 방식에 대해 알아보겠습니다.

만약, Tanstack Route의 기본 사용법에 대해 모른다면 이전 글을 먼저 읽어주세요.

`Tanstack Route 기본 사용법`

목차

  1. 폴더 구조
  2. useParams 훅을 사용하여 파라미터 확인하기
  3. 동적 페이지로 이동하기
  4. Search Params 사용하기
  5. 데이터 로드 후 동적 페이지에 표시하기
  6. getRouteApi 함수를 사용하여 컴포넌트에서 데이터 로드하기
  7. pendingComponent 옵션을 사용하여 로딩 중일 때 표시할 컴포넌트 설정하기

폴더 구조

다이나믹 라우트를 사용하기 위해 다음과 같은 폴더 구조에 대한 예시를 살펴보겠습니다.

src
├── ...
├── routes/
│   ├── posts/
│   │   ├── index.tsx
│   │   └── $postId.tsx // 동적 라우팅
│   └── ...

tanstack route는 동적인 라우팅을 사용하기 위해 $ 기호를 사용합니다.

예를 들어, $postId.tsx 파일은 /posts/$postId 경로로 접근할 수 있습니다.

경로 예시) http://localhost:5173/posts/1

우선 위와 같은 폴더 구조를 만들어줍니다.

posts 폴더 안에 index.tsx 파일과 $postId.tsx 파일을 만들어줍니다.

폴더에 파일을 만들어주면 코드는 이전 글 설명과 같이 자동으로 생성됩니다.

src/routes/posts/index.tsx
// posts 폴더 안에 있는 index.tsx 파일입니다.
import { createFileRoute } from "@tanstack/react-router";
 
export const Route = createFileRoute("/posts/")({
  component: RouteComponent,
});
 
function RouteComponent() {
  return "Hello /posts/!";
}
src/routes/posts/$postId.tsx
// posts 폴더 안에 있는 $postId.tsx 파일입니다.
import { createFileRoute } from "@tanstack/react-router";
 
export const Route = createFileRoute("/posts/$postId")({
  component: RouteComponent,
});
 
function RouteComponent() {
  return "Hello /posts/$postId!";
}
참고: 파일 이름에 `$` 기호를 사용하면 동적 라우팅으로 인식됩니다.

위와 같은 방식 이외 폴더를 만들어 동적 라우팅을 사용할 수 있습니다.

이전 글에서 설명한 바와 같이, 폴더 이름을 $ 기호를 사용하여 동적 라우팅으로 인식할 수 있습니다.

다음은 폴더를 사용한 다이나믹 라우팅의 예시입니다.

이름 충돌을 피하기 위해 이번 예시에서는 posts 폴더 이름을 news로 사용하였습니다.

src
├── ...
├── routes/
│   ├── news/
│   │   └── $newsId/ // 동적 라우팅
│   │       └── index.tsx // 폴더 라우팅이면 반드시 있어야 합니다.
│   └── ...

위와 같은 폴더 구조에서 $newsId 폴더는 /news/$newsId 경로로 접근할 수 있습니다.

경로 예시) http://localhost:5173/news/1

단, 위 예시 폴더를 보면 news/ 폴더 안에 index.tsx 파일이 없습니다.

/news 경로로 접근하면 Not Found 에러를 반환합니다.

만약 /news 경로로 접근하면 반드시 news/ 폴더 안에 index.tsx 파일이 있어야 합니다.

src
├── ...
├── routes/
│   ├── news/
│   │   └── index.tsx // news 페이지의 메인 페이지
│   │   └── $newsId/
│   │       └── index.tsx // news 페이지의 동적 페이지
│   └── ...

다이나믹 라우트를 위해 두가지 방식의 폴더 & 파일 구조를 사용할 수 있습니다.


tanstack route는 기본적으로 제공하지 않는 경로에 접근하면 404 (Not Found) 에러를 반환합니다.

$postId.tsx 파일은 /posts/$postId 경로로 접근할 수 있습니다.

만약, /posts/1 경로로 접근하면 Not Found 에러를 반환하지 않고 posts/[해당 postId] 경로로 접근합니다.

useParams 훅을 사용하여 현재 경로의 파라미터를 확인할 수 있습니다.

useParams 훅을 사용하여 파라미터 확인하기

src/routes/posts/$postId.tsx
import { createFileRoute, useParams } from "@tanstack/react-router";
 
export const Route = createFileRoute("/posts/$postId")({
  component: RouteComponent,
});
 
function RouteComponent() {
  const { postId } = useParams({ strict: false });
 
  return <h1>`Hello /posts/{postId}`</h1>;
}

이제, 예시) http://localhost:5173/posts/1 경로로 접근하면 Hello /posts/1 문자열을 반환합니다.

Tanstack Route - Dynamic Routing

Tanstack Route - Dynamic Routing

strict: false 엄격모드 해제 옵션을 사용하지 않는다면 Route.useParams()를 사용하여 경로의 파라미터를 확인할 수 있습니다.

src/routes/posts/$postId.tsx
//...
function RouteComponent() {
  const { postId } = Route.useParams();
 
  return <h1>`Hello /posts/{postId}`</h1>;
}

또한, createFileRoute 함수에서 loaderbeforeLoad 옵션을 사용하여 데이터를 로드할 수 있습니다.

이는 잠시 후 데이터를 로드하는 방법에 대해 알아보겠습니다.

더욱 자세한 내용은 다음 링크를 참고하세요.

path params 참고 링크: `Tanstack Route - Path Params`

이제 <Link> 컴포넌트를 사용하여 동적 페이지로 이동해 보겠습니다.

동적 페이지로 이동하기

posts 폴더 안에 index.tsx 파일을 열어줍니다.

src/routes/posts/index.tsx
//...
function RouteComponent() {
  return (
    <div>
      <h1>Posts</h1>
      <p>Post Page</p>
      <Link to="/posts/$postId" params={{ postId: "1" }}>
        Post 1
      </Link>
    </div>
  );
}

tanstack route에서 제공하는 <Link> 컴포넌트를 사용하여 동적 페이지로 이동할 수 있습니다.

to 속성에 이동할 경로를 입력하고, params 속성에 경로의 파라미터를 입력합니다.

/posts/$postId는 경로 이름 입니다. params 속성에 입력한 값은 경로 이름에 있는 $postId에 들어갑니다.

위 방법 외에 또한 함수를 사용하여 동적 페이지로 이동할 수 있습니다.

src/routes/posts/index.tsx
//...
function RouteComponent() {
  return (
    <Link to="/posts/$postId" params={(prev) => ({ ...prev, postId: "2" })}>
      Post 2
    </Link>
  );
}

React를 사용해보셨다면 prev 값을 ...prev 처럼 이전 값을 복사하는 것에 익숙할 것입니다.

이는 다른 경로의 이미 있는 값을 복사하여 사용할 때 유용합니다.

함수를 사용한 params를 사용하려면 반드시 ... 연산자를 사용해야 합니다.

이제 각 Link를 통해 해당 id를 가진 페이지로 이동할 수 있습니다.

다음은 useNavigate 훅을 사용하여 동적 페이지로 이동하는 방법에 대해 알아보겠습니다.

간혹, <Link> 컴포넌트를 사용하지 않고(예를들어, 버튼을 클릭하여 이동할 때) 동적 페이지로 이동해야 할 때가 있습니다.

이럴 때는 useNavigate 훅을 사용하여 동적 페이지로 이동할 수 있습니다.

다음은 버튼을 클릭하여 동적 페이지로 이동하는 예시입니다.

useNavigate 훅을 사용하여 동적 페이지로 이동하기

useNavigate훅은 새로운 경로로 이동할 때 사용합니다.

경로 이름, 검색 매개변수, 해시 등 위치 상태에 따라 이동할 수 있습니다.

다음은 useNavigate 훅을 사용하여 동적 페이지로 이동하는 예시입니다.

src/routes/posts/index.tsx
//...
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
 
export const Route = createFileRoute("/posts/")({
  component: RouteComponent,
});
 
function RouteComponent() {
  const navigate = useNavigate({ from: "/posts" });
 
  return (
    <button
      onClick={() => {
        navigate({ to: "$postId", params: { postId: "1" } });
      }}
    >
      Post 1
    </button>
  );
}

useNavigate 훅에 from 속성을 사용하여 이동할 경로를 지정합니다.

다음 navigate 함수에 to 속성을 사용하여 이동할 경로를 지정합니다.

params 속성을 사용하여 경로의 파라미터를 입력합니다.

이제 버튼을 클릭하면 해당 id를 가진 페이지로 이동합니다.

만약 useNavigate 훅에 from 속성을 사용하지 않으면 다음과 같이 navigate 함수에 to 속성을 사용하여 이동할 경로를 직접 입력해야 합니다.

src/routes/posts/index.tsx
//...
function RouteComponent() {
  const navigate = useNavigate();
 
  return (
    <button
      onClick={() =>
        navigate({ to: "/posts/$postId", params: { postId: "1" } })
      }
    >
      Post 1
    </button>
  );
}

이제 버튼을 클릭하면 해당 id를 가진 페이지로 이동합니다.

이렇게 만들어진 경로는 이전에 설명한 useParams 훅을 사용하여 파라미터를 확인할 수 있습니다. #useParams

Search Params 사용하기

이제 검색 매개변수에 대해 알아보겠습니다.

검색 매개변수는 경로 뒤에 ? 기호를 사용하여 표시합니다.

tanstack route에서 제공하는 <Link> 컴포넌트에서 search 속성을 사용하여 검색 매개변수를 사용할 수 있습니다.

다음은 검색 매개변수를 사용하는 예시입니다.

src/routes/posts/index.tsx
//...
function RouteComponent() {
  return (
    <Link to="/posts" search={{ page: 1 }}>
      Search 1
    </Link>
  );
}

search 속성에 검색 매개변수를 입력합니다.

Search 1 링크를 클릭하면 /posts?page=1 경로로 이동합니다.

만약 동적 페이지에서 search를 사용하려면 반드시 params 속성을 사용해야 합니다.

src/routes/posts/$postId.tsx
//...
function RouteComponent() {
  return (
    <Link to="/posts/$postId" params={{ postId: "1" }} search={{ page: 1 }}>
      Search 1
    </Link>
  );
}

이제 buttonuseNavigate 훅을 사용하여 동적 페이지로 이동하는 방법에 대해 알아보겠습니다.

src/routes/posts/index.tsx
//...
function RouteComponent() {
  const navigate = useNavigate();
 
  return (
    <button
      onClick={() => {
        navigate({
          to: "/posts",
          search: { page: 1 },
        });
      }}
    >
      Button Search 1
    </button>
  );
}

이제 버튼을 클릭하면 /posts?page=1 경로로 이동합니다.

page 검색 매개변수는 이전에 설명한 useParams 훅을 사용하여 확인할 수 있습니다.

방법은 useParams 훅을 사용하여 파라미터 확인하기에 설명한 방법과 비슷하나, 타입을 따로 지정해야 합니다.

useSearch 훅을 사용하여 검색 매개변수를 확인하는 방법에 대해 알아보겠습니다.

src/routes/posts/$postId.tsx
//...
function RouteComponent() {
  const { page } = Route.useSearch(); // 이렇게 사용하면 타입이 지정되지 않습니다.
 
  return <h1>`Search Page: {page}`</h1>;
}
src/routes/posts/$postId.tsx
interface PostSearchParams {
  page?: number;
}
 
export const Route = createFileRoute("/posts/")({
  validateSearch: (search: Record<string, unknown>): PostSearchParams => {
    return {
      page: search.page ? Number(search.page) : undefined,
    };
  },
  component: RouteComponent,
});
 
function RouteComponent() {
  const { page } = Route.useSearch();
 
  return <h1>`Search Page: {page}`</h1>;
}

useParams는 내부적으로 폴더 및 파일 이름을 타입으로 지정하지만, useSearch는 경우에 따라 설정이 다르기 때문에 명시적으로 타입을 지정해야 합니다.

tanstack route는 매우 엄격한 타입 체크를 하기 때문에 반드시 타입을 지정해야 합니다.

단, interface에서 optional을 사용하지 않으면 항상 값이 있는 상태이기 때문에, url은 항상 page의 값이 있는 상태입니다. ex) /posts?page=1

고로 ? 옵셔널을 사용하여 값이 없을 경우 posts 페이지로 이동 하도록 합니다. 이런 타입들을 사용해 Link의 타입 체크를 통과할 수 있습니다.

지금까지 동적 라우팅 및 paramssearch에 대해 알아보았습니다.

데이터 로드 후 동적 페이지에 표시하기

이제 데이터를 로드하여 동적 페이지에 표시하는 방법에 대해 알아보겠습니다.

데이터를 사용하기 위해 jsonplaceholder 사이트에서 제공하는 API를 사용하겠습니다.

`jsonplaceholder`

우선 services 폴더를 src 폴더 안에 만들어줍니다.

그리고 그 안에 api.ts 파일을 만들어줍니다.

api를 사용할 폴더 구조 및 파일 이름은 자유롭게 지정해도 됩니다.

axiosreact-query 등 기타 라이브러리를 사용해도 되지만 여기서는 fetch 함수를 사용하여 데이터를 로드하겠습니다.

src/services/api.ts
// jsonplaceholder API getPosts function
 
export interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}
 
export const getPosts = async (): Promise<Post[]> => {
  try {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts?_limit=10`
    );
    if (!response.ok) {
      throw new Error("데이터를 불러오는 데 실패하였습니다.");
    }
 
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
    return [];
  }
};

데이터가 많을 수 있으니 최대 10개만 불러오도록 설정하였습니다.

이제 posts 폴더 안에 index.tsx 파일에서 데이터를 로드하여 페이지에 표시하는 방법에 대해 알아보겠습니다.

tanstack route에서 제공하는 loader 옵션을 사용하여 데이터를 로드할 수 있습니다.

src/routes/posts/index.tsx
import { createFileRoute } from "@tanstack/react-router";
import { getPosts } from "../../services/api";
 
export const Route = createFileRoute("/posts/")({
  loader: async () => await getPosts(),
  component: RouteComponent,
});

이제 데이터를 로드하여 페이지에 표시하는 방법에 대해 알아보겠습니다.

loader 옵션에서 반환한 데이터는 RouteComponent 컴포넌트의 useLoaderData 훅을 사용하여 확인할 수 있습니다.

title을 가져와서 Linkbutton을 사용하여 동적 페이지로 이동해 보겠습니다.

src/routes/posts/index.tsx
//... createFileRoute 부분
 
function RouteComponent() {
  const navigate = useNavigate();
  const posts = Route.useLoaderData();
 
  return (
    <div>
      <h1>Posts</h1>
      <p>Post Page</p>
      <ul>
        {posts.map((post) => (
          <li
            key={post.id}
            style={{
              marginBottom: "1rem",
              marginTop: "1rem",
            }}
          >
            <p>
              {/* <button> 컴포넌트를 사용하여 다른 경로로 이동할 수 있습니다. */}
              <button
                onClick={() => {
                  navigate({
                    to: "/posts/$postId",
                    params: { postId: post.id.toString() },
                  });
                }}
              >
                {post.title}
              </button>{" "}
            </p>
            {/* <Link> 컴포넌트를 사용하여 다른 경로로 이동할 수 있습니다. */}
            <p>
              <Link to="/posts/$postId" params={{ postId: post.id.toString() }}>
                {post.title}
              </Link>
            </p>
          </li>
        ))}
      </ul>
    </div>
  );
}

이제 버튼 및 링크를 클릭하면 해당 id를 가진 페이지로 이동합니다.

여기서 잠깐, tanstack route에서 제공하는 getRouteApi에 대해서 알아봅시다.

getRouteApi 함수는 현재 경로에 대한 정보를 가져올 수 있습니다.

이를 통해 다른 컴포넌트에서 데이터를 로드할 수 있습니다.

예를 들어, /posts 페이지에서 loader 옵션을 사용하여 데이터를 로드하고, PostList 컴포넌트에서 해당 데이터를 사용할 수 있습니다.

getRouteApi 함수를 사용하여 컴포넌트에서 데이터 로드하기

src/components/PostList.tsx
import { getRouteApi } from "@tanstack/react-router";
 
const route = getRouteApi("/posts/");
 
const PostList = () => {
  const posts = route.useLoaderData();
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  );
};
 
export default PostList;

이제 /posts 경로에 PostList 컴포넌트를 사용하여 데이터를 로드하여 페이지에 표시합니다.

src/routes/posts/index.tsx
//... posts page
return <PostList />;

getRouteApiuseLoaderData 뿐만 아니라, 이전에 사용했던 useParams, useSearch 훅을 사용할 수 있습니다.

단, 사용하기 위해서는 해당 페이지에서 설정이 되어 있어야합니다.

다음은 useParams, useSearch 훅을 사용하는 예시 입니다.

src/components/PostList.tsx
import { getRouteApi } from "@tanstack/react-router";
const postRoute = getRouteApi("/posts/");
 
const PostList = () => {
  const posts = postRoute.useLoaderData();
  const params = postRoute.useParams();
  const search = postRoute.useSearch();
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  );
};
 
export default PostList;
단, `getRouteApi` 함수는 현재 경로에 대한 정보를 가져오기 때문에 다른 경로에서 사용할 수 없습니다.

이제 동적 라우트 /posts/$postId 페이지에서 개별적으로 데이터를 로드하여 페이지에 표시하는 방법에 대해 알아보겠습니다.

/posts/$postId 페이지에서 데이터를 로드하는 방법은 이전에 설명한 방법과 비슷합니다.

다만, loader 옵션을 사용하여 데이터를 로드합니다.

다음과 같이 id를 받아서 데이터를 가져오는 api가 있다고 가정합니다.

src/services/api.ts
export const getPost = async (id: number): Promise<Post> => {
  try {
    const response = await fetch(
      `https://jsonplaceholder.typicode.com/posts/${id}`
    );
    if (!response.ok) {
      throw new Error("데잍터를 불러오는데 실패했습니다.");
    }
 
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
    return {} as Post;
  }
};

/posts/$postId 페이지에서 데이터를 로드하는 방법은 다음과 같습니다.

src/routes/posts/$postId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { getPost } from "../../services/api";
 
export const Route = createFileRoute("/posts/$postId")({
  loader: async ({ params }) => await getPost(+params.postId), // 파라미터는 문자열 타입이기 때문에 받는 데이터가 숫자 타입이라면 숫자로 변환해야 합니다.
  component: RouteComponent,
});
 
function RouteComponent() {
  const post = Route.useLoaderData();
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
}

위와 같이 데이터를 로드하여 페이지에 표시할 수 있습니다.

데이터가 느릴땐 tanstack router에서 제공하는 pendingComponent 옵션을 사용하여 로딩 중일 때 표시할 컴포넌트를 설정할 수 있습니다.

pendingComponent 옵션을 사용하여 로딩 중일 때 표시할 컴포넌트 설정하기

src/routes/posts/$postId.tsx
// ...
export const Route = createFileRoute("/posts/$postId")({
  loader: async ({ params }) => {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    // 임시로 Promise를 사용하여 3초 뒤에 데이터를 가져오도록 설정하였습니다.
    return await getPost(+params.postId);
  },
  pendingComponent: () => <div>Loading...</div>,
  pendingMs: 500, // 기본적으로 1000ms 입니다. 별도의 설정을 할 수 있습니다.
  component: RouteComponent,
});
 
// ... RouteComponent 부분

createFileRoute 함수에서 pendingComponent 옵션을 사용하여 로딩 중일 때 표시할 컴포넌트를 설정할 수 있습니다.

pendingComponent는 데이터를 로드하는 기본 시간이 1000ms로 정해져있습니다.

1초 이내에 데이터를 로드하면 pendingComponent 컴포넌트는 표시되지 않습니다.

pendingMs 옵션을 사용하여 데이터를 가져올 지연 시간을 설정할 수 있습니다.

마무리

이제 tanstack route에서 제공하는 동적 라우팅, params, search, 그리고 데이터를 로드하는 방법에 대해 알아보았습니다.

다음 기회가 된다면 tanstack route에서 보호된 라우팅 및 더욱 다양한 기능에 대해 포스팅을 해보겠습니다.

해당 포스팅에서 사용한 코드는 다음 GitHub에서 확인할 수 있습니다.

`Tanstack Route Dynamic`

관련 태그