개인의 기록
  • 소개
  • 프로젝트
  • 글
  • 링크

© 2026 newgirok

← 글 목록

React Router — SPA URL 기반 라우팅 라이브러리

2026년 5월 30일
ReactSPA라우팅React Router

React Router는 React 애플리케이션에서 URL 경로와 컴포넌트를 연결하는 라우팅 라이브러리입니다. 브라우저의 페이지 새로고침 없이 URL을 변경하고 그에 맞는 컴포넌트를 렌더링하는 SPA(Single Page Application)의 핵심 역할을 담당합니다.

npm install react-router-dom

1. BrowserRouter

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가 표준입니다.


2. Routes

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 페이지에 활용합니다.


3. Route

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 속성은 부모 라우트의 경로와 정확히 일치할 때 렌더링될 기본 자식 라우트를 지정합니다.


4. Nested Route

중첩 라우트를 사용하면 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

5. Outlet

중첩 라우트에서 부모 컴포넌트가 <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 }>();

6. Link

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가 되어 스타일을 다르게 적용할 수 있습니다.


7. Navigate

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 속성을 주면 히스토리 스택에 현재 경로를 남기지 않습니다. 로그인 페이지에서 뒤로 가기를 눌렀을 때 다시 리다이렉트되는 무한 루프를 방지합니다.


8. useNavigate

컴포넌트 렌더링이 아닌 이벤트나 비동기 작업 완료 후 이동이 필요할 때 사용합니다.

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 } });

9. useParams

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에서 타입 단언(!) 또는 방어 코드가 필요합니다.


10. Loader

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는 컴포넌트가 렌더링되기 전에 실행되므로, 컴포넌트 내부에서 별도의 로딩 상태를 관리할 필요가 없습니다.


11. Action

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를 반환하면 액션 완료 후 지정 경로로 이동합니다.


12. Error Boundary

각 라우트에 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 응답 오류인지 판별합니다.

← 이전 글Context API — Props 없이 컴포넌트 간 데이터 공유
다음 글 →React Hook Form — 폼 상태와 유효성 검증 라이브러리