설명
Next.js를 주로 써오다 React도 배우려 하면서 기존의 라우팅 방식이 찜찜하여 React Router를 찾아보게 되었습니다. 놀랍게도, Next.js처럼 폴더 기반 라우트 구조, SSR, CSR 지원, 코드 스플리팅 등의 유용한 기능을 제공하는 Framework Mode가 도입되었음을 알게 되었습니다.
이전의 라우팅 방식도 여전히 지원되지만, 충분히 도입할 만한 장점이 있어 마이그레이션을 고려할 만하다고 판단하여 React Router의 기본 개념, 주요 기능을 정리하였습니다.
⚠️ 계속해서 버전이 업데이트되고 있어 내용이 변경될 수 있으므로 공식 문서 참고를 권장합니다. 또한, 여러 기능들이 제공되지만 추후 변경될 가능성이 있어 자세한 내용은 서술하지 않습니다.
React Router v7 Framework Mode
React Router v7은 전통적인 클라이언트 측 라우팅을 넘어, 프레임워크 수준의 기능을 제공하는 Framework Mode를 도입하였습니다. 기존의 Next.js나 Remix와 같은 프레임워크의 라우팅 경험을 유연하게 React Router에 적용한 방식입니다.
특징
- 파일 시스템 기반 라우팅: 파일 및 폴더 구조가 직접 라우트 구조로 변환됩니다.
- 선언적 데이터 로딩: 라우트 수준에서 데이터 요구사항을 정의합니다.
- 빌드 타임 최적화: 라우트 구조가 빌드 타임에 분석되어 최적화됩니다.
- 코드 생성: 라우트 구성과 관련된 코드가 자동으로 생성됩니다.
- 타입 안전성: TypeScript와의 긴밀한 통합을 통해 강력한 타입 안전성을 제공합니다.
기존 라우팅 방식과의 차이
JSX 방식
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/products/:id" element={<Product />} />
</Routes>
</BrowserRouter>
);
}
객체 방식
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
...
}
])
function App() {
return (
<RouterProvider router={router} />
)
}
Framework Mode:
import { type RouteConfig, route, index, layout, prefix } from "@react-router/dev/routes";
export default [
index("./home.tsx"),
route("teams/:teamId", "./team.tsx"),
layout("./auth/layout.tsx", [
route("login", "./auth/login.tsx"),
route("register", "./auth/register.tsx"),
]),
...prefix("concerts", [
index("./concerts/home.tsx"),
route(":city", "./concerts/city.tsx"),
route("trending", "./concerts/trending.tsx"),
]),
] satisfies RouteConfig;
// app.team.tsx
import type { Route } from "./+types/team"; // provides type safety/inference
// renders after the loader is done
export default function Component({ loaderData }: Route.ComponentProps) { ... }
주요 기능
1. 데이터 로딩
렌더링되기 전에 route componen 에 데이터를 제공
Client Data Loading
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
export async function clientLoader({ params }: Route.ClientLoaderArgs) {
const res = await fetch(`/api/products/${params.pid}`);
const product = await res.json();
return product;
}
// HydrateFallback is rendered while the client loader is running
export function HydrateFallback() { return <div>Loading...</div> }
export default function Product({ loaderData }: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
Server Data Loading
// route("products/:pid", "./product.tsx");
import type { Route } from "./+types/product";
import { fakeDb } from "../db";
export async function loader({ params }: Route.LoaderArgs) {
const product = await fakeDb.getProduct(params.pid);
return product;
}
export default function Product({ loaderData }: Route.ComponentProps) {
const { name, description } = loaderData;
return (
<div>
<h1>{name}</h1>
<p>{description}</p>
</div>
);
}
2. actions
Client Actions
// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { someApi } from "./api";
export async function clientAction({ request }: Route.ClientActionArgs) {
let formData = await request.formData();
let title = formData.get("title");
let project = await someApi.updateProject({ title });
return project;
}
export default function Project({ actionData }: Route.ComponentProps) {
return (
<div>
<h1>Project</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (<p>{actionData.title} updated</p>) : null}
</div>
);
}
Server Actions
// route('/projects/:projectId', './project.tsx')
import type { Route } from "./+types/project";
import { Form } from "react-router";
import { someApi } from "./api";
export async function clientAction({ request }: Route.ClientActionArgs) {
let formData = await request.formData();
let title = formData.get("title");
let project = await someApi.updateProject({ title });
return project;
}
export default function Project({ actionData }: Route.ComponentProps) {
return (
<div>
<h1>Project</h1>
<Form method="post">
<input type="text" name="title" />
<button type="submit">Submit</button>
</Form>
{actionData ? (<p>{actionData.title} updated</p>) : null}
</div>
);
}
3. TypeScript 안정성
React Router는 app/routes.ts
에서 라우트 구성을 실행하여 앱의 경로를 결정한 뒤, 각 라우트에 대한 타입을 .react-router/types/
디렉터리 내 +types/<route file>.d.ts
로 생성합니다. rootDirs
를 설정하면 TypeScript가 이 파일들을 실제 라우트 모듈과 동일한 위치에 있는 것처럼 가져올 수 있습니다.
import type { Route } from "./+types/product";
// types generated for this route 👆
export function loader({ params }: Route.LoaderArgs) {
// 👆 { id: string }
return { planet: `world #${params.id}` };
}
export default function Component({
loaderData, // 👈 { planet: string }
}: Route.ComponentProps) {
return <h1>Hello, {loaderData.planet}!</h1>;
}
4. 에러 처리
에러 처리 기능:
- 라우트별 에러 바운더리
- 로더와 액션에서 발생한 에러의 자동 캐치
- 중첩된 라우트에서 계층적 에러 처리
// root.ts
import { Route } from "./+types/root";
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
if (isRouteErrorResponse(error)) {
return (
<>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
// route/blog.tsx
import { isRouteErrorResponse useRouteError } from "react-router";
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<>
<h1>{error.status} {error.statusText}</h1>
<p>{error.data}</p>
</>
);
} else if (error instanceof Error) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<p>The stack trace is:</p>
<pre>{error.stack}</pre>
</div>
);
} else {
return <h1>Unknown Error</h1>;
}
}
5. 의존성 예비 로딩
성능 최적화:
// routes/blog
export function links() {
return [
{ rel: "preload", href: "/api/user-data", as: "fetch" },
{ rel: "stylesheet", href: "/styles/user-profile.css" }
];
}
6. 메타데이터 관리
페이지별 메타데이터 설정:
// routes/blog
export function meta({ data }) {
return [
{ title: `${data.post.title} | My Blog` },
{ name: "description", content: data.post.excerpt }
];
}
참고
'React' 카테고리의 다른 글
[React] react 렌더링 방식 이해와 memoization (0) | 2025.03.16 |
---|---|
React Hook Form 이해 (0) | 2024.11.02 |
[React] 지나친 추상화 및 파일 분리 시 주의점 (0) | 2024.11.02 |
[React 18. 2] 공식 문서 훑으면서 몰랐던 부분 찾아보기 (0) | 2024.01.04 |
React 에서 이벤트 위임 하지 않는 이유 (0) | 2023.12.22 |