junyeokk
Blog
React·2026. 02. 09

Route Guard (인증 라우트 보호)

SPA에서 특정 페이지는 로그인한 사용자만 접근할 수 있어야 한다. 관리자 대시보드, 설정 페이지 같은 화면에 비로그인 사용자가 URL을 직접 입력해서 접근하면 안 된다. Route Guard는 라우트 레벨에서 인증 여부를 검사하고, 미인증 사용자를 로그인 페이지로 보내는 패턴이다. React Router에서는 Layout Route와 <Outlet>을 활용해서 구현한다.

ProtectedRoute 패턴

가장 기본적인 Route Guard는 인증 상태를 확인해서 통과시키거나 리다이렉트하는 컴포넌트다.

tsx
import { Navigate, Outlet } from 'react-router';
import { useAuthStore } from '../stores/authStore';

export function ProtectedRoute() {
  const isAuthenticated = useAuthStore((s) => s.isAuthenticated);

  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }

  return <Outlet />;
}

이 컴포넌트는 두 가지 일만 한다. Zustand 스토어에서 isAuthenticated를 확인하고, 로그인되지 않았으면 <Navigate>로 로그인 페이지로 보낸다. 로그인되어 있으면 <Outlet />을 렌더링해서 자식 라우트를 표시한다.

<Navigate to="/login" replace />에서 replace 속성이 중요하다. replace가 없으면 브라우저 히스토리에 현재 URL이 남아서, 로그인 후 뒤로 가기를 누르면 다시 보호된 페이지로 가려다가 로그인으로 리다이렉트되는 무한 루프 같은 상황이 발생할 수 있다. replace를 사용하면 히스토리를 대체하므로 이 문제를 방지한다.

Layout Route: path 없는 라우트

React Router에서 ProtectedRoute를 적용할 때, path가 없는 라우트를 사용한다. 이를 Layout Route라고 한다.

tsx
export const router = createBrowserRouter([
  // 로그인 페이지 (공개, 보호 없음)
  {
    path: '/login',
    element: <LoginPage />,
  },
  // 보호된 영역 (인증 필요)
  {
    element: <ProtectedRoute />,  // path가 없음!
    children: [
      {
        path: '/',
        element: <RootLayout />,
        children: [
          { index: true, element: <HomePage /> },
          { path: 'managers', element: <ManagerListPage /> },
          { path: 'managers/:id', element: <ManagerDetailPage /> },
          { path: 'stores', element: <StoreListPage /> },
          // ... 나머지 보호된 페이지들
        ],
      },
    ],
  },
]);

Layout Route는 path가 없기 때문에 URL 매칭에 관여하지 않는다. 대신 자식 라우트들을 감싸는 "래퍼" 역할만 한다. <ProtectedRoute><Outlet />을 렌더링하면, React Router가 현재 URL에 맞는 자식 라우트를 그 자리에 렌더링한다.

이 구조의 장점은 보호가 필요한 모든 페이지를 한 곳에서 관리할 수 있다는 것이다. 개별 페이지마다 인증 검사 코드를 넣을 필요 없이, 라우트 트리 구조만으로 보호 범위를 설정할 수 있다.

Outlet의 역할

<Outlet />은 React Router에서 중첩 라우트의 자식을 렌더링하는 자리표시자(placeholder)다.

URL: /managers 라우트 트리 매칭: ProtectedRoute (Layout Route, path 없음) └─ RootLayout (path: '/') └─ ManagerListPage (path: 'managers') 렌더링 결과: <ProtectedRoute> → isAuthenticated 확인 <Outlet /> → RootLayout 렌더링 <AdminLayout> <Outlet /> → ManagerListPage 렌더링 <ManagerListPage />

<Outlet />이 없으면 자식 라우트가 렌더링될 자리가 없어서 아무것도 표시되지 않는다. ProtectedRoute에서 <Outlet />을 반환하는 것이 자식 라우트들을 "통과시킨다"는 의미인 이유다.

RootLayout: 공통 레이아웃 적용

ProtectedRoute 아래에 또 다른 Layout Route를 두면 인증된 페이지에만 공통 레이아웃을 적용할 수 있다.

tsx
URL: /managers

라우트 트리 매칭:
  ProtectedRoute (Layout Route, path 없음)
    └─ RootLayout (path: '/')
        └─ ManagerListPage (path: 'managers')

렌더링 결과:
  <ProtectedRoute>        → isAuthenticated 확인
    <Outlet />            → RootLayout 렌더링
      <AdminLayout>
        <Outlet />        → ManagerListPage 렌더링
          <ManagerListPage />

AdminLayout은 사이드바, 헤더 같은 공통 UI를 포함하고, <Outlet />에 각 페이지 콘텐츠가 들어간다. 로그인 페이지는 ProtectedRoute 밖에 있으므로 이 레이아웃이 적용되지 않는다.

[로그인 페이지] → LoginPage (레이아웃 없음) [보호된 페이지들] → ProtectedRoute → RootLayout(AdminLayout) → 각 페이지

전체 인증 흐름

프론트엔드의 인증 관련 기능들이 어떻게 연결되는지 전체 흐름을 정리해보자.

1. 사용자가 /managers 접근 └─ ProtectedRoute: isAuthenticated 확인 └─ false → Navigate to /login 2. 로그인 페이지에서 이메일/비밀번호 입력 └─ useLoginMutation.mutate({ email, password }) └─ POST /admin/auth/login └─ 성공: onSuccess에서 setAuth(user, token) └─ Zustand 스토어에 token, user 저장 └─ persist 미들웨어가 localStorage에도 저장 └─ /managers로 이동 3. /managers 페이지 로드 └─ ProtectedRoute: isAuthenticated 확인 └─ true → Outlet 렌더링 → ManagerListPage 표시 └─ useManagersQuery() 실행 └─ Request 인터셉터가 자동으로 토큰 첨부 └─ GET /admin/members (Authorization: Bearer <token>) 4. 토큰 만료 시 └─ API 응답 401 └─ Response 인터셉터: clearAuth() + redirect /login 5. 새로고침 시 └─ persist 미들웨어가 localStorage에서 상태 복원 └─ isAuthenticated: true → 정상 접근

이 흐름에서 Zustand(상태관리), TanStack Query(서버 통신), Axios 인터셉터(토큰 첨부/에러 처리), Route Guard(페이지 보호)가 각자의 역할을 수행하면서 하나의 인증 시스템을 구성한다.

요약

Route Guard는 라우트 트리에서 인증을 검사하는 패턴이다. Layout Route(path 없는 라우트)에 ProtectedRoute를 배치하고, 인증 여부에 따라 <Outlet />(통과) 또는 <Navigate />(리다이렉트)를 반환한다. 이 패턴을 사용하면 개별 페이지가 아닌 라우트 구조 단위로 접근 제어를 관리할 수 있고, Zustand의 persist와 결합하면 새로고침 후에도 인증 상태가 유지된다.