📝 개요
라우팅은 사용자가 URL 을 이동할 떄 그에 맞는 화면 컴포넌트를 보여주는 구조를 의미한다. SPA(Single Page Application) 인 React 에서는 실제로 페이지를 새로고침하지 않고 URL 경로에 따라 필요한 컴포넌트만 렌더링하거나 교체하는 방식으로 동작한다.
React 에서 라우팅을 구현하기 위해 사용하는 대표적인 라이브러리가 react-router-dom 이다. 이를 통해 페이지 이동, URL 파라미터 처리, 라우트 보호, 리다이렉트 같은 기능을 손쉽게 구현할 수 있다.
프로젝트에서는 로그인 상태 (accessToken) 를 기준으로 인증 여부를 판단하고 인증되지 않은 사용자는 /login 페이지로 이동되도록 한다. 모든 실제 화면은 / 루트 아래에서 <PrivateRoute> 와 <MainLayout> 을 통해 보여지고 Zustand 를 이용한 토큰 상태 관리를 통해서 이를 제어한다. 라우팅 구조는 React Router v6 + Navigate + Outlet 기반이다.
다음은 프로젝트에 적용된 라우팅 구조를 설명한다.
1️⃣ App.tsx
import AppRoutes from '@/routes/AppRoutes';
import { BrowserRouter } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<AppRoutes />
</BrowserRouter>
);
}
export default App;
앱 전체에 React Router 를 적용하는 진입점이다. <BrowserRouter> 는 HTML5 history API 를 사용하는 라우터 컨테이너이다. 실제 라우팅 구조는 AppRoutes 에서 설정한다.
react-router-dom & BrowserRouter
react-router-dom
React 전용 라우팅 라이브러리로 브라우저 환경에 맞는 라우터 (BrowserRouter) 와 모바일 / 네이티브 환경에 맞는 라우터 (NavigateRouter, HashRouter) 를 구분하여 제공해준다.
<Routes>, <Route>: 경로 기반 컴포넌트 매핑<Link>: 페이지 전환용 앵커<Navigate>: 조건에 따라 강제 이동useLocation, useNavigate, useParams: 훅 기반 라우팅 컨트롤
BrowserRouter
| 항목 | 설명 |
|---|---|
| 정의 | react-router-dom에서 제공하는 브라우저 전용 라우터 |
| 동작 방식 | 브라우저의 History API (pushState, replaceState)를 사용하여 URL을 변경 |
| 장점 | 실제 주소창 URL을 깔끔하게 유지 (예: /login, /users/1) |
| 특징 | 페이지를 새로고침하지 않고도 URL 변경 및 이동 가능 |
모든 라우팅 구조는 <BrowserRouter> 하위에서 <Routes> 와 <Route> 로 정의된다.
2️⃣ AppRoutes.tsx
import MainLayout from '@/layout/MainLayout';
import LoginPage from '@/pages/auth/LoginPage';
import DashboardPage from '@/pages/DashboardPage';
import NotFoundPage from '@/pages/errors/NotFoundPage';
import { PrivateRoute } from '@/components/route/PrivateRoute';
import RoleGuard from '@/components/route/RoleGuard';
import UnauthorizedPage from '@/pages/errors/UnauthorizedPage';
import { useAuthStore } from '@/store/useAuthStore';
import { Navigate, Route, Routes } from 'react-router-dom';
const AppRoutes = () => {
const { accessToken } = useAuthStore();
return (
<Routes>
{/* 1. 로그인 페이지 */}
<Route
path="/login"
element={accessToken ? <Navigate to="/dashboard" replace /> : <LoginPage />}
/>
{/* 2. 보호된 페이지 */}
<Route
path="/"
element={
<PrivateRoute>
<MainLayout />
</PrivateRoute>
}
>
{/* 대시보드 - 모든 사용자 */}
<Route path="dashboard" element={<DashboardPage />} />
{/* 공지사항 */}
<Route path="notices" element={<NoticeList />} />
<Route
path="notices/write"
element={
<RoleGuard requiredAuth="MANAGER">
<NoticeWrite />
</RoleGuard>
}
/>
{/* 보고서 - 모든 사용자 */}
<Route path="reports" element={<ReportList />} />
<Route path="reports/write" element={<ReportWrite />} />
{/* 사용자 */}
<Route path="users" element={<UserList />} />
<Route
path="users/write"
element={
<RoleGuard requiredAuth="MANAGER">
<UserWrite />
</RoleGuard>
}
/>
<Route path="users/password" element={<UserPassword />} />
{/* 휴가 - 모든 사용자 */}
<Route path="vacations" element={<VacationList />} />
<Route path="vacations/write" element={<VacationWrite />} />
<Route path="vacations/approval" element={<VacationApproval />} />
{/* 휴가 정보 - 관리자만 */}
<Route
path="balances"
element={
<RoleGuard requiredAuth="ADMIN">
<VacationBalanceList />
</RoleGuard>
}
/>
<Route
path="balances/write"
element={
<RoleGuard requiredAuth="ADMIN">
<VacationBalanceWrite />
</RoleGuard>
}
/>
{/* 권한 없음 페이지 */}
<Route path="/unauthorized" element={<UnauthorizedPage />} />
{/* 기본 경로 리다이렉트 */}
<Route index element={<Navigate to="dashboard" replace />} />
{/* 3. 로그인 후 잘못된 페이지 이동 시 : 404 에러 페이지 */}
<Route path="*" element={<NotFoundPage />} />
</Route>
{/* 4. 로그인 하지 않았으면 로그인 페이지로 이동 */}
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
);
};
export default AppRoutes;
<Routes> 태그 안에 <Route> 를 통해서 렌더링 될 컴포넌트와 URL 주소를 지정한다. accessToken 의 상태를 통해서 로그인한 사용자의 라우팅 주소를 설정하고 <PrivateRoute> 를 통해서 인증되지 않은 사용자에 대해서 로그인 페이지로 이동하도록 한다.
<RoleGuard /> 를 통해서 로그인 한 사용자의 권한에 따라 접근 제어를 한다. 권한에 맞지 않는 요청을 했을 때는 권한 없음 페이지를 띄우게 되는데 이는 Outlet 의 범위 안에 있기 때문에 MainLayout 의 Content 영역에 해당 페이지가 띄워진다. (Topbar, Sidebar 유지)
라우팅과 MainLayout 의 Outlet 구조 적용
import SideBar from '@/components/common/Sidebar';
import TopBar from '@/components/common/Topbar';
import { Outlet } from 'react-router-dom';
const MainLayout = () => {
return (
<div className="flex h-screen bg-gray-100">
{/* 사이드바 */}
<SideBar />
{/* 메인 컨텐츠 영역 */}
<div className="flex flex-col flex-1">
{/* 상단 바 */}
<TopBar />
{/* 메인 컨텐츠 영역 */}
<main className="flex-1 overflow-auto p-6">
<Outlet /> {/* 중첩 라우트 표시 위치 */}
</main>
</div>
</div>
);
};
export default MainLayout;
Outlet 은 react-router-dom 에서 제공하는 중첩 라우팅 을 위한 컴포넌트이다. 부모 라우트 안에서 자식 라우트가 렌더링 될 위치를 지정하는 용도로 사용된다. 보통 공통 레이아웃 (MainLayout) 컴포넌트에 내부에 작성되며, Header 와 Sider 는 고정된 채로 자식페이지가 <Outlet /> 의 영역에서 동적으로 바뀌어 렌더링된다.
| 목적 | 설명 |
|---|---|
| 공통 레이아웃 구성 | Header, Sidebar 같은 공통 UI는 고정하고 그 안에서 자식 화면만 바뀌게 함 |
| 중첩 구조 대응 | 여러 depth의 경로 구조 (/users, /users/:id, /users/edit) 대응 가능 |
| 컴포넌트 분리 | UI 레이아웃과 실제 페이지 화면을 분리해서 유지보수 용이 |
3️⃣ PrivateRoute.tsx
import { useAuthStore } from '@/store/useAuthStore';
import { Navigate, useLocation } from 'react-router-dom';
// props 로 children 값이 들어옴 : 타입 정의
interface PrivateRouteProps {
children: React.ReactNode; // <PrivateRoute> 태그 안의 모든 JSX 코드를 받게 한다.
}
const PrivateRoute = ({ children }: PrivateRouteProps) => {
const { accessToken } = useAuthStore();
const location = useLocation();
if (!accessToken) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
};
export default PrivateRoute;
PrivateRoute 는 accessToken 의 상태에 따라서 로그인 페이지로 이동할 것인지 컴포넌트의 요소를 그릴지 결정한다. 이를 통해서 로그인되지 않은 사용자에 대한 처리를 한다.
React.ReactNode: JSX 에서 쓸 수 있는 모든 타입을 포함하는 타입 (JSX 요소, 문자열, null, 배열 등.. 이는PirvateRoute로 감쌀 수 있게 한다)const location = useLocation();: 리액트 라우터에서 현재 URL의 경로, 쿼리, 해시 등의 정보를 가져오는 훅이다. 이는 사용자가 어디로 가려는지 알 수 있고 원래 가려던 페이지로 돌아가기 위해 쓴다.
useLocation() 사용 예시 : 로그인 성공 후 다시 원래 페이지로 이동
// 코드 위치 : LoginPage.tsx
const location = useLocation();
const from = location.state?.from?.pathname || '/'; // 가려던 페이지 or /
navigate(from);
- 토큰이 만료되어 원래 이동하려던 “공지사항 페이지” 로 이동되지 못하고 로그인 페이지로 넘어온다.
- 로그인이 성공하면 원래 이동하려면 “공지사항 페이지” 로 이동할 수 있도록
navigate한다.
4️⃣ RoleGuard.tsx
import { useAuthStore } from '@/store/useAuthStore';
import type { UserInfo } from '@/types/auth';
import React from 'react';
import { Navigate } from 'react-router-dom';
// 사용자 권한 타입 (기존 UserInfo.userAuth와 동일)
export type UserAuth = UserInfo['userAuth'];
// 권한 레벨 매핑
const AUTH_LEVELS: Record<UserAuth, number> = {
USER: 1,
MANAGER: 2,
ADMIN: 3,
};
interface RoleGuardProps {
children: React.ReactNode;
requiredAuth: UserAuth;
fallbackPath?: string;
}
const RoleGuard: React.FC<RoleGuardProps> = ({
children,
requiredAuth,
fallbackPath = '/unauthorized',
}) => {
const { userInfo } = useAuthStore();
// 권한 체크
const currentUserAuth = userInfo?.userAuth;
const hasPermission =
currentUserAuth && AUTH_LEVELS[currentUserAuth] >= AUTH_LEVELS[requiredAuth];
if (!hasPermission) {
return <Navigate to={fallbackPath} replace />;
}
return <>{children}</>;
};
export default RoleGuard;
RoleGuard 는 사용자 권한에 따라 페이지 접근을 제어하는 라우팅 보호 컴포넌트이다. 로그인된 사용자의 권한이 특정 권한 등급 이상인지 판단하며, 조건을 만족하지 않으면 지정된 경로로 리디렉션 시킨다.
로그인된 사용자의 권한은 로그인 시 지정되는 UserInfo 를 userAuthStore 에서 zustand 가 관리하고 있기 때문에 로그인 후에 페이지 이동 시 UserInfo 의 userAuth 의 값에 따라 판단된다.
'Project > Frontend' 카테고리의 다른 글
| [Groupware] React : axios (API 통신) (0) | 2025.06.04 |
|---|
