React Router는 다양한 라우팅 패턴을 지원하며, 주요 메서드는 다음과 같다:
route()
특정 경로에 대한 컴포넌트 연결index()
부모 경로의 기본 자식 라우트layout()
공통 레이아웃을 적용하는 라우트prefix()
여러 경로에 공통 접두사(prefix) 추가React Router v7에서는 app/routes.ts
파일에서 경로(route)를 설정한다.
route()
에는 두 파라미터가 있다. URL과 일치하는 URL 패턴과 동작을 정의하는 컴포넌트에 대한 파일 경로다.
// app/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("some/path", "./some/file.tsx"),
// 패턴 ^ ^ 파일경로
] satisfies RouteConfig;
// app/routes.ts
import {
type RouteConfig,
route,
index,
layout,
prefix,
} from "@react-router/dev/routes";
export default [
// 기본 경로 ('/')
index("./home.tsx"),
// '/about' 경로
route("about", "./about.tsx"),
// '/auth' 관련 경로 (레이아웃 적용)
layout("./auth/layout.tsx", [
route("login", "./auth/login.tsx"), // '/auth/login'
route("register", "./auth/register.tsx"), // '/auth/register'
]),
// '/concerts'로 시작하는 경로 (접두사 사용)
...prefix("concerts", [
index("./concerts/home.tsx"), // '/concerts'
route(":city", "./concerts/city.tsx"), // '/concerts/:city'
route("trending", "./concerts/trending.tsx"), // '/concerts/trending'
]),
] satisfies RouteConfig;
URL | 렌더링 컴포넌트 |
---|---|
/ | home.tsx |
/about | about.tsx |
/auth/login | auth/login.tsx (레이아웃 포함) |
/auth/register | auth/register.tsx (레이아웃 포함) |
/concerts | concerts/home.tsx |
/concerts/seoul | concerts/city.tsx (params.city = "seoul" ) |
/concerts/trending | concerts/trending.tsx |
📌 파일 기반 라우팅 (fs-routes)
@react-router/fs-routes
패키지를 사용하면 폴더 구조에 따라 자동으로 라우트를 구성할 수 있다.
// app/routes.ts
import { type RouteConfig, route } from "@react-router/dev/routes";
import { flatRoutes } from "@react-router/fs-routes";
export default [
route("/", "./home.tsx"), // 수동으로 정의한 기본 경로
...(await flatRoutes()), // 파일 시스템 기반 라우트 자동 생성
] satisfies RouteConfig;
routes.ts
에서 참조되는 파일은 각 경로의 동작을 정의한다.
// app/routes.ts
route("teams/:teamId", "./team.tsx"),
// route module ^^^^^^^^
// app/team.tsx
// 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>;
}
액션(actions), 헤더(headers), 에러 바운더리(error boundaries) 등 자세한 부분은 다음 포스트에서 다룬다.
중첩 라우트(Nested Routes)는 부모 라우트 내부에 자식 라우트를 포함하는 방식이다.
이를 통해 관련된 여러 페이지를 논리적으로 그룹화하고, 공통 UI를 유지하면서 개별 페이지를 동적으로 교체할 수 있다.
// app/routes.ts
import {
type RouteConfig,
route,
index,
} from "@react-router/dev/routes";
export default [
// 부모 라우트
route("dashboard", "./dashboard.tsx", [
// 자식 라우트
index("./home.tsx"), // "/dashboard"
route("settings", "./settings.tsx"), // "/dashboard/settings"
]),
] satisfies RouteConfig;
// app/dashboard.tsx
import { Outlet } from "react-router";
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* 자식 라우트(home.tsx 또는 settings.tsx)가 렌더링될 위치 */}
<Outlet />
</div>
);
}
<Outlet />
은 dashboard.tsx
내부에서 자식 라우트가 렌더링될 자리를 지정함/dashboard
일 때는 home.tsx
가 <Outlet />
에 렌더링됨/dashboard/settings
일 때는 settings.tsx
가 <Outlet />
에 렌더링됨routes.ts
의 모든 경로는 app/root.tsx
모듈 내부에 중첩되어 있다.
layout()
은 새로운 URL 세그먼트를 추가하지 않고, 자식 라우트를 감싸는 부모 역할을 한다.
// app/routes.ts
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"), // `/contact`
]),
// "projects"라는 경로를 가지는 하위 그룹을 생성
...prefix("projects", [
index("./projects/home.tsx"), // `/projects`
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"), // `/projects/:pid`
route(":pid/edit", "./projects/edit-project.tsx"), // `/projects/:pid/edit`
]),
]),
] satisfies RouteConfig;
위의 설정을 기준으로 경로와 렌더링 결과를 정리하면:
URL | 렌더링되는 파일 |
---|---|
/ | marketing/layout.tsx → marketing/home.tsx |
/contact | marketing/layout.tsx → marketing/contact.tsx |
/projects | projects/project-layout.tsx → projects/home.tsx |
/projects/:pid | projects/project-layout.tsx → projects/project.tsx |
/projects/:pid/edit | projects/project-layout.tsx → projects/edit-project.tsx |
✔ layout()
은 새로운 URL 세그먼트를 추가하지 않지만, 내부적으로 자식 라우트를 감싸는 부모 역할을 한다.
✔ /projects/:pid
경로로 접근하면, project-layout.tsx
가 먼저 렌더링되고, 그 안의 <Outlet />
을 통해 project.tsx
가 출력된다.
📌 layout()을 사용할 때 이 필요한 이유
layout()은 단독으로 화면을 렌더링하는 것이 아니라, 자식 라우트를 감싸는 부모 역할을 한다.
이를 위해 을 사용해야 한다.
// ./projects/project-layout.tsx
import { Outlet } from "react-router";
export default function ProjectLayout() {
return (
<div>
<aside>Example sidebar</aside> {/* 모든 하위 페이지에서 공통으로 표시 */}
<main>
<Outlet /> {/* 여기에 자식 라우트가 렌더링됨 */}
</main>
</div>
);
}
ProjectLayout
은 모든 projects 관련 페이지의 공통 레이아웃을 제공<Outlet />
을 사용하여, projects/:pid
, projects/:pid/edit
등의 자식 라우트가 동적으로 렌더링됨projects/:pid
로 접근하면, <Outlet />
에 project.tsx
가 렌더링됨projects/:pid/edit
로 접근하면, <Outlet />
에 edit-project.tsx
가 렌더링됨📌 layout()
vs route()
차이점**
구분 | layout() | route() |
---|---|---|
URL에 영향 | ❌ 없음 | ✅ 있음 |
공통 레이아웃 제공 | ✅ 가능 | ❌ 개별 페이지만 렌더링 |
<Outlet /> 사용 여부 | ✅ 필요 | ❌ 불필요 |
용도 | 여러 개의 페이지를 감싸는 부모 컴포넌트 | 단일 페이지 정의 |
✔ layout()
을 사용하면 여러 페이지에서 공통 레이아웃을 쉽게 유지할 수 있음
✔ route()
는 특정 경로에 해당하는 개별 페이지를 정의하는 용도
index()
는 부모 URL이 호출되었을 때 기본으로 렌더링될 컴포넌트를 지정한다.
자식 경로를 가질 수 없다. (즉, index() 내부에는 또 다른 route()를 추가할 수 없음)
// app/routes.ts
import {
type RouteConfig,
route,
index,
} from "@react-router/dev/routes";
export default [
// renders into the root.tsx Outlet at /
index("./home.tsx"),
route("dashboard", "./dashboard.tsx", [
// renders into the dashboard.tsx Outlet at /dashboard
index("./dashboard-home.tsx"),
route("settings", "./dashboard-settings.tsx"),
]),
] satisfies RouteConfig;
prefix
를 사용하면 공통 경로(prefix)를 여러 개의 하위 경로에 적용할 수 있다.
즉, 부모 레이아웃 없이 특정 그룹의 경로에 동일한 접두사를 추가하는 기능이다.
다음 경로에 대하여
✅ /projects
✅ /projects/:pid
✅ /projects/:pid/edit
부모 레이아웃 사용
layout("./projects/layout.tsx", [
index("./projects/home.tsx"),
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]);
layout.tsx
를 거쳐야 한다. layout.tsx
가 필요하지 않다면 불필요한 중첩을 만들게 된다.prefix를 사용한 개선된 방식
export default [
...prefix("projects", [
index("./projects/home.tsx"),
layout("./projects/project-layout.tsx", [
route(":pid", "./projects/project.tsx"),
route(":pid/edit", "./projects/edit-project.tsx"),
]),
]),
];
projects/
라는 공통 접두사가 자동 적용된다./projects/
는 레이아웃 없이 직접 렌더링된다./projects/:pid
, /projects/:pid/edit
는 레이아웃을 적용한다.동적 세그먼트(Dynamic Segments)는 경로의 특정 부분을 변수처럼 사용하는 기능이다.
경로에서 :paramName 형태로 작성하면 URL에서 해당 값을 추출하여 params 객체로 전달할 수 있다.
// app/routes.ts
route("teams/:teamId", "./team.tsx"),
// app/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
}
하나의 경로에서 여러 개의 동적 세그먼트를 사용할 수도 있다.
// app/routes.ts
route("c/:categoryId/p/:productId", "./product.tsx"),
// app/product.tsx
import type { Route } from "./+types/product";
async function loader({ params }: LoaderArgs) {
// ^? { categoryId: string; productId: string }
}
❗ 주의할 점
각 동적 세그먼트는 고유해야 한다.
동일한 이름의 세그먼트를 사용하면 나중에 정의된 값이 기존 값을 덮어쓴다.
route(":id/:id", "./error.tsx"); // 잘못된 예제
세그먼트 끝에 ?를 추가하여 경로 세그먼트를 선택 사항으로 만들 수 있다.
경로 매개변수(:param 형태)를 선택적으로 허용할 수 있다.
// app/routes.ts
route(":lang?/categories", "./categories.tsx"),
동적인 매개변수뿐만 아니라 일반적인 경로 세그먼트도 선택적으로 만들 수 있다.
// app/routes.ts
route("users/:userId/edit?", "./user.tsx");
스플랫(Splats)은 catchall 또는 star segments라고도 하며, /*
패턴을 사용하여 경로의 나머지 부분을 모두 포함하는 라우팅 방식이다.
즉, 특정 경로 이후의 모든 하위 경로를 하나의 변수로 받아올 수 있다.
// app/routes.ts
route("files/*", "./files.tsx"),
/files/
로 시작하는 모든 URL을 files.tsx에서 처리한다.// app/files.tsx
export async function loader({ params }: Route.LoaderArgs) {
console.log(params["*"]); // "docs/readme.md" (예시)
}
Splats를 변수명으로 할당하기
const { "*": splat } = params;
console.log(splat); // "docs/readme.md"
Component Routes는 URL에 따라 특정 컴포넌트를 렌더링하는 기능을 제공한다.
import { Routes, Route } from "react-router";
function Wizard() {
return (
<div>
<h1>Some Wizard with Steps</h1>
<Routes>
<Route index element={<StepOne />} />
<Route path="step-2" element={<StepTwo />} />
<Route path="step-3" element={<StepThree />} />
</Routes>
</div>
);
}
🔹 Wizard 컴포넌트 내부에서 <Routes>
를 사용 → 이 컴포넌트는 자체적으로 라우팅을 관리한다.
🔹 index
→ 기본적으로 렌더링될 StepOne
컴포넌트
🔹 step-2
, step-3
→ URL이 /step-2
, /step-3
일 때 각각 StepTwo
, StepThree
를 렌더링
다만, Component Routes 방식은 기존의 route module 방식과 달리 몇 가지 제약이 있다.
❌지원되지 않는 기능:
✅ 주로 사용하는 경우: