🎯 학습 목표
- React Router v6의 Routes/Route 중첩 구조와 Outlet을 이해한다
- useParams, useNavigate, useLocation을 각각 코드로 활용할 수 있다
- Protected Route(인증 라우트)를 직접 구현할 수 있다
- React.lazy와 Suspense로 코드 스플리팅을 구현한다
- SPA 라우팅 원리(History API)를 이해한다
📖 핵심 개념 1 — SPA 라우팅 원리
전통적인 멀티 페이지 앱(MPA)에서는 링크를 클릭할 때마다 서버에서 새 HTML 페이지를 내려받습니다. SPA(Single Page Application)는 브라우저의 History API를 활용해 페이지 전체를 새로 받지 않고, JavaScript로 URL과 UI를 동시에 변경합니다. React Router는 이 작업을 추상화한 라이브러리입니다.
// 설치
// npm install react-router-dom
// main.jsx — BrowserRouter로 앱 감싸기
import { BrowserRouter } from 'react-router-dom';
createRoot(document.getElementById('root')).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
📖 핵심 개념 2 — 중첩 라우트와 Outlet
// App.jsx — 라우트 구조 정의
import { Routes, Route, Link, Navigate } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// 코드 스플리팅: 각 페이지를 필요할 때만 로드
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));
const NotFound = lazy(() => import('./pages/NotFound'));
function App() {
return (
<Suspense fallback={<div>페이지 로딩 중...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* 중첩 라우트: /dashboard, /dashboard/profile, /dashboard/settings */}
<Route path="/dashboard" element={<DashboardLayout />}>
{/* index: /dashboard 접근 시 기본으로 표시 */}
<Route index element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* 인증 필요 라우트 */}
<Route
path="/admin"
element={
<RequireAuth>
<AdminPage />
</RequireAuth>
}
/>
{/* 404 처리 */}
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
);
}
// DashboardLayout — 중첩 라우트의 공통 레이아웃
function DashboardLayout() {
return (
<div className="dashboard">
<nav>
{/* Link는 a 태그 역할 — 페이지 새로고침 없이 이동 */}
<Link to="/dashboard">대시보드 홈</Link>
<Link to="/dashboard/profile">프로필</Link>
<Link to="/dashboard/settings">설정</Link>
</nav>
{/* Outlet: 중첩된 자식 Route의 컴포넌트가 여기에 렌더링됨 */}
<main><Outlet /></main>
</div>
);
}
📖 핵심 개념 3 — useParams, useNavigate, useLocation
import { useParams, useNavigate, useLocation } from 'react-router-dom';
// useParams — URL 파라미터 읽기
// Route: <Route path="/posts/:postId/comments/:commentId" element={...} />
function PostDetail() {
const { postId, commentId } = useParams();
// URL이 /posts/42/comments/7 이면 postId="42", commentId="7"
return <p>게시글 {postId}의 댓글 {commentId}</p>;
}
// useNavigate — 프로그래밍 방식 이동
function LoginForm() {
const navigate = useNavigate();
async function handleLogin(e) {
e.preventDefault();
const success = await doLogin();
if (success) {
navigate('/dashboard'); // 이동
// navigate('/dashboard', { replace: true }); // 히스토리 교체 (뒤로가기 방지)
// navigate(-1); // 뒤로 가기
// navigate(1); // 앞으로 가기
}
}
return <form onSubmit={handleLogin}>...</form>;
}
// useLocation — 현재 URL 정보
function NavLink({ to, children }) {
const location = useLocation();
const isActive = location.pathname === to;
// location.pathname: "/dashboard/profile"
// location.search: "?tab=recent"
// location.hash: "#section2"
// location.state: navigate로 전달한 state
return (
<Link
to={to}
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{children}
</Link>
);
}
📖 핵심 개념 4 — Protected Route
// hooks/useAuth.js
function useAuth() {
// 실제로는 Context나 Zustand에서 인증 상태를 가져옴
const token = localStorage.getItem('authToken');
return { isAuthenticated: !!token };
}
// components/RequireAuth.jsx
import { Navigate, useLocation } from 'react-router-dom';
function RequireAuth({ children }) {
const { isAuthenticated } = useAuth();
const location = useLocation();
if (!isAuthenticated) {
// 로그인 페이지로 리디렉션
// state에 현재 위치를 저장 → 로그인 후 원래 페이지로 돌아가기 위해
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children;
}
// LoginPage.jsx — 로그인 후 원래 페이지로 이동
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
// RequireAuth가 저장한 원래 경로, 없으면 대시보드로
const from = location.state?.from?.pathname || '/dashboard';
async function handleLogin() {
await doLogin();
navigate(from, { replace: true }); // 원래 페이지로 이동
}
return <button onClick={handleLogin}>로그인</button>;
}
⚠️ 흔한 실수 (よくあるミス)
- Link 대신 a 태그 사용:
<a href="/about">는 페이지 전체를 새로 로드합니다. SPA에서는 반드시<Link to="/about">를 사용하세요. - BrowserRouter를 여러 번 감싸기: 앱 전체를 하나의 BrowserRouter로 감싸야 합니다. 여러 번 감싸면 라우터 컨텍스트 충돌이 발생합니다.
- useParams 타입 주의: useParams가 반환하는 값은 항상 문자열입니다. 숫자 ID가 필요하면
parseInt(postId, 10)으로 변환하세요. - 중첩 라우트에서 Outlet 누락: 부모 Route에 Outlet을 렌더링하지 않으면 자식 Route가 표시되지 않습니다.
- v5와 v6 혼용: React Router v6에서
<Switch>는<Routes>로,exactprop은 불필요해졌습니다. v5 문법을 v6에서 사용하면 에러가 발생합니다.
💡 실무 팁
- NavLink 활용: React Router의
NavLink는 현재 경로와 일치하면 자동으로 active 클래스를 추가합니다. 네비게이션 메뉴에 활용하면 편리합니다. - 코드 스플리팅 기본 적용: 각 페이지를
lazy()로 감싸는 것을 기본 패턴으로 사용하면 초기 번들 크기가 크게 줄어 첫 로드 속도가 개선됩니다. - useSearchParams: URL 쿼리 파라미터(
?sort=asc&page=2)를 다루려면useSearchParams훅을 사용합니다. 검색, 필터, 페이지네이션 구현에 유용합니다. - 서버 설정 주의: BrowserRouter를 사용하면 서버에서 모든 경로를
index.html로 리다이렉트하도록 설정해야 합니다. 그렇지 않으면 URL 직접 입력 시 404가 발생합니다.