React Router v7 Framework - Routing

FeelsBotMan·2025년 2월 19일
0

React Router v7

목록 보기
2/4
post-thumbnail

Routing

React Router는 다양한 라우팅 패턴을 지원하며, 주요 메서드는 다음과 같다:

  • route() 특정 경로에 대한 컴포넌트 연결
  • index() 부모 경로의 기본 자식 라우트
  • layout() 공통 레이아웃을 적용하는 라우트
  • prefix() 여러 경로에 공통 접두사(prefix) 추가

Configuring Routes

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
/aboutabout.tsx
/auth/loginauth/login.tsx (레이아웃 포함)
/auth/registerauth/register.tsx (레이아웃 포함)
/concertsconcerts/home.tsx
/concerts/seoulconcerts/city.tsx (params.city = "seoul")
/concerts/trendingconcerts/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;
  • flatRoutes() → 파일 시스템을 스캔해 폴더 구조대로 라우트를 생성
  • 파일 시스템 라우팅과 수동 라우트 설정을 혼합할 수 있다.

Route Modules

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

중첩 라우트(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;
  • dashboard.tsx가 부모 라우트이며, 그 내부에 home.tsx와 settings.tsx가 중첩됨
  • /dashboard를 방문하면 home.tsx가 렌더링됨
  • /dashboard/settings를 방문하면 settings.tsx가 렌더링됨
// 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 />에 렌더링됨

Root Route

routes.ts의 모든 경로는 app/root.tsx 모듈 내부에 중첩되어 있다.

Layout Routes

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.tsxmarketing/home.tsx
/contactmarketing/layout.tsxmarketing/contact.tsx
/projectsprojects/project-layout.tsxprojects/home.tsx
/projects/:pidprojects/project-layout.tsxprojects/project.tsx
/projects/:pid/editprojects/project-layout.tsxprojects/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 Routes

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;

Route Prefixes

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

동적 세그먼트(Dynamic Segments)는 경로의 특정 부분을 변수처럼 사용하는 기능이다.
경로에서 :paramName 형태로 작성하면 URL에서 해당 값을 추출하여 params 객체로 전달할 수 있다.

// app/routes.ts
route("teams/:teamId", "./team.tsx"),
  • ✅ /teams/123
  • ✅ /teams/abc
// 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"),
  • ✅ /c/electronics/p/5678
  • ✅ /c/books/p/1234
// app/product.tsx
import type { Route } from "./+types/product";

async function loader({ params }: LoaderArgs) {
  //                    ^? { categoryId: string; productId: string }
}

❗ 주의할 점
각 동적 세그먼트는 고유해야 한다.
동일한 이름의 세그먼트를 사용하면 나중에 정의된 값이 기존 값을 덮어쓴다.

route(":id/:id", "./error.tsx"); // 잘못된 예제

Optional Segments

세그먼트 끝에 ?를 추가하여 경로 세그먼트를 선택 사항으로 만들 수 있다.

경로 매개변수(:param 형태)를 선택적으로 허용할 수 있다.

// app/routes.ts
route(":lang?/categories", "./categories.tsx"),
  • ✅ /categories
  • ✅ /en/categories
  • ✅ /ko/categories

동적인 매개변수뿐만 아니라 일반적인 경로 세그먼트도 선택적으로 만들 수 있다.

// app/routes.ts
route("users/:userId/edit?", "./user.tsx");
  • ✅ /users/123
  • ✅ /users/123/edit

Splats

스플랫(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" (예시)
}
  • params["*"]을 사용하면 files/ 이후의 모든 문자열을 가져올 수 있다.

Splats를 변수명으로 할당하기

const { "*": splat } = params;
console.log(splat); // "docs/readme.md"
  • 구조 분해 할당을 이용해 params["*"]를 splat이라는 이름으로 바꿀 수 있다.

Component Routes

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 방식과 달리 몇 가지 제약이 있다.

❌지원되지 않는 기능:

  • 데이터 로딩 (Loaders) → URL 이동 시 서버에서 데이터를 미리 가져오는 기능이 없음
  • 액션 (Actions) → form을 통한 데이터 변경 및 자동 UI 갱신 기능이 없음
  • 코드 스플리팅 (Code Splitting) → 필요한 페이지의 코드만 로드하는 기능이 없음
  • SEO 설정 → 페이지별 메타태그 관리 기능이 없음

✅ 주로 사용하는 경우:

  • 다단계 마법사(Wizard) UI처럼 특정 컴포넌트 내에서 단계별 이동이 필요한 경우
  • 앱 내부에서 별도의 작은 라우팅을 만들고 싶을 때
  • 데이터 로딩과 관련 없는 단순한 UI 라우팅이 필요할 때
profile
안드로이드 페페

0개의 댓글