C.W.K.
Stream
Lesson 06 of 07 · published

React Router 로 SPA 라우팅

~18 min · react-router, routing, spa, history-api

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
SPA 는 HTML 한 장. 라우터 없으면 네비게이션 = 전체 리로드. 라우터 있으면 URL 만 바뀌고 뷰가 교체되고, 브라우저 뒤로가기는 사용자가 기대한 대로 동작해.

라우터가 푸는 문제

Vite 앱은 모든 URL 에 index.html 한 장 줘. 브라우저가 main.tsx 로드, <App /> 마운트, 끝. 사용자가 /conversations/abc 링크 클릭하면 브라우저가 전체 리로드 (React 상태 파괴) 하고 서버는 똑같은 index.html 다시 보냄. 매번 시작점.

클라이언트 사이드 라우터가 그 클릭 인터셉트해서 history.pushState() 로 URL 갱신, React 한테 새 URL 기반으로 다른 컴포넌트 트리 렌더하라고 말함. 리로드 없음. 상태 생존. 뒤로가기 동작.

React Router 핵심 5개

  1. <BrowserRouter> 가 앱 감싸고 라우터 context 노출.
  2. <Routes> + <Route> 가 URL → 컴포넌트 매핑 선언.
  3. <Link> 가 클릭에 리로드 대신 pushState 호출하는 앵커 렌더.
  4. useParams() 가 routed 컴포넌트 안에서 동적 URL 세그먼트 읽음.
  5. useNavigate() 가 프로그래매틱 네비게이션용 함수 반환 (예: form submit 후).

최소 앱

라우트는 플랫 또는 nested 가능. Nested 라우트는 부모 컴포넌트를 레이아웃으로 쓰고 자식이 채울 자리에 <Outlet /> 렌더. 사이드바 + 메인 패널이 페이지 전환에도 마운트된 채 유지되는 패턴 (cwkPippa 채팅 UI 처럼).

NavLink vs Link

NavLink 는 자기 target 이 현재 URL 과 매치되는지 아는 Link. function-as-className 으로 active 상태 노출해서 매칭 로직 직접 안 쓰고 nav bar 의 현재 아이템 하이라이트 가능.

서버 사이드 함정: catch-all rewrite

SPA 배포 후 https://yourapp.com/conversations/abc 같은 URL 직접 진입하면 서버에 도착. 서버는 그 경로에 파일 없으니 404. 해결: asset 아닌 모든 경로를 /index.html 로 rewrite 하도록 호스트 설정. Vercel, Cloudflare Pages, Netlify 다 한 줄 설정 있어. Tauri 앱은 필요 없음 (서버 없음).

일부 SPA 는 라우팅 자체가 필요 없어. 나중에 만들 Cinder 스타일 Tauri 앱은 창 하나에 뷰 하나. 백엔드 conversation 에 바인딩, candidate board 보여줌, 이벤트에 반응. URL 없음, React Router 없음. 튜토리얼이 시키니까 라우터 넣지 마 — 실제로 URL 로 주소 지정 가능한 뷰가 여러 개일 때 추가.

대안들

TanStack Router 가 떠오르는 도전자 (타입 안전한 라우트, 통합된 search params). Wouter 가 미니멀리스트 (1.5KB, hook 기반). React Router 가 디폴트. TS 라우트 추론 1급으로 원하면 TanStack, 진짜 KB 깎아야 하면 Wouter. 그 외엔 React Router.

Code

main.tsx — 앱을 라우터로 감싸기·tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
App.tsx — 레이아웃을 가진 플랫 + nested 라우트·tsx
import { Routes, Route, Outlet, NavLink, useParams } from "react-router-dom";

function Layout() {
  return (
    <div className="flex min-h-screen">
      <nav className="w-48 border-r p-4 space-y-2">
        <NavLink
          to="/"
          end
          className={({ isActive }) => isActive ? "text-brand" : "text-muted"}
        >
          Home
        </NavLink>
        <NavLink
          to="/conversations"
          className={({ isActive }) => isActive ? "text-brand" : "text-muted"}
        >
          Conversations
        </NavLink>
      </nav>
      <main className="flex-1 p-6">
        <Outlet />
      </main>
    </div>
  );
}

function Home() { return <h1>Welcome</h1>; }
function Conversations() { return <h1>List</h1>; }
function Conversation() {
  const { id } = useParams<{ id: string }>();
  return <h1>Conversation {id}</h1>;
}

export default function App() {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route path="/" element={<Home />} />
        <Route path="/conversations" element={<Conversations />} />
        <Route path="/conversations/:id" element={<Conversation />} />
        <Route path="*" element={<p>404</p>} />
      </Route>
    </Routes>
  );
}

External links

Exercise

Bootstrap 프로젝트에 react-router-dom 설치. 위 레이아웃 + 뷰 셋 (Home, Conversations, Conversation/:id) 연결. 확인: (1) NavLink 클릭이 리로드 없이 URL 갱신; (2) 뒤로가기 동작; (3) /conversations/abc 페이지에서 새로고침해도 올바른 뷰 보임 (로컬에선 Vite 가 처리; 빌드 테스트면 레슨에 언급된 서버 rewrite 필요).
Hint
Deep URL refresh 가 404 면 dev server 는 OK (Vite 가 처리). 프로덕션은 _redirects 파일 (Netlify), vercel.json rewrite, 또는 동등한 설정 필요. Track 8 에서 다룸.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.