React Router v6는 선언적 라우팅을 단순화하고, 데이터 로딩/변경을 라우트 계층으로 끌어올리는 Data Router 기능(v6.4+)을 도입했습니다. 이 글은 실무에서 바로 쓸 수 있는 최신 v6 사용법과 베스트 프랙티스를 정리합니다.
1. v6 핵심 변화 한눈에 보기
- Switch → Routes, Redirect → Navigate, exact 제거, component/render → element로 변경되었습니다.
- 중첩 라우팅 기본 탑재, 상대 경로 네비게이션 개선, 레이아웃은 Outlet으로 구성합니다.
- Data Router(createBrowserRouter, loader, action, errorElement, useLoaderData, useNavigation, defer/Await)로 라우트 단에서 데이터 로딩/뮤테이션과 에러 핸들링을 다룹니다.
- 라우트 단위 코드 스플리팅(lazy) 지원으로 초기 번들 크기를 줄일 수 있습니다.
2. 설치와 기본 세팅
// 1) 설치
// npm i react-router-dom
// 2) Data Router 기반 엔트리 (권장)
import React from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import RootLayout from "./layouts/RootLayout";
import Home from "./routes/Home";
import ErrorPage from "./routes/ErrorPage";
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
],
},
]);
createRoot(document.getElementById("root")).render(
<RouterProvider router={router} />
);
기존 BrowserRouter + Routes 방식도 가능하지만, Data Router로 통일하면 실무에서 데이터 흐름이 단순해집니다.
3. 레이아웃과 중첩 라우팅
// layouts/RootLayout.jsx
import { NavLink, Outlet } from "react-router-dom";
export default function RootLayout() {
return (
<div>
<nav>
<NavLink to="/" end>Home</NavLink>
<NavLink to="/posts">Posts</NavLink>
</nav>
<main>
<Outlet /> {/* 자식 라우트가 렌더링됩니다 */}
</main>
</div>
);
}
중첩 경로에서 상대 네비게이션이 기본값입니다. 예: 자식 라우트에서 navigate("..")는 상위 경로로 이동합니다.
4. CRUD 예제로 보는 Data Router(loader/action)
// routes config
import { createBrowserRouter, redirect } from "react-router-dom";
import RootLayout from "./layouts/RootLayout";
import ErrorPage from "./routes/ErrorPage";
import PostsLayout from "./routes/PostsLayout";
import PostsList, { loader as postsLoader } from "./routes/PostsList";
import PostDetail, { loader as postLoader } from "./routes/PostDetail";
import NewPost, { action as createPostAction } from "./routes/NewPost";
export const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
errorElement: <ErrorPage />,
children: [
{
path: "posts",
element: <PostsLayout />,
children: [
{ index: true, element: <PostsList />, loader: postsLoader },
{ path: ":postId", element: <PostDetail />, loader: postLoader },
{ path: "new", element: <NewPost />, action: createPostAction },
],
},
],
},
]);
// routes/PostsList.jsx
import { useLoaderData, Link } from "react-router-dom";
export async function loader() {
const res = await fetch("/api/posts");
if (!res.ok) throw new Response("Failed to load", { status: res.status });
return res.json();
}
export default function PostsList() {
const posts = useLoaderData();
return (
<div>
<h2>Posts</h2>
<Link to="new">새 글 작성</Link>
{posts.map(p => (
<div key={p.id}>
<Link to={String(p.id)}>{p.title}</Link>
</div>
))}
</div>
);
}
// routes/PostDetail.jsx
import { useLoaderData } from "react-router-dom";
export async function loader({ params }) {
const res = await fetch(`/api/posts/${params.postId}`);
if (res.status === 404) throw new Response("Not found", { status: 404 });
if (!res.ok) throw new Error("Server error");
return res.json();
}
export default function PostDetail() {
const post = useLoaderData();
return (
<article>
<h2>{post.title}</h2>
<p>{post.body}</p>
</article>
);
}
// routes/NewPost.jsx
import { Form, redirect, useNavigation } from "react-router-dom";
export async function action({ request }) {
const formData = await request.formData();
const payload = Object.fromEntries(formData);
const res = await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Response("Create failed", { status: res.status });
const created = await res.json();
return redirect(`/posts/${created.id}`); // 성공 시 상세로 이동
}
export default function NewPost() {
const nav = useNavigation();
const busy = nav.state === "submitting";
return (
<div>
<h2>새 글 작성</h2>
<Form method="post">
<p><input name="title" placeholder="제목" required /></p>
<p><textarea name="body" placeholder="내용" required /></p>
<button type="submit" disabled={busy}>{busy ? "저장 중..." : "저장"}</button>
</Form>
</div>
);
}
Form는 action으로 직접 POST를 보내며, 완료 후 관련 로더가 자동 재검증(revalidation)됩니다.
5. 에러 경계와 오류 처리
// routes/ErrorPage.jsx
import { useRouteError, isRouteErrorResponse } from "react-router-dom";
export default function ErrorPage() {
const err = useRouteError();
if (isRouteErrorResponse(err)) {
return <div>에러 {err.status}: {err.statusText}</div>;
}
return <div>문제가 발생했습니다: {String(err?.message ?? err)}</div>;
}
loader/action에서 throw Response(...) 또는 Error를 던지면 해당 라우트 체인의 errorElement가 렌더링됩니다.
6. 로딩 상태 처리(useNavigation)
// 전역 로딩 인디케이터 예시 (레이아웃에서)
import { useNavigation, Outlet } from "react-router-dom";
export default function PostsLayout() {
const nav = useNavigation();
const pending = nav.state === "loading" || nav.state === "submitting";
return (
<div>
{pending && <div style={{ height: 3, background: "#4f46e5" }} />}
<Outlet />
</div>
);
}
라우트 전환/액션 진행 중 상태를 한 곳에서 다룰 수 있어 스피너 난립을 줄입니다.
7. 지연 로딩: 라우트 레벨 lazy
// lazy 라우트 구성 (권장: Data Router와 함께)
const router = createBrowserRouter([
{
path: "/",
lazy: () => import("./routes/root.lazy"), // 모듈에서 loader/Component 등 제공
},
]);
// routes/root.lazy.js
export async function loader() {
return { msg: "hello" };
}
export function Component() {
return <div>코드 스플리팅된 루트</div>;
}
export function ErrorBoundary({ error }) {
return <div>에러: {String(error)}</div>;
}
route.lazy는 해당 경로에 진입할 때만 코드와 로더를 가져옵니다. React.Suspense를 별도로 감싸지 않아도 됩니다. 기존 React.lazy로 element를 lazy 처리하는 것도 가능합니다.
8. defer와 Await로 스트리밍 로딩
// routes/PostDetailDefer.jsx (예시)
import { defer } from "react-router-dom";
export async function loader({ params }) {
const postPromise = fetch(`/api/posts/${params.postId}`).then(r => r.json());
const commentsPromise = fetch(`/api/posts/${params.postId}/comments`).then(r => r.json());
return defer({ post: postPromise, comments: commentsPromise });
}
import { useLoaderData, Await } from "react-router-dom";
import { Suspense } from "react";
export default function PostDetailDefer() {
const data = useLoaderData();
return (
<div>
<Suspense fallback={<p>게시글 로딩...</p>}>
<Await resolve={data.post}>{post => <h2>{post.title}</h2>}</Await>
</Suspense>
<Suspense fallback={<p>댓글 로딩...</p>}>
<Await resolve={data.comments}>{cs => cs.map(c => <p key={c.id}>{c.body}</p>)}</Await>
</Suspense>
</div>
);
}
큰 데이터는 지연 스트리밍하여 첫 페인트를 빠르게 가져갈 수 있습니다.
9. 동적 세그먼트와 쿼리 파라미터
// useParams
import { useParams } from "react-router-dom";
const { postId } = useParams();
// useSearchParams
import { useSearchParams } from "react-router-dom";
const [sp, setSp] = useSearchParams();
const page = Number(sp.get("page") ?? 1);
const next = page + 1;
setSp({ page: String(next) }, { replace: true });
검색 파라미터를 상태처럼 다루며, 브라우저 히스토리 관리도 함께 처리합니다.
10. 라우트 보호(인증 가드)
// 인증 체크를 loader에서 수행
import { redirect } from "react-router-dom";
export async function protectedLoader({ request }) {
const isAuthed = await fetch("/api/me", { headers: { Accept: "application/json" } })
.then(r => r.ok);
if (!isAuthed) return redirect(`/login?next=${new URL(request.url).pathname}`);
return null;
}
// 라우트 구성
{
path: "dashboard",
loader: protectedLoader,
element: <Dashboard />,
}
UI 컴포넌트에서 인증 여부를 판단하기보다, 라우터에서 접근 제어를 처리하면 깔끔합니다.
11. useRoutes와 JSX 기반 정의(대안)
import { useRoutes } from "react-router-dom";
export default function App() {
const element = useRoutes([
{ path: "/", element: <Home /> },
{ path: "/about", element: <About /> },
]);
return element;
}
useRoutes는 간단한 앱에 유용하지만, 데이터 로딩/에러/액션을 표준화하려면 Data Router를 권장합니다.
12. v5 → v6 마이그레이션 요약
- Switch → Routes
- Redirect → Navigate
- exact 제거(경로 매칭 개선)
- component/render → element: element에는 JSX 요소를 전달합니다.
- withRouter 제거 → useNavigate/useLocation/useParams 훅 사용
- useHistory → useNavigate
13. 실무 팁 체크리스트
- 데이터는 useEffect가 아닌 loader/action으로 옮겨 SSR/스트리밍/에러 경계를 일관 처리합니다.
- 액션 성공 시 redirect로 라우팅 흐름을 단순화합니다.
- 목록/상세가 같은 트리 안에 있으면 액션 이후 자동 재검증으로 캐시 동기화를 얻습니다.
- 에러는 Response(status)로 명시해 UX를 통제합니다(404/401/500 등).
- 라우트 lazy로 초기 번들을 줄이고, 상위 레이아웃은 가능하면 고정하여 전환 비용을 최소화합니다.
- 컨트롤 바/탑바 등 전역 로딩 UI는 useNavigation을 레이아웃에서 처리합니다.
이 가이드를 바탕으로 라우트 정의, 데이터 로딩/액션, 에러/로딩 상태를 라우터 중심으로 재구성해 보세요. 컴포넌트는 화면 표현에 집중하고, 라우터는 데이터 흐름과 전환을 책임질 때 개발 체감이 가장 크게 좋아집니다.
'React' 카테고리의 다른 글
| React 앱에서 Lazy Loading 이미지 처리하기 (1) | 2026.04.16 |
|---|---|
| React에서 Formik과 Yup으로 폼 검증 구현하기 (0) | 2026.04.15 |
| React Suspense로 데이터 로딩 경험 개선하기 (0) | 2026.04.14 |
| React에서 Virtualized List 구현하기 (0) | 2026.04.13 |
| React Hooks로 커스텀 훅 설계하기 (1) | 2026.04.13 |