라우팅 개요 (React Router)
SPA의 라우팅 개념, React Router v7의 기본 사용법, 그리고 4부 Next.js App Router와의 동작 방식 비교를 한 번에 다룹니다.
14장에서 성능 최적화 도구들을 다뤘습니다. 본 챕터는 2부의 마지막 챕터입니다. 지금까지 우리는 하나의 화면 안에서 일어나는 일들을 다뤘는데, 실제 앱은 보통 여러 화면을 갖습니다. 메뉴 클릭에 따라 화면이 바뀌고, URL이 바뀌고, 뒤로 가기 버튼도 동작해야 합니다. 이런 화면 전환을 다루는 도구가 라우팅입니다.
본 챕터에서 다룰 React Router의 모델은 4부(모던 Next.js)의 App Router와 비교 가능한 형태입니다. 본 챕터 마지막에 두 모델의 의사결정 표를 두어 “어느 경우에 무엇이 적절한지"의 감각을 잡아 두겠습니다.
전통적인 웹 vs SPA #
전통적인 웹 페이지는 사용자가 링크를 클릭할 때마다 브라우저가 서버에 새 페이지를 요청하고, 서버가 만든 새 HTML을 받아 화면 전체를 다시 그렸습니다. 페이지 전환마다 흰 깜빡임이 생기는 그 방식입니다.
**SPA (Single Page Application)**는 처음 한 번 HTML을 받은 뒤, 이후의 화면 전환은 자바스크립트로 화면을 다시 그리는 방식입니다. 서버에 새 HTML을 요청하지 않고 클라이언트가 알아서 화면을 갈아 끼우니, 전환이 빠르고 부드럽습니다.
Vite로 만든 기본 리액트 앱은 SPA입니다. SPA는 처음 받은 그 HTML 안에서 모든 일이 일어나므로, “URL이 바뀌면 어떤 화면을 보여줄지"를 우리가 직접 정해 줘야 합니다. 이걸 클라이언트 사이드 라우팅이라고 부르고, 리액트 생태계의 사실상 표준 라이브러리가 React Router입니다.
React Router 설치 #
지금까지 쓰던 Vite 프로젝트에 React Router를 추가합니다.
pnpm add react-router이 책은 React Router v7을 기준으로 합니다. v7부터 단일 react-router 패키지로 통합되었고, 옛 자료에서 자주 보이는 react-router-dom은 더 이상 별도 패키지가 아닙니다. 예전 코드를 마이그레이션할 때는 import만 react-router-dom → react-router로 바꾸면 API는 거의 그대로 동작합니다.
가장 단순한 예시 #
라우팅의 기본 구조를 먼저 보겠습니다.
import { BrowserRouter, Routes, Route, Link } from 'react-router';
function Home() {
return <h1>홈 페이지</h1>;
}
function About() {
return <h1>소개 페이지</h1>;
}
function App() {
return (
<BrowserRouter>
<nav style={{ padding: '8px', borderBottom: '1px solid #ccc' }}>
<Link to="/">홈</Link>
{' | '}
<Link to="/about">소개</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
export default App;핵심 요소들:
<BrowserRouter>— 라우팅을 활성화하는 최상위 래퍼. 앱 전체를 감쌈<Routes>— 여러<Route>중 현재 URL과 일치하는 하나를 골라 렌더링하는 컨테이너<Route path="..." element={<...>} />— 어떤 경로에 어떤 컴포넌트를 보여줄지 정의<Link to="...">— 화면 깜빡임 없이 라우트를 전환하는 링크
<a href="/about"> 같은 일반 anchor 태그를 쓰면 브라우저가 페이지를 새로 로드해 SPA의 장점을 잃습니다. 반드시 <Link>를 써야 클라이언트 사이드 전환이 일어납니다.
URL 파라미터 — 동적 경로 #
상품 상세 페이지나 사용자 프로필처럼, URL의 일부가 동적으로 바뀌는 경로가 있습니다. 콜론 (:)을 붙여 동적 부분을 표시합니다.
<Route path="/users/:userId" element={<UserProfile />} />/users/123, /users/cheolsu 같은 URL이 모두 이 라우트에 매칭됩니다. 컴포넌트 안에서는 useParams 훅으로 동적 부분의 값을 꺼냅니다.
import { useParams } from 'react-router';
function UserProfile() {
const { userId } = useParams();
return <h1>사용자 ID: {userId}</h1>;
}
export default UserProfile;useParams는 path에 명시된 파라미터를 모두 객체로 돌려줍니다. path="/users/:userId/posts/:postId" 라면 { userId, postId }를 꺼낼 수 있습니다.
프로그래매틱 네비게이션 — useNavigate #
링크 외에도 코드로 직접 이동시켜야 할 때가 있습니다. 폼 제출 후 결과 페이지로 이동하거나, 로그아웃 버튼이 홈으로 보내는 식입니다. useNavigate 훅을 씁니다.
import { useState } from 'react';
import { useNavigate } from 'react-router';
function LoginForm() {
const [email, setEmail] = useState('');
const navigate = useNavigate();
function handleSubmit(e) {
e.preventDefault();
// ... 로그인 처리 ...
navigate('/dashboard');
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit">로그인</button>
</form>
);
}navigate('/path')로 이동하고, navigate(-1)이면 뒤로 가기, navigate(1)이면 앞으로 가기입니다.
쿼리 파라미터 — useSearchParams #
URL의 ?key=value&key2=value2 부분 (쿼리 스트링)을 다룰 때는 useSearchParams를 씁니다.
import { useSearchParams } from 'react-router';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';
return (
<div>
<input
value={query}
onChange={(e) => setSearchParams({ q: e.target.value })}
placeholder="검색어"
/>
<p>현재 검색어: {query}</p>
</div>
);
}useSearchParams는 useState와 비슷한 인터페이스로 동작합니다. 입력에 따라 URL 자체가 /search?q=리액트처럼 갱신되고, 새로고침하거나 URL을 공유해도 같은 상태가 복원됩니다. 검색 결과 페이지처럼 “URL에 상태가 반영되어야 하는” 경우에 유용합니다.
중첩 라우트와 Outlet #
여러 페이지가 같은 레이아웃 (헤더, 사이드바 등)을 공유할 때는 중첩 라우트가 깔끔합니다. 부모 라우트가 공통 레이아웃을 그리고, 자식 라우트가 그 안의 콘텐츠 영역을 채우는 구조입니다.
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users/:userId" element={<UserProfile />} />
</Route>
</Routes>import { Link, Outlet } from 'react-router';
function Layout() {
return (
<div>
<header style={{ padding: '8px', background: '#f4f4f4' }}>
<Link to="/">홈</Link>
{' | '}
<Link to="/about">소개</Link>
</header>
<main style={{ padding: '16px' }}>
<Outlet />
</main>
</div>
);
}<Outlet />이 있는 위치에 자식 라우트의 컴포넌트가 렌더링됩니다. <Route index>는 부모 경로 (/)와 정확히 매치될 때 보여 줄 자식을 의미합니다.
이 패턴 덕에 헤더 / 푸터 코드를 한 곳에 두고도, URL에 따라 가운데 콘텐츠만 바꿀 수 있습니다. 4부의 Next.js App Router도 같은 동작 원리 (layout + 자식 페이지)를 파일 시스템 기반으로 자동화한 것입니다.
404페이지 #
매칭되는 라우트가 없을 때의 페이지를 만들려면 path="*"로 와일드카드 라우트를 마지막에 둡니다.
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>위에서부터 매칭을 시도하다 어디에도 안 맞으면 *가 잡아냅니다.
활성 링크 표시 — NavLink #
네비게이션 바에서 현재 페이지 링크를 강조하고 싶을 때 <Link> 대신 <NavLink>를 씁니다.
import { NavLink } from 'react-router';
function Nav() {
return (
<nav>
<NavLink
to="/"
end
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'tomato' : 'inherit',
})}
>
홈
</NavLink>
{' | '}
<NavLink
to="/about"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'tomato' : 'inherit',
})}
>
소개
</NavLink>
</nav>
);
}style (또는 className)에 함수를 전달하면 isActive 정보를 받아 분기할 수 있습니다. end prop은 “정확히 이 경로일 때만 활성"이라는 뜻으로, / 같은 루트 경로에서 자주 씁니다. 안 붙이면 모든 하위 경로에서도 활성으로 잡힙니다.
React Router vs Next.js App Router — 의사결정 #
이 책의 4부는 Next.js App Router를 다룹니다. 본 챕터의 React Router와 같은 문제 (URL에 따른 화면 전환)를 다른 방식으로 푸는 도구입니다. 두 동작 방식을 짧게 비교해 둡니다.
| 항목 | React Router (v7) | Next.js App Router |
|---|---|---|
| 라우트 정의 | <Route path> 컴포넌트 트리 | 파일 시스템 (app/users/[userId]/page.tsx) |
| 동적 파라미터 | :userId + useParams | [userId] 폴더 + params prop |
| 레이아웃 | <Outlet /> + 중첩 라우트 | app/layout.tsx (자동 중첩) |
| 데이터 페칭 | 컴포넌트 안 useEffect (또는 v7의 loader API) | Server Component 함수 본문에서 직접 fetch |
| SSR | 옵션 (Framework Mode 추가 셋업 필요) | 기본 |
| 빌드 결과 | 클라이언트 SPA | RSC + 클라이언트 컴포넌트 혼합 |
| 학습 곡선 | 비교적 단순 | App Router + RSC 모델 학습 필요 |
| 적합한 경우 | 빠르게 만드는 SPA, 클라이언트만 동작하는 도구 / 대시보드 | SEO가 중요한 서비스, 풀스택 앱, server-first 모델 |
본 챕터의 React Router는 클라이언트 사이드 SPA에 적합합니다. SEO 요구가 낮고, 서버 데이터 페칭이 단순하고, 빠르게 SPA 한 개 띄우면 되는 경우입니다. Next.js App Router는 풀스택 앱과 SEO가 중요한 서비스에 적합합니다.
이 책의 6부(풀스택 Todo capstone)는 Next.js로 만듭니다. 이 챕터의 React Router는 “리액트만으로 SPA 라우팅을 어떻게 다루는가"의 토대를 잡는 단계입니다.
직접 해보기 #
지금까지 배운 것들을 종합해 작은 미니 사이트를 만들어 봅시다. 홈, 소개, 사용자 목록, 사용자 상세 4개 페이지가 있습니다.
src/Layout.jsx:
import { NavLink, Outlet } from 'react-router';
function Layout() {
const linkStyle = ({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'tomato' : 'inherit',
marginRight: '12px',
});
return (
<div>
<header style={{ padding: '12px', background: '#f4f4f4', borderBottom: '1px solid #ccc' }}>
<NavLink to="/" end style={linkStyle}>홈</NavLink>
<NavLink to="/about" style={linkStyle}>소개</NavLink>
<NavLink to="/users" style={linkStyle}>사용자</NavLink>
</header>
<main style={{ padding: '16px' }}>
<Outlet />
</main>
</div>
);
}
export default Layout;src/pages/Home.jsx:
function Home() {
return (
<div>
<h1>홈</h1>
<p>리액트 라우터 미니 사이트입니다.</p>
</div>
);
}
export default Home;src/pages/UserList.jsx:
import { Link } from 'react-router';
const USERS = [
{ id: 1, name: '철수' },
{ id: 2, name: '영희' },
{ id: 3, name: '민수' },
];
function UserList() {
return (
<div>
<h1>사용자 목록</h1>
<ul>
{USERS.map(user => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
</div>
);
}
export default UserList;src/pages/UserDetail.jsx:
import { useParams, useNavigate } from 'react-router';
const USERS = {
1: { name: '철수', email: 'cheolsu@example.com' },
2: { name: '영희', email: 'younghee@example.com' },
3: { name: '민수', email: 'minsu@example.com' },
};
function UserDetail() {
const { userId } = useParams();
const navigate = useNavigate();
const user = USERS[userId];
if (!user) {
return (
<div>
<h1>사용자를 찾을 수 없습니다.</h1>
<button onClick={() => navigate('/users')}>목록으로</button>
</div>
);
}
return (
<div>
<h1>{user.name}</h1>
<p>이메일: {user.email}</p>
<button onClick={() => navigate(-1)}>뒤로</button>
</div>
);
}
export default UserDetail;src/pages/NotFound.jsx:
import { Link } from 'react-router';
function NotFound() {
return (
<div>
<h1>404 — 페이지를 찾을 수 없습니다</h1>
<Link to="/">홈으로 돌아가기</Link>
</div>
);
}
export default NotFound;src/App.jsx:
import { BrowserRouter, Routes, Route } from 'react-router';
import Layout from './Layout';
import Home from './pages/Home';
import UserList from './pages/UserList';
import UserDetail from './pages/UserDetail';
import NotFound from './pages/NotFound';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="users" element={<UserList />} />
<Route path="users/:userId" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;저장하고 브라우저에서 확인해 보세요.
- 헤더 메뉴를 클릭하면 새로고침 없이 화면이 바뀝니다
- 현재 페이지의 링크는 굵은 글씨에 토마토색
- 사용자 목록에서 이름 클릭 → 동적 URL (
/users/1)로 이동 → 상세 페이지 - “뒤로” 버튼을 누르면 브라우저 뒤로 가기
- 주소창에
/존재하지않는경로를 직접 입력하면 404페이지
지금까지 이 책의 1~2부에서 배운 거의 모든 것 (컴포넌트 분리, props, useState, 이벤트 처리, 조건부 / 리스트 렌더링)이 한 사이트에 들어 있습니다.
연습문제 #
- 위 미니 사이트에 검색 기능을 추가해 보세요.
/users페이지에 검색창을 두고, 입력값을useSearchParams로 URL에?q=...형태로 반영합니다. 새로고침하거나 URL을 공유해도 검색 상태가 복원되는지 확인합니다. - 보호된 라우트 만들기.
useState로isLoggedIn을 다루는 간단한 인증 흐름을 만들고,/admin라우트에 접근할 때isLoggedIn === false이면 자동으로/login으로 리다이렉트 (<Navigate to="/login" />) 하도록 만들어 보세요. 32장 (인증과 세션)의 토대가 됩니다. - React Router와 Next.js App Router 비교. 위 미니 사이트를 머릿속에서 Next.js App Router로 옮긴다면
app/디렉토리 구조가 어떻게 될지 짧게 적어 보세요. 예)app/layout.tsx,app/page.tsx,app/users/page.tsx,app/users/[userId]/page.tsx. 4부 22~23장 진입 전에 한 번 그려 두면 도움이 됩니다.
한 줄 요약: SPA는 화면 전환을 클라이언트에서 처리한다. React Router의 핵심은
BrowserRouter,Routes,Route,Link/NavLink. 동적 경로는:param+useParams, 프로그래매틱 이동은useNavigate, 쿼리 파라미터는useSearchParams, 공통 레이아웃은 중첩 라우트 +<Outlet />, 404는path="*". SEO가 중요하고 풀스택이라면 4부의 Next.js App Router가 더 적합하다.
다음 챕터 #
본 챕터로 2부 효과 · 상태 · 라우팅이 마무리됩니다. 1부의 컴포넌트 / props / state / 이벤트 / 폼에 더해, 2부에서 useEffect / 상태 끌어올리기 / Context / 커스텀 훅 / 성능 / 라우팅까지 손에 넣었습니다. 라이브러리 없이 작은 SPA를 처음부터 끝까지 만들 수 있는 도구를 갖춘 셈입니다.
다음 16장 TypeScript + React 셋업부터 3부가 시작됩니다. 지금까지의 모든 코드를 TypeScript 위에 다시 올립니다. props · 훅 · 이벤트 · 폼 · Context · API 응답 타이핑을 6 챕터에 걸쳐 차례로 다룹니다.