React 요약 10 - 라우팅 (React Router)
Updated:
기본 라우팅 설정
import { BrowserRouter, Routes, Route, Link, Outlet, useParams, useNavigate } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<div className="app">
<Header />
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/products" element={<Products />}>
<Route index element={<ProductList />} />
<Route path=":productId" element={<ProductDetail />} />
</Route>
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
<Footer />
</div>
</BrowserRouter>
);
}
function Header() {
return (
<header>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/products">Products</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
</header>
);
}
function Home() {
return (
<div>
<h1>Home Page</h1>
<p>Welcome to our website!</p>
</div>
);
}
function About() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company.</p>
</div>
);
}
function Products() {
return (
<div>
<h1>Products</h1>
{/* Outlet은 중첩된 라우트의 컴포넌트를 렌더링 */}
<Outlet />
</div>
);
}
function ProductList() {
return (
<div>
<h2>Product List</h2>
<ul>
<li><Link to="/products/1">Product 1</Link></li>
<li><Link to="/products/2">Product 2</Link></li>
<li><Link to="/products/3">Product 3</Link></li>
</ul>
</div>
);
}
function ProductDetail() {
// URL 파라미터 가져오기
const { productId } = useParams();
// 프로그래매틱 네비게이션을 위한 navigate 함수
const navigate = useNavigate();
return (
<div>
<h2>Product Detail: {productId}</h2>
<p>This is the detail page for product {productId}.</p>
<button onClick={() => navigate('/products')}>
Back to Products
</button>
</div>
);
}
function Contact() {
return (
<div>
<h1>Contact Us</h1>
<p>Get in touch with our team.</p>
</div>
);
}
function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link to="/">Go Home</Link>
</div>
);
}
function Footer() {
return (
<footer>
<p>© 2023 My React App</p>
</footer>
);
}
코멘트: React Router는 React 애플리케이션에서 클라이언트 사이드 라우팅을 구현하는 가장 인기 있는 라이브러리입니다. 주요 컴포넌트와 기능은 다음과 같습니다:
BrowserRouter
: HTML5 History API를 사용하여 UI를 URL과 동기화Routes
: 여러Route
컴포넌트를 그룹화하고 현재 URL과 일치하는 첫 번째 라우트를 렌더링Route
: URL 패턴과 렌더링할 컴포넌트를 매핑Link
: 페이지를 새로고침하지 않고 다른 라우트로 이동하는 링크 생성Outlet
: 중첩 라우트의 자식 컴포넌트를 렌더링하는 위치 지정useParams
: URL 파라미터 접근useNavigate
: 프로그래매틱 네비게이션 (코드에서 페이지 이동)
라우트 보호 및 인증
import { useState, createContext, useContext } from 'react';
import { BrowserRouter, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
// 인증 컨텍스트 생성
const AuthContext = createContext(null);
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const login = (username, password) => {
// 실제로는 API 호출 등을 통해 인증
return new Promise((resolve, reject) => {
setTimeout(() => {
if (username === 'admin' && password === 'password') {
const user = { id: 1, username, name: 'Admin User', role: 'admin' };
setUser(user);
resolve(user);
} else if (username === 'user' && password === 'password') {
const user = { id: 2, username, name: 'Regular User', role: 'user' };
setUser(user);
resolve(user);
} else {
reject(new Error('Invalid credentials'));
}
}, 1000);
});
};
const logout = () => {
setUser(null);
};
return (
<AuthContext.Provider value=>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
return useContext(AuthContext);
}
// 보호된 라우트 컴포넌트
function ProtectedRoute({ children, requiredRole }) {
const { user } = useAuth();
const location = useLocation();
if (!user) {
// 로그인되지 않은 경우 로그인 페이지로 리디렉션
// state를 통해 원래 가려던 경로 전달
return <Navigate to="/login" state= replace />;
}
if (requiredRole && user.role !== requiredRole) {
// 필요한 역할이 없는 경우 권한 없음 페이지로 리디렉션
return <Navigate to="/unauthorized" replace />;
}
return children;
}
function App() {
return (
<AuthProvider>
<BrowserRouter>
<div className="app">
<Header />
<main>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/logout" element={<Logout />} />
<Route path="/unauthorized" element={<Unauthorized />} />
{/* 로그인이 필요한 라우트 */}
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
{/* 특정 역할이 필요한 라우트 */}
<Route
path="/admin"
element={
<ProtectedRoute requiredRole="admin">
<AdminPanel />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</div>
</BrowserRouter>
</AuthProvider>
);
}
function Header() {
const { user } = useAuth();
return (
<header>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
{user ? (
<>
<li><Link to="/dashboard">Dashboard</Link></li>
{user.role === 'admin' && (
<li><Link to="/admin">Admin Panel</Link></li>
)}
<li><Link to="/logout">Logout ({user.name})</Link></li>
</>
) : (
<li><Link to="/login">Login</Link></li>
)}
</ul>
</nav>
</header>
);
}
function Home() {
return (
<div>
<h1>Home Page</h1>
<p>Welcome to our application!</p>
</div>
);
}
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const location = useLocation();
// 로그인 후 리디렉션할 경로
const from = location.state?.from?.pathname || '/dashboard';
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
await login(username, password);
navigate(from, { replace: true });
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div>
<h1>Login</h1>
{error && <p className="error">{error}</p>}
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username:</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
<p>Hint: Use "admin/password" or "user/password"</p>
</div>
);
}
function Logout() {
const { logout } = useAuth();
const navigate = useNavigate();
// 컴포넌트 마운트 시 로그아웃 처리
useEffect(() => {
logout();
navigate('/');
}, [logout, navigate]);
return <div>Logging out...</div>;
}
function Dashboard() {
const { user } = useAuth();
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {user.name}!</p>
<p>This is your personal dashboard.</p>
</div>
);
}
function AdminPanel() {
return (
<div>
<h1>Admin Panel</h1>
<p>This area is restricted to administrators only.</p>
</div>
);
}
function Unauthorized() {
return (
<div>
<h1>Unauthorized</h1>
<p>You do not have permission to access this page.</p>
<Link to="/">Go Home</Link>
</div>
);
}
function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link to="/">Go Home</Link>
</div>
);
}
코멘트: 라우트 보호는 인증 및 권한 부여가 필요한 애플리케이션에서 중요한 패턴입니다. 이 예제에서는 다음과 같은 핵심 개념을 보여줍니다:
AuthContext
를 사용한 인증 상태 관리ProtectedRoute
컴포넌트를 통한 라우트 보호- 역할 기반 접근 제어(RBAC)
- 로그인 후 원래 가려던 페이지로 리디렉션
- 조건부 UI 렌더링 (사용자 역할에 따라 다른 메뉴 표시)
실제 애플리케이션에서는 JWT나 세션 쿠키와 같은 인증 토큰을 사용하고, 토큰 만료 처리, 자동 로그인 등의 기능을 추가해야 합니다.
라우트 데이터 로딩
import { createBrowserRouter, RouterProvider, useLoaderData, useParams, Link } from 'react-router-dom';
// 데이터 로더 함수
async function postsLoader() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('Failed to fetch posts');
}
return response.json();
}
async function postLoader({ params }) {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.postId}`);
if (!response.ok) {
throw new Error(`Failed to fetch post ${params.postId}`);
}
return response.json();
}
// 컴포넌트
function Root() {
return (
<div className="app">
<header>
<nav>
<Link to="/">Home</Link>
<Link to="/posts">Posts</Link>
</nav>
</header>
<main>
<Outlet />
</main>
<footer>
<p>© 2023 My Blog</p>
</footer>
</div>
);
}
function Home() {
return (
<div>
<h1>Welcome to My Blog</h1>
<p>Check out our latest posts!</p>
<Link to="/posts">View Posts</Link>
</div>
);
}
function Posts() {
// useLoaderData를 통해 로더 함수에서 반환된 데이터 접근
const posts = useLoaderData();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}
function Post() {
const post = useLoaderData();
return (
<div>
<h1>{post.title}</h1>
<p>{post.body}</p>
<Link to="/posts">Back to Posts</Link>
</div>
);
}
function ErrorBoundary() {
const error = useRouteError();
return (
<div className="error-container">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p><i>{error.statusText || error.message}</i></p>
<Link to="/">Go Home</Link>
</div>
);
}
// 라우터 설정
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorBoundary />,
children: [
{
index: true,
element: <Home />
},
{
path: "posts",
element: <Posts />,
loader: postsLoader,
errorElement: <ErrorBoundary />
},
{
path: "posts/:postId",
element: <Post />,
loader: postLoader,
errorElement: <ErrorBoundary />
}
]
}
]);
function App() {
return <RouterProvider router={router} />;
}
코멘트: React Router v6.4 이상에서는 데이터 로딩을 위한 새로운 패턴을 도입했습니다. 이 패턴의 주요 특징은 다음과 같습니다:
loader
함수: 라우트 렌더링 전에 데이터를 미리 로드useLoaderData
Hook: 로더 함수에서 반환된 데이터에 접근errorElement
: 로딩 중 오류 발생 시 표시할 UIuseRouteError
Hook: 오류 정보에 접근이 접근 방식의 장점은 다음과 같습니다:
- 데이터 로딩 로직과 UI 로직의 분리
- 라우트 전환 전에 데이터 로딩 시작 (더 빠른 사용자 경험)
- 오류 처리의 일관성
- 서버 사이드 렌더링과의 호환성
중첩 라우팅과 레이아웃
import { BrowserRouter, Routes, Route, Link, Outlet, useLocation } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
{/* 기본 레이아웃 */}
<Route path="/" element={<MainLayout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
{/* 대시보드 레이아웃 */}
<Route path="dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
{/* 중첩된 설정 페이지 */}
<Route path="settings/:section" element={<SettingsSection />} />
</Route>
{/* 인증 레이아웃 */}
<Route path="auth" element={<AuthLayout />}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="forgot-password" element={<ForgotPassword />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
// 메인 레이아웃
function MainLayout() {
return (
<div className="main-layout">
<MainHeader />
<div className="content">
<Outlet />
</div>
<MainFooter />
</div>
);
}
function MainHeader() {
// 현재 경로에 따라 활성 링크 스타일 지정
const location = useLocation();
const isActive = (path) => {
return location.pathname === path ? 'active' : '';
};
return (
<header className="main-header">
<div className="logo">My App</div>
<nav>
<ul>
<li>
<Link to="/" className={isActive('/')}>Home</Link>
</li>
<li>
<Link to="/about" className={isActive('/about')}>About</Link>
</li>
<li>
<Link to="/dashboard" className={isActive('/dashboard')}>Dashboard</Link>
</li>
<li>
<Link to="/auth/login" className={isActive('/auth/login')}>Login</Link>
</li>
</ul>
</nav>
</header>
);
}
function MainFooter() {
return (
<footer className="main-footer">
<p>© 2023 My App. All rights reserved.</p>
</footer>
);
}
// 대시보드 레이아웃
function DashboardLayout() {
const location = useLocation();
const isActive = (path) => {
return location.pathname === path ? 'active' : '';
};
return (
<div className="dashboard-layout">
<h1>Dashboard</h1>
<div className="dashboard-container">
<aside className="sidebar">
<nav>
<ul>
<li>
<Link to="/dashboard" className={isActive('/dashboard')}>
Dashboard Home
</Link>
</li>
<li>
<Link to="/dashboard/profile" className={isActive('/dashboard/profile')}>
Profile
</Link>
</li>
<li>
<Link to="/dashboard/settings" className={isActive('/dashboard/settings')}>
Settings
</Link>
</li>
</ul>
</nav>
</aside>
<main className="dashboard-content">
<Outlet />
</main>
</div>
</div>
);
}
// 인증 레이아웃
function AuthLayout() {
return (
<div className="auth-layout">
<div className="auth-container">
<div className="auth-form-container">
<Outlet />
</div>
<div className="auth-info">
<h2>Welcome to My App</h2>
<p>Sign in to access all features.</p>
</div>
</div>
</div>
);
}
// 페이지 컴포넌트들
function Home() {
return (
<div>
<h1>Home Page</h1>
<p>Welcome to our application!</p>
</div>
);
}
function About() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company and mission.</p>
</div>
);
}
function DashboardHome() {
return (
<div>
<h2>Dashboard Overview</h2>
<p>Welcome to your dashboard!</p>
<div className="dashboard-stats">
{/* 대시보드 통계 및 위젯 */}
</div>
</div>
);
}
function Profile() {
return (
<div>
<h2>User Profile</h2>
<p>Manage your profile information.</p>
{/* 프로필 폼 */}
</div>
);
}
function Settings() {
return (
<div>
<h2>Settings</h2>
<p>Configure your account settings.</p>
<div className="settings-sections">
<Link to="/dashboard/settings/account">Account Settings</Link>
<Link to="/dashboard/settings/notifications">Notification Settings</Link>
<Link to="/dashboard/settings/privacy">Privacy Settings</Link>
</div>
</div>
);
}
function SettingsSection() {
const { section } = useParams();
return (
<div>
<h2>{section.charAt(0).toUpperCase() + section.slice(1)} Settings</h2>
<Link to="/dashboard/settings">Back to Settings</Link>
{/* 섹션별 설정 UI */}
{section === 'account' && (
<div>
<h3>Account Settings</h3>
{/* 계정 설정 폼 */}
</div>
)}
{section === 'notifications' && (
<div>
<h3>Notification Preferences</h3>
{/* 알림 설정 폼 */}
</div>
)}
{section === 'privacy' && (
<div>
<h3>Privacy Controls</h3>
{/* 개인정보 설정 폼 */}
</div>
)}
</div>
);
}
function Login() {
return (
<div>
<h2>Login</h2>
<form>
{/* 로그인 폼 */}
<div>
<label htmlFor="email">Email:</label>
<input type="email" id="email" />
</div>
<div>
<label htmlFor="password">Password:</label>
<input type="password" id="password" />
</div>
<button type="submit">Login</button>
</form>
<div className="auth-links">
<Link to="/auth/forgot-password">Forgot Password?</Link>
<Link to="/auth/register">Create an Account</Link>
</div>
</div>
);
}
function Register() {
return (
<div>
<h2>Create an Account</h2>
<form>
{/* 회원가입 폼 */}
</form>
<div className="auth-links">
<Link to="/auth/login">Already have an account? Login</Link>
</div>
</div>
);
}
function ForgotPassword() {
return (
<div>
<h2>Forgot Password</h2>
<form>
{/* 비밀번호 찾기 폼 */}
</form>
<div className="auth-links">
<Link to="/auth/login">Back to Login</Link>
</div>
</div>
);
}
function NotFound() {
return (
<div>
<h1>404 - Page Not Found</h1>
<p>The page you are looking for does not exist.</p>
<Link to="/">Go Home</Link>
</div>
);
}
코멘트: 중첩 라우팅은 복잡한 UI 구조를 구성하는 강력한 방법입니다. 이 패턴의 주요 이점은 다음과 같습니다:
- 레이아웃 재사용: 공통 UI 요소(헤더, 사이드바, 푸터 등)를 여러 페이지에서 재사용
- UI 계층 구조: 애플리케이션의 논리적 구조를 라우트 구조로 표현
- 코드 구성: 관련 컴포넌트와 라우트를 함께 그룹화하여 코드 구성 개선
- 점진적 UI 로딩: 상위 레이아웃은 유지하면서 내부 콘텐츠만 변경
Outlet
컴포넌트는 중첩 라우팅의 핵심으로, 자식 라우트의 컴포넌트가 렌더링될 위치를 지정합니다. 이를 통해 복잡한 다단계 UI를 구성할 수 있습니다.
Leave a comment