React Router는 React 애플리케이션에서 URL 경로와 컴포넌트를 연결하는 라우팅 라이브러리입니다. 브라우저의 페이지 새로고침 없이 URL을 변경하고 그에 맞는 컴포넌트를 렌더링하는 SPA(Single Page Application)의 핵심 역할을 담당합니다.
npm install react-router-dom
BrowserRouter: HTML5 History API(pushState, replaceState)를 기반으로 URL을 관리하는 최상위 라우터 컴포넌트
BrowserRouter는 앱 전체를 감싸는 컨텍스트 제공자 역할을 합니다. 내부의 모든 컴포넌트에서 라우팅 관련 훅과 컴포넌트를 사용할 수 있게 됩니다.
import { BrowserRouter } from "react-router-dom";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
HashRouter는 URL에 #을 사용하는 방식이며, 서버 설정이 어려운 환경에서 씁니다. 일반적인 웹 서비스에서는 BrowserRouter가 표준입니다.
Routes는 자식 Route 목록을 순서대로 검사하여 현재 URL과 가장 잘 일치하는 하나를 렌더링합니다. v5의 Switch를 대체합니다.
import { Routes, Route } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users/:id" element={<UserProfile />} />
<Route path="*" element={<NotFound />} />
</Routes>
);
}
path="*"는 위의 어떤 경로도 일치하지 않을 때 매칭되는 와일드카드 경로입니다. 404 페이지에 활용합니다.
Route: URL 경로(path)와 렌더링할 컴포넌트(element)를 1:1로 연결하는 단위
Route는 path와 element 두 가지 핵심 속성을 가집니다.
// 정확한 경로
<Route path="/about" element={<About />} />
// 동적 세그먼트: :id는 URL에서 읽어올 수 있는 파라미터
<Route path="/users/:id" element={<UserProfile />} />
// 인덱스 라우트: 부모 경로와 정확히 일치할 때 렌더링
<Route index element={<Dashboard />} />
index 속성은 부모 라우트의 경로와 정확히 일치할 때 렌더링될 기본 자식 라우트를 지정합니다.
중첩 라우트를 사용하면 URL 계층 구조와 컴포넌트 계층 구조를 일치시킬 수 있습니다. 헤더·사이드바 같은 공통 레이아웃을 부모에 두고 자식만 교체하는 패턴에 적합합니다.
function App() {
return (
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="stats" element={<Stats />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
);
}
URL: /dashboard → DashboardLayout + DashboardHome
URL: /dashboard/stats → DashboardLayout + Stats
URL: /dashboard/settings → DashboardLayout + Settings
중첩 라우트에서 부모 컴포넌트가 <Outlet />을 렌더링하면, 현재 URL에 맞는 자식 컴포넌트가 그 자리에 나타납니다.
function DashboardLayout() {
return (
<div>
<nav>
<Link to="/dashboard">홈</Link>
<Link to="/dashboard/stats">통계</Link>
<Link to="/dashboard/settings">설정</Link>
</nav>
<main>
{/* 자식 라우트 컴포넌트가 여기에 렌더링됨 */}
<Outlet />
</main>
</div>
);
}
useOutletContext를 사용하면 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 수도 있습니다.
// 부모
<Outlet context={{ user }} />
// 자식
const { user } = useOutletContext<{ user: User }>();
HTML <a> 태그는 클릭 시 페이지를 새로고침합니다. <Link>는 새로고침 없이 URL만 변경하여 SPA 방식으로 전환합니다.
import { Link, NavLink } from "react-router-dom";
function Nav() {
return (
<nav>
<Link to="/">홈</Link>
<Link to="/about">소개</Link>
{/* NavLink: 현재 URL과 일치하면 active 클래스 자동 추가 */}
<NavLink
to="/dashboard"
className={({ isActive }) => (isActive ? "active" : "")}
>
대시보드
</NavLink>
</nav>
);
}
NavLink는 Link의 확장판으로, 현재 경로와 일치하면 isActive가 true가 되어 스타일을 다르게 적용할 수 있습니다.
Navigate는 렌더링되는 순간 지정된 경로로 이동합니다. 인증 여부에 따른 보호 라우트 구현에 자주 사용됩니다.
import { Navigate } from "react-router-dom";
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
// 미인증 사용자를 로그인 페이지로 리다이렉트
return <Navigate to="/login" replace />;
}
return <>{children}</>;
}
// 사용
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
replace 속성을 주면 히스토리 스택에 현재 경로를 남기지 않습니다. 로그인 페이지에서 뒤로 가기를 눌렀을 때 다시 리다이렉트되는 무한 루프를 방지합니다.
컴포넌트 렌더링이 아닌 이벤트나 비동기 작업 완료 후 이동이 필요할 때 사용합니다.
import { useNavigate } from "react-router-dom";
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login(credentials);
// 로그인 성공 후 대시보드로 이동
navigate("/dashboard");
};
return <form onSubmit={handleSubmit}>...</form>;
}
// 뒤로 가기 / 앞으로 가기
navigate(-1); // 뒤로
navigate(1); // 앞으로
// replace: 현재 히스토리를 교체 (뒤로 가기 시 이전 페이지로 감)
navigate("/home", { replace: true });
// state: 이동할 때 데이터를 함께 전달
navigate("/result", { state: { score: 95 } });
Route에서 :paramName으로 선언한 동적 세그먼트 값을 읽어옵니다.
// 라우트 선언
<Route path="/users/:userId/posts/:postId" element={<Post />} />
// 컴포넌트 내부
import { useParams } from "react-router-dom";
function Post() {
const { userId, postId } = useParams<{
userId: string;
postId: string;
}>();
const { data: post } = useQuery({
queryKey: ["post", userId, postId],
queryFn: () => fetchPost(userId!, postId!),
});
return <article>{post?.title}</article>;
}
useParams의 반환값은 모두 string | undefined입니다. TypeScript에서 타입 단언(!) 또는 방어 코드가 필요합니다.
v6.4에서 도입된 데이터 라우터 기능입니다. 컴포넌트 안에서 데이터를 가져오는 대신 라우트 진입 시점에 미리 로드하여 워터폴(waterfall) 문제를 줄입니다.
import { createBrowserRouter, RouterProvider, useLoaderData } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/users/:id",
element: <UserProfile />,
loader: async ({ params }) => {
const user = await fetchUser(params.id!);
return { user };
},
},
]);
function UserProfile() {
const { user } = useLoaderData() as { user: User };
return <p>{user.name}</p>;
}
function Root() {
return <RouterProvider router={router} />;
}
loader는 컴포넌트가 렌더링되기 전에 실행되므로, 컴포넌트 내부에서 별도의 로딩 상태를 관리할 필요가 없습니다.
action은 라우트에서 POST, PUT, DELETE 요청을 처리합니다. HTML <Form> 컴포넌트와 함께 사용합니다.
import { Form, redirect, useActionData } from "react-router-dom";
const router = createBrowserRouter([
{
path: "/todos/new",
element: <NewTodo />,
action: async ({ request }) => {
const formData = await request.formData();
const title = formData.get("title") as string;
if (!title) {
return { error: "제목을 입력해 주세요." };
}
await createTodo({ title });
return redirect("/todos");
},
},
]);
function NewTodo() {
const actionData = useActionData() as { error?: string } | undefined;
return (
<Form method="post">
<input name="title" />
{actionData?.error && <p>{actionData.error}</p>}
<button type="submit">추가</button>
</Form>
);
}
redirect를 반환하면 액션 완료 후 지정 경로로 이동합니다.
각 라우트에 errorElement를 지정하면 해당 라우트 또는 하위 라우트에서 오류가 발생했을 때 지정한 컴포넌트를 렌더링합니다.
import { useRouteError, isRouteErrorResponse } from "react-router-dom";
function ErrorPage() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status} 오류</h1>
<p>{error.statusText}</p>
</div>
);
}
return <p>알 수 없는 오류가 발생했습니다.</p>;
}
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />, // 전역 오류 처리
children: [
{
path: "users/:id",
element: <UserProfile />,
loader: fetchUser,
errorElement: <UserErrorPage />, // 이 라우트 전용 오류 처리
},
],
},
]);
useRouteError로 오류 객체를 읽고, isRouteErrorResponse로 HTTP 응답 오류인지 판별합니다.