1 SPA와 라우팅
전통적인 웹 사이트에서 페이지를 이동하면 서버에서 새 HTML 파일을 받아 전체 페이지를 새로고침한다.
SPA(Single Page Application)에서는 HTML 파일이 하나이다. 페이지 전환은 JavaScript가 현재 페이지의 컴포넌트를 교체하는 방식으로 이루어진다. 서버에 새 HTML을 요청하지 않으므로 전환이 빠르고 부드럽다.
그러나 SPA도 URL이 변경되어야 한다. 사용자가 /monitoring 페이지를 북마크하거나, 뒤로가기 버튼을 누르거나, URL을 직접 입력해서 접근할 수 있어야 한다. React Router가 이 URL ↔︎ 컴포넌트 매핑을 담당한다.
전통 웹:
/home → 서버에서 home.html 로드
/monitoring → 서버에서 monitoring.html 로드
SPA + React Router:
/home → App 컴포넌트 안에서 HomePage 렌더링
/monitoring → App 컴포넌트 안에서 MonitoringPage 렌더링
2 설치와 기본 설정
2.1 라우트 정의
// src/main.tsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import App from "./App";
import HomePage from "./pages/HomePage";
import ChatPage from "./pages/ChatPage";
import MonitoringPage from "./pages/MonitoringPage";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{ index: true, element: <HomePage /> },
{ path: "chat", element: <ChatPage /> },
{ path: "monitoring", element: <MonitoringPage /> },
],
},
]);
createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />
);createBrowserRouter는 React Router v6.4 이상에서 권장하는 방식이다. 라우트를 객체 배열로 정의하여 구조가 명확하다.
2.2 레이아웃 컴포넌트
// src/App.tsx
import { Outlet } from "react-router-dom";
import Header from "./components/Header";
import Sidebar from "./components/Sidebar";
function App() {
return (
<div className="app">
<Header />
<div className="content">
<Sidebar />
<main>
<Outlet /> {/* 자식 라우트가 여기에 렌더링된다 */}
</main>
</div>
</div>
);
}<Outlet />은 현재 URL에 매칭되는 자식 라우트 컴포넌트가 렌더링되는 위치이다. URL이 /chat이면 <Outlet /> 자리에 <ChatPage />가 들어간다. Header와 Sidebar는 페이지가 바뀌어도 유지된다.
3 네비게이션
3.1 Link 컴포넌트
<a> 태그 대신 <Link>를 사용한다. <a>는 페이지를 새로고침하지만, <Link>는 SPA 내에서 컴포넌트만 교체한다.
import { Link, NavLink } from "react-router-dom";
function Sidebar() {
return (
<nav>
{/* Link — 기본 네비게이션 */}
<Link to="/">홈</Link>
{/* NavLink — 현재 경로와 매칭되면 active 클래스 자동 추가 */}
<NavLink
to="/chat"
className={({ isActive }) => isActive ? "nav-link active" : "nav-link"}
>
채팅
</NavLink>
<NavLink to="/monitoring" className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}>
모니터링
</NavLink>
</nav>
);
}NavLink는 Link의 확장으로, 현재 URL과 매칭 여부를 isActive로 제공한다. 네비게이션 메뉴에서 현재 페이지를 하이라이트할 때 사용한다.
4 동적 라우트와 URL 매개변수
URL의 일부를 변수로 받는다.
// 라우트 정의
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{ path: "agents/:agentName", element: <AgentPage /> },
{ path: "agents/:agentName/run/:runId", element: <RunDetailPage /> },
],
},
]);// 매개변수 사용
import { useParams } from "react-router-dom";
function AgentPage() {
const { agentName } = useParams<{ agentName: string }>();
return <h1>{agentName} 에이전트</h1>;
}
// /agents/qna_chatbot → agentName = "qna_chatbot"
// /agents/data_standardizer → agentName = "data_standardizer"4.1 쿼리 매개변수
URL 뒤의 ?key=value를 읽는다.
import { useSearchParams } from "react-router-dom";
function MonitoringPage() {
const [searchParams, setSearchParams] = useSearchParams();
const period = searchParams.get("period") || "7d";
const agent = searchParams.get("agent") || "all";
const changePeriod = (newPeriod: string) => {
setSearchParams({ period: newPeriod, agent });
};
return (
<div>
<p>기간: {period}, 에이전트: {agent}</p>
<button onClick={() => changePeriod("30d")}>30일</button>
</div>
);
}
// /monitoring?period=7d&agent=qna_chatbot5 중첩 라우팅
라우트 안에 라우트를 중첩하여 레이아웃을 계층적으로 구성한다.
const router = createBrowserRouter([
{
path: "/",
element: <App />, // 최상위 레이아웃 (Header + Sidebar)
children: [
{ index: true, element: <HomePage /> },
{
path: "agents/:agentName",
element: <AgentLayout />, // 에이전트별 레이아웃 (탭 네비게이션)
children: [
{ index: true, element: <AgentOverview /> },
{ path: "chat", element: <AgentChat /> },
{ path: "documents", element: <AgentDocuments /> },
{ path: "settings", element: <AgentSettings /> },
],
},
{ path: "monitoring", element: <MonitoringPage /> },
],
},
]);function AgentLayout() {
const { agentName } = useParams();
return (
<div>
<h1>{agentName}</h1>
<nav>
<NavLink to="">개요</NavLink>
<NavLink to="chat">채팅</NavLink>
<NavLink to="documents">문서</NavLink>
<NavLink to="settings">설정</NavLink>
</nav>
<Outlet /> {/* AgentOverview, AgentChat 등이 여기에 렌더링 */}
</div>
);
}중첩 라우팅에서 NavLink의 to 값은 상대 경로이다. to="chat"은 현재 경로 뒤에 /chat을 붙인다. 이 방식은 FastAPI의 APIRouter(prefix=...)와 유사하다.
6 404 처리
매칭되는 라우트가 없을 때 보여줄 페이지를 정의한다.
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{ index: true, element: <HomePage /> },
{ path: "chat", element: <ChatPage /> },
// ... 다른 라우트들
// 모든 미매칭 경로를 잡는다
{ path: "*", element: <NotFoundPage /> },
],
},
]);
function NotFoundPage() {
const navigate = useNavigate();
return (
<div>
<h1>페이지를 찾을 수 없다</h1>
<p>요청한 경로가 존재하지 않는다.</p>
<button onClick={() => navigate("/")}>홈으로</button>
</div>
);
}path: "*"는 와일드카드로 위에서 매칭되지 않은 모든 경로를 잡는다. 라우트 배열의 마지막에 위치해야 한다.
7 AI Agent 대시보드 라우트 구조 예시
실제 AI Agent 플랫폼에서 사용하는 라우트 구조이다.
const router = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [
{ index: true, element: <HomePage /> },
// 에이전트 페이지
{ path: "agents/qna_chatbot", element: <QnaChatbotPage /> },
{ path: "agents/data_standardizer", element: <DataStandardizerPage /> },
// 운영 페이지
{ path: "monitoring", element: <MonitoringPage /> },
{ path: "records", element: <RecordsPage /> },
// 실험 페이지
{ path: "experiments", element: <ExperimentsPage /> },
{ path: "experiments/:experimentId", element: <ExperimentDetailPage /> },
// 404
{ path: "*", element: <NotFoundPage /> },
],
},
]);이 구조에서 AppLayout은 Header, Sidebar를 포함하는 공통 레이아웃이고, <Outlet />에 각 페이지가 들어간다.
8 관련 주제
선행 지식
- React 기초 – 컴포넌트, Props, State
후속 주제
- React에서 API 호출 – 각 페이지에서 API 연동
다른 카테고리 연결
- FastAPI 입문 – APIRouter의 prefix 패턴과 React Router의 유사성
- CORS와 Proxy – SPA 개발 시 프록시 설정