File base routing vs Confing base routing
최근 React Router Framework mode를 사용하여 프로젝트를 진행했습니다.
React Router에서 Route 정의는 기본적으로 routes.ts에 코드로 정의하지만 저는 `Next.js`를 썼던 입장에서 `File Route`가 편하여 `@react-router/fs-routes`를 사용하여 `File Route`로 Routes를 정의했습니다.
하지만 편안해서 사용했던 `File Route`에는 단점이 몇 가지 있었습니다.
- 개발자가 File Naming Convention을 숙지해야 함
- 생각보다 Routing 구조가 눈에 안 들어옴
- Route 변경 시 파일 이름을 변경해야 함, `config base routing`은 코드만 변경하면 됨
또 유튜브에 찾아보니 React Router Framework(Remix)의 `Brook께서 발표한 Why File-based Routing Sucks and Why You Should Use It이라는 영상도 봤습니다.
이분에 따르면 file routing의 장점은
- 시작하기 편안함
- 파일이름을 통해 URL에 어떻게 보일지 유추 가능
- 컨벤션이 이미 공식문서에 문서화되어 있음
- Automatic code splitting
또 단점은 다음과 같이 언급했습니다.
- 컨벤션이 정해져 있는 것은 좋지만, 이를 학습해야 함.
- Code Organization (이분의 의견에 따르면 한눈에 안 들온다고 할 수 있음. File Base Routing은 폴더를 타고, 또 타고 들어가야 함.
- 파일이름이 점점 많은 라우팅 기능을 포함함에 따라 복잡하고 이상해지고 있음
위의
이에 저는 지금까지 진행했던 프로젝트를 Config base routing으로 바꾸기로 마음먹었고, 마음먹은 김에 React Router의 문서를 정리하였습니다.
우선 가장 중요한 Routing을 정의하는 방법과 그 구성요소에 대해 정리해 보겠습니다.
Routing 정의 및 구성요소
Config file 작성
React Router Framework mode에서 routes 설정은 기본적으로 app/routes.ts파일에서 할 수 있습니다.
각각의 route는 URL 패턴과 그 URL 패턴에 매칭할 실제 route module을 필요로 합니다.
import { type RouteConfig, route, } from "@react-router/dev/routes";
export default [
route("some/path", "./some/file.tsx"),
// 패턴 ^ ^ route module (실제 파일)
] satisfies RouteConfig;
Route module
Route module에서는 action, loader, headers, error boundaries 같은 다양한 기능들을 정의할 수 있습니다.
// provides type safety/inference
import type { Route } from "./+types/team";
// provides `loaderData` to the component
export async function loader({ params }: Route.LoaderArgs) {
let team = await fetchTeam(params.teamId);
return { name: team.name };
}
// renders after the loader is done
export default function Component({
loaderData,
}: Route.ComponentProps) {
return <h1>{loaderData.name}</h1>;
}
Nested route
각각의 Route는 부모 Route에 nesting 될 수 있습니다.
import {
type RouteConfig,
route,
index,
} from "@react-router/dev/routes";
export default [
// parent route
route("dashboard", "./dashboard.tsx", [
// child routes
index("./home.tsx"),
route("settings", "./settings.tsx"),
]),
] satisfies RouteConfig;
위의 예시에서 부모의 path(dashboard)는 자동으로 자식 Route를 포함합니다.
따라서 위의 설정은 /dashboard와 /dashboard/settings에 대한 설정을 생성합니다.
Outlet
이렇게 Route가 nested 돼있을 경우 자식 Route는 Outlet 컴포넌트를 통해 렌더링 됩니다.
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* will either be home.tsx or settings.tsx */}
<Outlet />
</div>
);
}
Layout route
Layout Route는 자식에 대한 Layout을 생성합니다.
[[Nested Routes|nested route]]와 다른 점은 URL에 추가적인 segment를 생성하지 않습니다.
import {
type RouteConfig,
route,
layout,
index,
prefix,
} from "@react-router/dev/routes";
export default [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
] satisfies RouteConfig;
home.tsx와contact.tsx는 새로운 URL Path 생성 없이marketing/layout.tsx의 Nested route을 통해 렌더링 될 것입니다.project.tsx와edit-project.tsx는project-layout.tsx의 Nested route을 통해 렌더링 되고home.tsx는 그렇지 않습니다.
Route prefix
prefix를 사용하면 부모의 [[Route Modules|route file]]을 정의할 필요 없이 path에 prefix를 넣을 수 있습니다.
import {
type RouteConfig,
route,
layout,
index,
prefix,
} from "@react-router/dev/routes";
export default [
layout("./marketing/layout.tsx", [
index("./marketing/home.tsx"),
route("contact", "./marketing/contact.tsx"),
]),
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
] satisfies RouteConfig;
## Dynamic segment
>path segment가 `:`로 시작된다면 `dynamic segment`를 의미합니다. `route`와 URL이 매칭된다면, `dynamic segment`는 URL에서 파싱되어 다른 `router API`에게 `params`로 제공됩니다.
```typescript
route("teams/:teamId", "./team.tsx"),
import type { Route } from "./+types/team";
export async function loader({ params }: Route.LoaderArgs) {
// ^? { teamId: string }
}
export default function Component({
params,
}: Route.ComponentProps) {
params.teamId;
// ^ string
}
여러 개의 dynamic segments
route("c/:categoryId/p/:productId", "./product.tsx"),
import type { Route } from "./+types/product";
async function loader({ params }: LoaderArgs) {
// ^? { categoryId: string; productId: string }
}
Optional segment
optional segment를 segment 뒤에? 를 넣음으로써 만들 수 있습니다.
route(":lang?/categories", "./categories.tsx"),
route("users/:userId/edit?", "./user.tsx");
Splat
catchall 혹은 star로 불리는 segment를 만들 수 있습니다.
route path pattern이 /*로 끝나면 또 다른 /를 만날 때까지 어떤 문자열도 매칭됩니다.(심지어 /문자도 포함해서)
route("files/*", "./files.tsx"),
export async function loader({ params }: Route.LoaderArgs) {
// params["*"] will contain the remaining URL after files/
}
이때 params에서 desctruct를 할 수 있습니다.
const { "*": splat } = params;
또 어떠한 route에도 매칭되지 않는 요청을 다음과 같이 처리할 수 있습니다.
route("*", "./catchall.tsx"); // catcall route,
export function loader() {
throw new Response("Page not found", { status: 404 });
}
끝 맞히며
React Routing에서 가장 중요한 Routing 기능을 정리해 봤습니다.
이 외에도 Action, Loader 등 기능이 많지만 추후에 정리하도록 하겠습니다.
참고자료
https://reactrouter.com/start/framework/routing
Routing | React Router
reactrouter.com
https://www.youtube.com/watch?v=HR3Ufnr4HOc