본문 바로가기

React Router 사용법

React Router 8.3 Declarative mode에서 route tree 매칭, 중첩 Outlet, URL 값, 내비게이션과 직접 진입의 책임 경계를 구현합니다.

React Router는 현재 URL을 route tree에 맞추고, 선택된 React element tree를 렌더링하도록 연결하는 라우팅 라이브러리입니다. React 자체가 라우터인 것은 아니며, 서버의 문서 응답·HTTP 상태·인증과 인가까지 대신하지도 않습니다.

이 절은 [email protected]의 Declarative mode, 즉 BrowserRouter와 Routes/Route를 사용하는 구성을 기준으로 합니다. 이 모드는 URL 매칭, 링크 이동, active 상태를 제공하지만 route loader·action·pending UI·route error boundary를 함께 제공하는 Data/Framework mode와는 책임 범위가 다릅니다.

설치와 라우터 경계

React Router 8.3의 공식 Declarative mode 패키지를 설치합니다.

이 판본은 React와 React DOM 19.2.7 이상, Node.js 22.22.0 이상을 요구합니다. 기존 프로젝트에서 따라 한다면 package.json의 버전을 먼저 확인합니다. 또한 element={<Home />}의 값은 렌더링할 React element이며, Home 함수 자체를 넣는 것과 구분합니다. 첫 React 앱의 컴포넌트와 JSX 구성을 알고 있다는 전제입니다.

npm install react-router

src/main.jsx에서 BrowserRouter가 앱을 감싸도록 구성합니다.

src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

BrowserRouter는 브라우저 History API의 현재 location을 구독하고 클라이언트 라우팅에 연결합니다. 일반적인 Link 이동은 history에 새 항목을 추가하고, replace 옵션은 현재 항목을 바꾸며, 브라우저 뒤로·앞으로 이동은 기존 항목으로 이동합니다.

HashRouter는 route location을 URL의 hash 부분에 저장합니다. hash는 서버 요청에 전송되지 않으므로 각 pathname을 entry HTML로 재작성하기 어려운 환경에서 사용할 수 있지만, base URL에서 앱 문서를 제공하는 host 책임까지 없어지는 것은 아닙니다.

route tree 구성

Routes 안의 Route는 URL segment와 렌더링할 element를 연결합니다. 중첩된 Route는 URL 계층과 UI 계층을 함께 표현합니다.

src/App.jsx
import { Route, Routes } from "react-router";
import RootLayout from "./layouts/RootLayout";
import Home from "./pages/Home";
import About from "./pages/About";
import ProductDetail from "./pages/ProductDetail";
import NewProduct from "./pages/NewProduct";
import NotFound from "./pages/NotFound";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<RootLayout />}>
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="products/new" element={<NewProduct />} />
        <Route path="products/:id" element={<ProductDetail />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  );
}

부모 element는 자식 element가 들어갈 Outlet을 렌더링해야 합니다.

src/layouts/RootLayout.jsx
import { Outlet } from "react-router";
import Navbar from "../components/Navbar";

export default function RootLayout() {
  return (
    <>
      <Navbar />
      <main>
        <Outlet />
      </main>
    </>
  );
}

위 구성의 핵심은 다음과 같습니다.

  • index route는 부모 URL인 /에서 부모의 Outlet에 기본 자식을 렌더링합니다.
  • 자식 path는 부모 경로를 자동으로 상속합니다. 따라서 products/:id는 /products/456과 매칭됩니다.
  • 부모 RootLayout은 선택된 branch에 계속 포함되고, 일치한 자식 element가 Outlet 위치에 들어갑니다.
  • 부모가 Outlet을 렌더링하지 않으면 자식 route가 매칭되어도 자식 element를 표시할 자리가 없습니다.
  • path="*"는 남은 pathname을 받는 splat fallback입니다.

pathname에서 중첩 element tree까지 — 상태 추적

현재 /products/456은 후보 branch 중 products/:id를 고르고 RootLayout Outlet에 ProductDetail과 params.id 문자열을 전달한다. 선언 순서 오해를 줄이도록 현재 location/선택/출력을 추적한다.

pathname이 branch를 고르고 부모 Outlet이 자식을 렌더합니다

REACT ROUTING · URL & RESPONSIBILITY

pathname이 branch를 고르고 부모 Outlet이 자식을 렌더합니다

본문의 다섯 후보 가운데 /products/456은 동적 products/:id branch에 맞습니다. search와 hash는 같은 화면의 입력이며 pathname branch 선택과 구분합니다.

실제 ProductDetail · 문자열 ID와 데이터 조회

/products/456?tab=spec#buy
RootLayout → Outlet
→ ProductDetail
params.id = "456"
React 후드티 · 45,000원

ID 형식 오류, 존재하지 않는 데이터, 일치하는 route가 없는 경우는 별도 경계입니다. /products/new는 정적 branch가 동적 :id보다 구체적입니다.

다섯 실제 route 후보와 선택된 중첩 elementindex, about, products/new, products/:id, splat 다섯 후보 중 products/:id가 선택되고 RootLayout의 Outlet에 ProductDetail이 들어갑니다.location.pathname/products/456<Routes>가장 구체적인 match branch/ + indexHome/ + aboutAboutproducts/newNewProductproducts/:idProductDetail/ + *NotFoundRootLayout → <Outlet />ProductDetail ·params.id = "456"
다섯 실제 route 후보와 선택된 중첩 elementindex, about, products/new, products/:id, splat 다섯 후보 중 products/:id가 선택되고 RootLayout의 Outlet에 ProductDetail이 들어갑니다.location.pathname/products/456<Routes>가장 구체적인match branch/ + indexHome/ + aboutAboutproducts/newNewProductproducts/:idProductDetail/ + *NotFoundRootLayout →<Outlet />ProductDetailparams.id = "456"
  1. location.pathname

    /products/456이 route 매칭 입력이 됩니다.

  2. <Routes>가 다섯 branch를 비교합니다

    index·about·products/new·products/:id·*의 구체성을 비교하므로 단순한 첫 항목 선택이 아닙니다.

    후보 element: Home · About · NewProduct · ProductDetail · NotFound.

  3. / + products/:id가 선택됩니다

    부모와 자식 match가 함께 남고 params.id는 문자열 "456"입니다.

  4. 중첩 element tree를 렌더링합니다

    RootLayout이 유지되고 ProductDetail이 부모의 Outlet에 들어갑니다.

URL 구성 요소별 매칭 참여 여부와 읽기 API
URL 부분 예 React Router API route branch 선택
pathname /products/456 useParams() → { id: "456" } 참여 · path 패턴과 비교
search ?tab=spec useSearchParams() 같은 pathname branch의 보기 조건
hash #buy useLocation().hash branch 선택과 분리된 fragment

pathname · /products/456

path 패턴과 비교하며, 동적 segment 값은 useParams()로 읽습니다.

search · ?tab=spec

useSearchParams()로 읽고 갱신합니다. pathname branch 자체를 고르는 값은 아닙니다.

hash · #buy

useLocation().hash로 읽는 fragment이며 pathname 매칭과 분리됩니다.

정적 branch는 동적 branch보다 구체적이고 splat은 fallback입니다. 경로 param·search·hash는 모두 외부 입력이므로 사용 전에 형식과 허용 범위를 검증합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명/products/456이 route 매칭 입력이 됩니다.
원도표 조건·주석location.pathname: /products/456이 route 매칭 입력이 됩니다.
원도표 설명<Routes>가 다섯 branch를 비교합니다
원도표 조건·주석<Routes>가 다섯 branch를 비교합니다. index·about·products/new·products/:id·*의 구체성을 비교하므로 단순한 첫 항목 선택이 아닙니다. 후보 element: Home · About · NewProduct · ProductDetail · NotFound.
원도표 설명/ + products/:id가 선택됩니다
원도표 설명부모와 자식 match가 함께 남고 params.id는 문자열 "456"입니다.
원도표 조건·주석/ + products/:id가 선택됩니다. 부모와 자식 match가 함께 남고 params.id는 문자열 "456"입니다.
원도표 설명RootLayout이 유지되고 ProductDetail이 부모의 Outlet에 들어갑니다.
원도표 조건·주석중첩 element tree를 렌더링합니다. RootLayout이 유지되고 ProductDetail이 부모의 Outlet에 들어갑니다.
원도표 표 행URL 부분 | 예 | React Router API | route branch 선택
원도표 표 행pathname | /products/456 | useParams() → { id: "456" } | 참여 · path 패턴과 비교
원도표 표 행search | ?tab=spec | useSearchParams() | 같은 pathname branch의 보기 조건
원도표 표 행hash | #buy | useLocation().hash | branch 선택과 분리된 fragment
원도표 설명path 패턴과 비교하며, 동적 segment 값은 useParams()로 읽습니다.
원도표 설명useSearchParams()로 읽고 갱신합니다. pathname branch 자체를 고르는 값은 아닙니다.
원도표 설명useLocation().hash로 읽는 fragment이며 pathname 매칭과 분리됩니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 locationpathname: /products/456 / 후보: 5개의 route branchpathname은 /products/456이다등록 순서가 첫 매칭의 선택 규칙은 아니다
구체 branch 선택branch: RootLayout + ProductDetail / params.id: 문자열 456products/:id가 현재 경로를 매칭한다path 매칭은 resource의 존재 검증이 아니다
Outlet 표시부모: RootLayout / child: ProductDetail부모 RootLayout의 Outlet에 ProductDetail을 렌더한다부모가 Outlet을 빠뜨리면 자식의 표시 자리가 없다

pathname에서 중첩 element tree까지 — 상태 추적

현재 /products/456은 후보 branch 중 products/:id를 고르고 RootLayout Outlet에 ProductDetail과 params.id 문자열을 전달한다. 선언 순서 오해를 줄이도록 현재 location/선택/출력을 추적한다.

pathname이 branch를 고르고 부모 Outlet이 자식을 렌더합니다

REACT ROUTING · URL & RESPONSIBILITY

pathname이 branch를 고르고 부모 Outlet이 자식을 렌더합니다

본문의 다섯 후보 가운데 /products/456은 동적 products/:id branch에 맞습니다. search와 hash는 같은 화면의 입력이며 pathname branch 선택과 구분합니다.

실제 ProductDetail · 문자열 ID와 데이터 조회

/products/456?tab=spec#buy
RootLayout → Outlet
→ ProductDetail
params.id = "456"
React 후드티 · 45,000원

ID 형식 오류, 존재하지 않는 데이터, 일치하는 route가 없는 경우는 별도 경계입니다. /products/new는 정적 branch가 동적 :id보다 구체적입니다.

다섯 실제 route 후보와 선택된 중첩 elementindex, about, products/new, products/:id, splat 다섯 후보 중 products/:id가 선택되고 RootLayout의 Outlet에 ProductDetail이 들어갑니다.location.pathname/products/456<Routes>가장 구체적인 match branch/ + indexHome/ + aboutAboutproducts/newNewProductproducts/:idProductDetail/ + *NotFoundRootLayout → <Outlet />ProductDetail ·params.id = "456"
다섯 실제 route 후보와 선택된 중첩 elementindex, about, products/new, products/:id, splat 다섯 후보 중 products/:id가 선택되고 RootLayout의 Outlet에 ProductDetail이 들어갑니다.location.pathname/products/456<Routes>가장 구체적인match branch/ + indexHome/ + aboutAboutproducts/newNewProductproducts/:idProductDetail/ + *NotFoundRootLayout →<Outlet />ProductDetailparams.id = "456"
  1. location.pathname

    /products/456이 route 매칭 입력이 됩니다.

  2. <Routes>가 다섯 branch를 비교합니다

    index·about·products/new·products/:id·*의 구체성을 비교하므로 단순한 첫 항목 선택이 아닙니다.

    후보 element: Home · About · NewProduct · ProductDetail · NotFound.

  3. / + products/:id가 선택됩니다

    부모와 자식 match가 함께 남고 params.id는 문자열 "456"입니다.

  4. 중첩 element tree를 렌더링합니다

    RootLayout이 유지되고 ProductDetail이 부모의 Outlet에 들어갑니다.

URL 구성 요소별 매칭 참여 여부와 읽기 API
URL 부분 예 React Router API route branch 선택
pathname /products/456 useParams() → { id: "456" } 참여 · path 패턴과 비교
search ?tab=spec useSearchParams() 같은 pathname branch의 보기 조건
hash #buy useLocation().hash branch 선택과 분리된 fragment

pathname · /products/456

path 패턴과 비교하며, 동적 segment 값은 useParams()로 읽습니다.

search · ?tab=spec

useSearchParams()로 읽고 갱신합니다. pathname branch 자체를 고르는 값은 아닙니다.

hash · #buy

useLocation().hash로 읽는 fragment이며 pathname 매칭과 분리됩니다.

정적 branch는 동적 branch보다 구체적이고 splat은 fallback입니다. 경로 param·search·hash는 모두 외부 입력이므로 사용 전에 형식과 허용 범위를 검증합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명/products/456이 route 매칭 입력이 됩니다.
원도표 조건·주석location.pathname: /products/456이 route 매칭 입력이 됩니다.
원도표 설명<Routes>가 다섯 branch를 비교합니다
원도표 조건·주석<Routes>가 다섯 branch를 비교합니다. index·about·products/new·products/:id·*의 구체성을 비교하므로 단순한 첫 항목 선택이 아닙니다. 후보 element: Home · About · NewProduct · ProductDetail · NotFound.
원도표 설명/ + products/:id가 선택됩니다
원도표 설명부모와 자식 match가 함께 남고 params.id는 문자열 "456"입니다.
원도표 조건·주석/ + products/:id가 선택됩니다. 부모와 자식 match가 함께 남고 params.id는 문자열 "456"입니다.
원도표 설명RootLayout이 유지되고 ProductDetail이 부모의 Outlet에 들어갑니다.
원도표 조건·주석중첩 element tree를 렌더링합니다. RootLayout이 유지되고 ProductDetail이 부모의 Outlet에 들어갑니다.
원도표 표 행URL 부분 | 예 | React Router API | route branch 선택
원도표 표 행pathname | /products/456 | useParams() → { id: "456" } | 참여 · path 패턴과 비교
원도표 표 행search | ?tab=spec | useSearchParams() | 같은 pathname branch의 보기 조건
원도표 표 행hash | #buy | useLocation().hash | branch 선택과 분리된 fragment
원도표 설명path 패턴과 비교하며, 동적 segment 값은 useParams()로 읽습니다.
원도표 설명useSearchParams()로 읽고 갱신합니다. pathname branch 자체를 고르는 값은 아닙니다.
원도표 설명useLocation().hash로 읽는 fragment이며 pathname 매칭과 분리됩니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 locationpathname: /products/456 / 후보: 5개의 route branchpathname은 /products/456이다등록 순서가 첫 매칭의 선택 규칙은 아니다
구체 branch 선택branch: RootLayout + ProductDetail / params.id: 문자열 456products/:id가 현재 경로를 매칭한다path 매칭은 resource의 존재 검증이 아니다
Outlet 표시부모: RootLayout / child: ProductDetail부모 RootLayout의 Outlet에 ProductDetail을 렌더한다부모가 Outlet을 빠뜨리면 자식의 표시 자리가 없다

“첫 번째 Route”가 아니라 가장 잘 맞는 branch

Routes는 위에서부터 처음 맞은 Route 하나를 고르는 목록이 아닙니다. route tree를 branch로 펼쳐 구체성을 비교하고 현재 location에 가장 잘 맞는 branch를 렌더링합니다.

예를 들어 /products/new에는 정적 segment인 products/new가 products/:id보다 구체적입니다. /products/456에는 동적 branch가 맞고, 다른 branch가 없는 /unknown/path는 * fallback으로 갑니다. 선택 결과는 한 leaf만이 아니라 부모부터 자식까지의 match branch입니다.

Declarative Route의 element는 React element입니다. 이 Routes 기반 route들은 URL 매칭과 렌더링을 담당하며, route module의 loader·action·자동 code splitting·route ErrorBoundary에는 참여하지 않습니다.

사용자가 누르는 일반적인 내비게이션은 Link 또는 NavLink로 만듭니다.

src/components/Navbar.jsx
import { NavLink } from "react-router";

const navClassName = ({ isActive }) =>
  isActive ? "nav-link nav-link--active" : "nav-link";

export default function Navbar() {
  return (
    <nav aria-label="주요">
      <NavLink to="/" end className={navClassName}>
        Home
      </NavLink>
      <NavLink to="/about" className={navClassName}>
        About
      </NavLink>
      <NavLink to="/products/123" className={navClassName}>
        Product
      </NavLink>
    </nav>
  );
}

Link는 실제 <a href>를 렌더링하는 wrapper입니다. 적합한 일반 클릭은 클라이언트 라우팅으로 처리하면서도 키보드 활성화, 링크 주소 복사, 새 탭 열기 같은 anchor 의미를 유지합니다. reloadDocument를 지정하면 브라우저의 문서 탐색을 사용합니다.

NavLink는 Link에 active 표현을 더합니다. 위 예제처럼 className callback을 전달하면 callback이 반환한 문자열이 최종 class가 되므로, 활성 링크에는 nav-link--active가 적용됩니다. 활성 링크의 aria-current="page"는 자동으로 설정되며, style·children callback에서도 isActive를 읽을 수 있습니다. Declarative mode에서는 pending 탐색 상태가 제공되지 않습니다.

useNavigate는 사용자의 링크 클릭이 아닌 코드 주도 이동에 남겨 둡니다. 폼 제출 완료, 유휴 로그아웃, 시간이 끝난 퀴즈 같은 경우가 대표적입니다.

import { useNavigate } from "react-router";

function CheckoutForm() {
  const navigate = useNavigate();

  async function handleSubmit(event) {
    event.preventDefault();
    await saveOrder();
    navigate("/orders/complete", { replace: true });
  }

  return <form onSubmit={handleSubmit}>{/* 입력과 제출 버튼 */}</form>;
}

단순히 사용자가 “홈으로 이동”을 누르는 UI라면 button의 click handler에서 navigate("/")를 호출하는 것보다 <Link to="/">홈</Link>가 링크 의미와 브라우저 기본 동작을 더 잘 보존합니다.

params, search, hash

URL은 하나의 문자열이지만 route 매칭과 화면 입력에서는 역할을 나누어 읽습니다.

Path params

path="products/:id"의 :id는 한 pathname segment를 받아 useParams() 결과의 id가 됩니다.

src/pages/ProductDetail.jsx
import { useParams } from "react-router";

const products = {
  "123": { name: "React 신발", price: 99000 },
  "456": { name: "React 후드티", price: 45000 },
};

export default function ProductDetail() {
  const { id } = useParams();

  if (!id || !/^\d+$/.test(id)) {
    return <p>올바르지 않은 제품 ID입니다.</p>;
  }

  const product = products[id];

  if (!product) {
    return <p>제품을 찾을 수 없습니다.</p>;
  }

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.price.toLocaleString()}원</p>
    </article>
  );
}

useParams()의 값은 문자열이며 route 구성과 현재 match에 따라 없을 수도 있습니다. param 이름은 같은 path 안에서 고유하게 정하고, 숫자나 식별자로 사용하기 전에 형식과 허용 범위를 검증해야 합니다.

“route가 없음”과 “route는 맞았지만 해당 ID의 데이터가 없음”도 구분합니다. 전자는 path="*" fallback이고, 후자는 ProductDetail이 처리할 resource-not-found 상태입니다.

여기서 /^\d+$/는 숫자 문자로만 이루어졌는지 검사할 뿐, 양수 범위나 실제 제품을 보장하지 않습니다. 이 예제의 ID는 숫자로 계산할 값이 아니라 문자열 key입니다. 따라서 "00123"을 임의로 Number(id)로 바꿔 "123"과 같은 제품으로 취급하지 않습니다. 선행 0을 허용하거나 같은 ID로 정규화할지는 데이터 계약에서 먼저 결정합니다.

Search params

?q=react&page=2처럼 ? 뒤의 값은 useSearchParams()로 읽습니다. 반환된 setter를 호출하면 search가 바뀌는 navigation이 발생합니다.

import { useSearchParams } from "react-router";

function ProductFilters() {
  const [searchParams, setSearchParams] = useSearchParams();
  const sort = searchParams.get("sort") ?? "popular";

  return (
    <button onClick={() => setSearchParams({ sort: "price" })}>
      현재 정렬: {sort}
    </button>
  );
}

search는 같은 pathname 화면의 필터·정렬·페이지 같은 상태에 적합합니다. route path의 dynamic segment와 같은 값이 아니며, 필요한 기본값·허용값·배열 형식은 앱이 검증합니다.

Hash와 location

useLocation()은 pathname, search, hash, state, key를 포함한 현재 location을 반환합니다. #reviews 같은 hash는 fragment이며 pathname branch를 고르는 값이 아닙니다.

Link의 to에는 문자열뿐 아니라 다음처럼 pathname·search·hash를 나눈 객체도 전달할 수 있습니다.

<Link
  to={{
    pathname: "/products/456",
    search: "?tab=spec",
    hash: "#buy",
  }}
>
  구매 정보
</Link>

location state는 browser history에 저장되는 클라이언트 상태입니다. 서버에서 읽을 수 없고 새 문서 응답의 영속 데이터도 아니므로, 공유하거나 복원해야 하는 상태는 URL이나 서버 저장소를 사용합니다.

내부 탐색과 직접 진입은 경계가 다릅니다

내부 탐색과 직접 진입의 책임 경계 — 상태 추적

Link/history와 document GET의 다른 실행 경계가 같은 route matching에 합류한다. HTTP 404/200과 client splat UI가 별개임을 실제 URL 사례로 보강할 가치가 있다.

같은 route tree에 도달해도 문서 응답의 책임은 다릅니다

REACT ROUTING · URL & RESPONSIBILITY

같은 route tree에 도달해도 문서 응답의 책임은 다릅니다

현재 문서가 처리하는 내부 이동은 history와 location으로, 직접 진입은 host의 문서 응답으로 시작합니다. Declarative Routes는 실행 뒤 match와 element 렌더를 담당합니다.

본문 확인 URL · 직접 진입과 client fallback

/products/456?tab=spec#buy
→ 같은 상세 branch
/products/new → 정적 작성
/unknown/path → NotFound UI
(앱이 로드된 뒤)

host가 entry HTML 200을 반환한 경우 client wildcard가 HTTP 상태를 바꾸지 않습니다. 제목·초점·알림·스크롤은 앱의 전환 정책으로 확인합니다.

클라이언트 탐색과 문서 탐색의 분기 및 합류URL 이동이 실행 중인 앱에서 처리되면 Link 또는 navigate와 History를 거쳐 route matching으로 간다. 주소 입력이나 새로고침이면 host 요청을 거치며, route-aware host나 SSR이 404로 판정하면 HTTP 404를 반환한다. client-only SPA rewrite 또는 유효한 route는 진입 문서를 실행한 뒤 route matching으로 가고, SPA의 unmatched route는 path splat UI를 선택하더라도 HTTP 상태는 200일 수 있다. URL 이동 시작현재 앱 문서에서처리되는 이동인가?문서 탐색주소 입력 · 새로고침 ·새 탭클라이언트 탐색Link · NavLink ·useNavigatehost / server 요청GET /products/456browser history 이동PUSH · REPLACE · POPHTTP 404 응답인가?host의 응답 상태진입 문서 실행entry HTML 200 ·SSR · hydrateHTTP 404route-aware host ·SSR 상태location → Routesmatch branch ·render elementNOYESYESNO
클라이언트 탐색과 문서 탐색의 분기 및 합류URL 이동이 실행 중인 앱에서 처리되면 Link 또는 navigate와 History를 거쳐 route matching으로 간다. 주소 입력이나 새로고침이면 host 요청을 거치며, route-aware host나 SSR이 404로 판정하면 HTTP 404를 반환한다. client-only SPA rewrite 또는 유효한 route는 진입 문서를 실행한 뒤 route matching으로 가고, SPA의 unmatched route는 path splat UI를 선택하더라도 HTTP 상태는 200일 수 있다.URL 이동 시작현재 앱 문서에서처리되는 이동?문서 탐색주소 입력 · 새 탭새로고침host / server 요청GET /products/456HTTP 404 응답?host의 응답 상태HTTP 404route-aware host ·SSR 상태진입 문서 실행entry HTML 200 ·SSR · hydrate클라이언트 탐색Link · NavLink ·useNavigatebrowser historyPUSH · REPLACEPOPlocation → Routesmatch branch ·render elementNOYESYESNO

앱 내부 탐색

Link·NavLink와 navigate(to)는 기본 PUSH 또는 명시한 REPLACE를 만듭니다. 뒤로·앞으로 이동이 현재 앱 문서의 entry에 머무르면 POP으로 location을 바꾸고 route branch를 다시 매칭합니다.

직접 진입 · 새로고침 · 새 탭

브라우저가 host/server에 문서를 먼저 요청합니다. client-only 정적 SPA rewrite는 알 수 없는 비자산 경로에도 entry HTML 200을 보낸 뒤 path="*" UI를 고를 수 있습니다. 실제 HTTP 404는 route-aware host나 SSR이 결정합니다.

라우팅 API가 제공하는 기본값과 애플리케이션이 설계할 책임
경계 React Router 기본 앱·host가 결정할 것
링크·활성 상태 Link는 실제 anchor, NavLink는 활성 링크의 aria-current 값을 page로 설정 의미 있는 링크 문구와 useNavigate가 정말 비상호작용 이동인지 판단
초점·알림 route 변경 뒤 UI에 관한 가정을 하지 않음 문서 제목, 주 콘텐츠 초점, 필요한 live-region 완료·오류 알림
스크롤 Declarative mode는 location과 navigation primitives 제공 앱에서 복원 정책 구현, 또는 Data/Framework mode의 ScrollRestoration 사용
대기·오류 <Routes>는 match와 element 렌더만 담당 앱 상태·React error boundary, 또는 Data/Framework mode의 pending·route error API
404·보안 path="*"는 앱 안의 unmatched UI를 선택 HTTP 상태·문서 fallback·API 분리·입력 검증·인증과 인가는 server 책임

링크·활성 상태

Link는 실제 anchor이고 NavLink는 활성 링크의 aria-current 값을 page로 설정합니다. 링크 문구와 이동 수단 선택은 앱 책임입니다.

초점·알림·스크롤

route 변경 뒤 제목, 주 콘텐츠 초점, 필요한 live-region 알림과 스크롤 정책을 앱이 설계합니다. Declarative mode는 앱에서 복원 정책을 구현하며, Data/Framework mode에서는 ScrollRestoration을 선택할 수 있습니다.

대기·오류

Declarative <Routes>는 match와 렌더만 담당합니다. 앱 상태와 React error boundary를 쓰거나 Data/Framework mode의 전용 API를 선택합니다.

404·보안

path="*"는 UI fallback입니다. HTTP 상태, 문서 fallback, API 분리, 입력 검증과 최종 인가는 host/server가 강제합니다.

client-only SPA의 path="*"는 unmatched UI를 선택할 뿐 이미 받은 entry 문서의 200 상태를 바꾸지 않습니다. HashRouter는 route location을 server로 전송되지 않는 hash에 두어 pathname fallback 부담을 피하지만, base URL에서 앱 문서를 제공하는 host 책임까지 없애지는 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명Link·NavLink와 navigate(to)는 기본 PUSH 또는 명시한 REPLACE를 만듭니다. 뒤로·앞으로 이동이 현재 앱 문서의 entry에 머무르면 POP으로 location을 바꾸고 route branch를 다시 매칭합니다.
원도표 설명브라우저가 host/server에 문서를 먼저 요청합니다. client-only 정적 SPA rewrite는 알 수 없는 비자산 경로에도 entry HTML 200을 보낸 뒤 path="*" UI를 고를 수 있습니다. 실제 HTTP 404는 route-aware host나 SSR이 결정합니다.
원도표 표 행경계 | React Router 기본 | 앱·host가 결정할 것
원도표 표 행링크·활성 상태 | Link는 실제 anchor, NavLink는 활성 링크의 aria-current 값을 page로 설정 | 의미 있는 링크 문구와 useNavigate가 정말 비상호작용 이동인지 판단
원도표 표 행초점·알림 | route 변경 뒤 UI에 관한 가정을 하지 않음 | 문서 제목, 주 콘텐츠 초점, 필요한 live-region 완료·오류 알림
원도표 표 행스크롤 | Declarative mode는 location과 navigation primitives 제공 | 앱에서 복원 정책 구현, 또는 Data/Framework mode의 ScrollRestoration 사용
원도표 표 행대기·오류 | <Routes>는 match와 element 렌더만 담당 | 앱 상태·React error boundary, 또는 Data/Framework mode의 pending·route error API
원도표 표 행404·보안 | path="*"는 앱 안의 unmatched UI를 선택 | HTTP 상태·문서 fallback·API 분리·입력 검증·인증과 인가는 server 책임
원도표 설명Link는 실제 anchor이고 NavLink는 활성 링크의 aria-current 값을 page로 설정합니다. 링크 문구와 이동 수단 선택은 앱 책임입니다.
원도표 설명route 변경 뒤 제목, 주 콘텐츠 초점, 필요한 live-region 알림과 스크롤 정책을 앱이 설계합니다. Declarative mode는 앱에서 복원 정책을 구현하며, Data/Framework mode에서는 ScrollRestoration을 선택할 수 있습니다.
원도표 설명Declarative <Routes>는 match와 렌더만 담당합니다. 앱 상태와 React error boundary를 쓰거나 Data/Framework mode의 전용 API를 선택합니다.
원도표 설명path="*"는 UI fallback입니다. HTTP 상태, 문서 fallback, API 분리, 입력 검증과 최종 인가는 host/server가 강제합니다.
원도표 설명client-only SPA의 path="*"는 unmatched UI를 선택할 뿐 이미 받은 entry 문서의 200 상태를 바꾸지 않습니다. HashRouter는 route location을 server로 전송되지 않는 hash에 두어 pathname fallback 부담을 피하지만, base URL에서 앱 문서를 제공하는 host 책임까지 없애지는 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
앱 내부 탐색문서: 현재 문서 유지 / 현재: client locationLink와 navigate가 history와 location을 바꾼다새 HTML 요청과 같은 사건이 아니다
직접 진입 비교host: GET /products/456 / HTTP: 응답 status주소 입력은 host의 200 또는 404 응답을 먼저 얻는다HTTP 404라면 client의 성공 경로와 구별한다
route 결과UI: route 또는 NotFound / HTTP: 200 유지 가능HTML 200 뒤 매칭 또는 SPA의 wildcard UI를 고른다NotFound UI가 자동으로 HTTP 404를 만들지는 않는다

내부 탐색과 직접 진입의 책임 경계 — 상태 추적

Link/history와 document GET의 다른 실행 경계가 같은 route matching에 합류한다. HTTP 404/200과 client splat UI가 별개임을 실제 URL 사례로 보강할 가치가 있다.

같은 route tree에 도달해도 문서 응답의 책임은 다릅니다

REACT ROUTING · URL & RESPONSIBILITY

같은 route tree에 도달해도 문서 응답의 책임은 다릅니다

현재 문서가 처리하는 내부 이동은 history와 location으로, 직접 진입은 host의 문서 응답으로 시작합니다. Declarative Routes는 실행 뒤 match와 element 렌더를 담당합니다.

본문 확인 URL · 직접 진입과 client fallback

/products/456?tab=spec#buy
→ 같은 상세 branch
/products/new → 정적 작성
/unknown/path → NotFound UI
(앱이 로드된 뒤)

host가 entry HTML 200을 반환한 경우 client wildcard가 HTTP 상태를 바꾸지 않습니다. 제목·초점·알림·스크롤은 앱의 전환 정책으로 확인합니다.

클라이언트 탐색과 문서 탐색의 분기 및 합류URL 이동이 실행 중인 앱에서 처리되면 Link 또는 navigate와 History를 거쳐 route matching으로 간다. 주소 입력이나 새로고침이면 host 요청을 거치며, route-aware host나 SSR이 404로 판정하면 HTTP 404를 반환한다. client-only SPA rewrite 또는 유효한 route는 진입 문서를 실행한 뒤 route matching으로 가고, SPA의 unmatched route는 path splat UI를 선택하더라도 HTTP 상태는 200일 수 있다. URL 이동 시작현재 앱 문서에서처리되는 이동인가?문서 탐색주소 입력 · 새로고침 ·새 탭클라이언트 탐색Link · NavLink ·useNavigatehost / server 요청GET /products/456browser history 이동PUSH · REPLACE · POPHTTP 404 응답인가?host의 응답 상태진입 문서 실행entry HTML 200 ·SSR · hydrateHTTP 404route-aware host ·SSR 상태location → Routesmatch branch ·render elementNOYESYESNO
클라이언트 탐색과 문서 탐색의 분기 및 합류URL 이동이 실행 중인 앱에서 처리되면 Link 또는 navigate와 History를 거쳐 route matching으로 간다. 주소 입력이나 새로고침이면 host 요청을 거치며, route-aware host나 SSR이 404로 판정하면 HTTP 404를 반환한다. client-only SPA rewrite 또는 유효한 route는 진입 문서를 실행한 뒤 route matching으로 가고, SPA의 unmatched route는 path splat UI를 선택하더라도 HTTP 상태는 200일 수 있다.URL 이동 시작현재 앱 문서에서처리되는 이동?문서 탐색주소 입력 · 새 탭새로고침host / server 요청GET /products/456HTTP 404 응답?host의 응답 상태HTTP 404route-aware host ·SSR 상태진입 문서 실행entry HTML 200 ·SSR · hydrate클라이언트 탐색Link · NavLink ·useNavigatebrowser historyPUSH · REPLACEPOPlocation → Routesmatch branch ·render elementNOYESYESNO

앱 내부 탐색

Link·NavLink와 navigate(to)는 기본 PUSH 또는 명시한 REPLACE를 만듭니다. 뒤로·앞으로 이동이 현재 앱 문서의 entry에 머무르면 POP으로 location을 바꾸고 route branch를 다시 매칭합니다.

직접 진입 · 새로고침 · 새 탭

브라우저가 host/server에 문서를 먼저 요청합니다. client-only 정적 SPA rewrite는 알 수 없는 비자산 경로에도 entry HTML 200을 보낸 뒤 path="*" UI를 고를 수 있습니다. 실제 HTTP 404는 route-aware host나 SSR이 결정합니다.

라우팅 API가 제공하는 기본값과 애플리케이션이 설계할 책임
경계 React Router 기본 앱·host가 결정할 것
링크·활성 상태 Link는 실제 anchor, NavLink는 활성 링크의 aria-current 값을 page로 설정 의미 있는 링크 문구와 useNavigate가 정말 비상호작용 이동인지 판단
초점·알림 route 변경 뒤 UI에 관한 가정을 하지 않음 문서 제목, 주 콘텐츠 초점, 필요한 live-region 완료·오류 알림
스크롤 Declarative mode는 location과 navigation primitives 제공 앱에서 복원 정책 구현, 또는 Data/Framework mode의 ScrollRestoration 사용
대기·오류 <Routes>는 match와 element 렌더만 담당 앱 상태·React error boundary, 또는 Data/Framework mode의 pending·route error API
404·보안 path="*"는 앱 안의 unmatched UI를 선택 HTTP 상태·문서 fallback·API 분리·입력 검증·인증과 인가는 server 책임

링크·활성 상태

Link는 실제 anchor이고 NavLink는 활성 링크의 aria-current 값을 page로 설정합니다. 링크 문구와 이동 수단 선택은 앱 책임입니다.

초점·알림·스크롤

route 변경 뒤 제목, 주 콘텐츠 초점, 필요한 live-region 알림과 스크롤 정책을 앱이 설계합니다. Declarative mode는 앱에서 복원 정책을 구현하며, Data/Framework mode에서는 ScrollRestoration을 선택할 수 있습니다.

대기·오류

Declarative <Routes>는 match와 렌더만 담당합니다. 앱 상태와 React error boundary를 쓰거나 Data/Framework mode의 전용 API를 선택합니다.

404·보안

path="*"는 UI fallback입니다. HTTP 상태, 문서 fallback, API 분리, 입력 검증과 최종 인가는 host/server가 강제합니다.

client-only SPA의 path="*"는 unmatched UI를 선택할 뿐 이미 받은 entry 문서의 200 상태를 바꾸지 않습니다. HashRouter는 route location을 server로 전송되지 않는 hash에 두어 pathname fallback 부담을 피하지만, base URL에서 앱 문서를 제공하는 host 책임까지 없애지는 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명Link·NavLink와 navigate(to)는 기본 PUSH 또는 명시한 REPLACE를 만듭니다. 뒤로·앞으로 이동이 현재 앱 문서의 entry에 머무르면 POP으로 location을 바꾸고 route branch를 다시 매칭합니다.
원도표 설명브라우저가 host/server에 문서를 먼저 요청합니다. client-only 정적 SPA rewrite는 알 수 없는 비자산 경로에도 entry HTML 200을 보낸 뒤 path="*" UI를 고를 수 있습니다. 실제 HTTP 404는 route-aware host나 SSR이 결정합니다.
원도표 표 행경계 | React Router 기본 | 앱·host가 결정할 것
원도표 표 행링크·활성 상태 | Link는 실제 anchor, NavLink는 활성 링크의 aria-current 값을 page로 설정 | 의미 있는 링크 문구와 useNavigate가 정말 비상호작용 이동인지 판단
원도표 표 행초점·알림 | route 변경 뒤 UI에 관한 가정을 하지 않음 | 문서 제목, 주 콘텐츠 초점, 필요한 live-region 완료·오류 알림
원도표 표 행스크롤 | Declarative mode는 location과 navigation primitives 제공 | 앱에서 복원 정책 구현, 또는 Data/Framework mode의 ScrollRestoration 사용
원도표 표 행대기·오류 | <Routes>는 match와 element 렌더만 담당 | 앱 상태·React error boundary, 또는 Data/Framework mode의 pending·route error API
원도표 표 행404·보안 | path="*"는 앱 안의 unmatched UI를 선택 | HTTP 상태·문서 fallback·API 분리·입력 검증·인증과 인가는 server 책임
원도표 설명Link는 실제 anchor이고 NavLink는 활성 링크의 aria-current 값을 page로 설정합니다. 링크 문구와 이동 수단 선택은 앱 책임입니다.
원도표 설명route 변경 뒤 제목, 주 콘텐츠 초점, 필요한 live-region 알림과 스크롤 정책을 앱이 설계합니다. Declarative mode는 앱에서 복원 정책을 구현하며, Data/Framework mode에서는 ScrollRestoration을 선택할 수 있습니다.
원도표 설명Declarative <Routes>는 match와 렌더만 담당합니다. 앱 상태와 React error boundary를 쓰거나 Data/Framework mode의 전용 API를 선택합니다.
원도표 설명path="*"는 UI fallback입니다. HTTP 상태, 문서 fallback, API 분리, 입력 검증과 최종 인가는 host/server가 강제합니다.
원도표 설명client-only SPA의 path="*"는 unmatched UI를 선택할 뿐 이미 받은 entry 문서의 200 상태를 바꾸지 않습니다. HashRouter는 route location을 server로 전송되지 않는 hash에 두어 pathname fallback 부담을 피하지만, base URL에서 앱 문서를 제공하는 host 책임까지 없애지는 않습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
앱 내부 탐색문서: 현재 문서 유지 / 현재: client locationLink와 navigate가 history와 location을 바꾼다새 HTML 요청과 같은 사건이 아니다
직접 진입 비교host: GET /products/456 / HTTP: 응답 status주소 입력은 host의 200 또는 404 응답을 먼저 얻는다HTTP 404라면 client의 성공 경로와 구별한다
route 결과UI: route 또는 NotFound / HTTP: 200 유지 가능HTML 200 뒤 매칭 또는 SPA의 wildcard UI를 고른다NotFound UI가 자동으로 HTTP 404를 만들지는 않는다

앱이 이미 실행 중일 때 Link·NavLink·useNavigate가 처리하는 이동은 현재 문서 안에서 history와 location을 바꾸고 새 branch를 렌더링합니다. 이때 새 HTML 문서를 요청하지 않더라도 route component의 코드나 화면 데이터는 별도 네트워크 요청이 필요할 수 있습니다.

반면 주소창 입력, 새로고침, 새 탭 열기, 외부 문서에서의 진입은 React Router가 실행되기 전에 host/server에 목적 URL의 문서를 요청합니다.

  • client-routed SPA의 유효한 앱 경로는 entry HTML로 연결해야 합니다.
  • API와 정적 asset 요청은 SPA fallback에서 제외해야 합니다.
  • SSR이나 경로별 문서를 제공한다면 server가 URL에 맞는 문서와 상태를 반환합니다.
  • client-only 정적 SPA의 catch-all rewrite는 알 수 없는 비자산 경로에도 entry HTML을 200 OK로 보낼 수 있습니다.
  • 실제 HTTP 404가 필요하면 route를 아는 host/server, SSR·사전 생성 또는 별도의 상태 정책이 결정해야 합니다.

<Route path="*" element={<NotFound />} />는 앱이 실행된 뒤 unmatched UI를 고릅니다. 이미 200 OK로 받은 client-only entry 문서의 HTTP 상태를 이 component가 소급해 404로 바꾸지는 못합니다. 검색 엔진과 공유 링크에 정확한 상태가 중요하다면 route-aware server/host 또는 SSR·사전 생성 경계를 함께 설계합니다.

접근성, 스크롤, pending, error 책임

클라이언트 라우팅은 새 문서를 열지 않으므로 브라우저가 문서 탐색 때 하던 모든 전환 동작이 자동으로 반복되지는 않습니다.

  • 링크: 가능한 경우 Link·NavLink를 사용해 실제 anchor 의미를 유지합니다. NavLink의 aria-current="page"는 현재 위치를 보조 기술에 전달합니다.
  • 초점과 알림: route 변경 뒤 문서 제목을 갱신하고, 주 콘텐츠 heading이나 main에 초점을 옮길지 설계합니다. 전환이 길거나 결과가 중요하면 live region으로 loading·완료·오류를 알립니다. React Router는 바뀐 UI를 알 수 없으므로 이 정책을 가정하지 않습니다.
  • 스크롤: 모든 이동을 무조건 맨 위로 보내지 말고 새 위치, 뒤로·앞으로 복원, search-only 전환, hash 이동을 구분합니다. Declarative mode에서는 useLocation 등을 바탕으로 앱이 정책을 구현합니다. ScrollRestoration은 Data/Framework router용입니다.
  • pending: Declarative mode의 NavLink에는 isPending이 없고 useNavigation은 data router가 필요합니다. 이 구성에서는 data fetching library나 component 상태로 pending UI를 만들거나 Data/Framework mode를 선택합니다.
  • error: Routes/Route element 구성에서는 명시적인 비동기 오류 UI와 일반 React error boundary를 둡니다. loader/action 오류와 route-module ErrorBoundary·useRouteError가 필요하면 Data/Framework mode를 사용합니다.
  • 보안: param·search·location state와 클라이언트 route guard는 신뢰 경계가 아닙니다. 서버는 모든 요청에서 입력 검증과 인증·인가를 다시 강제해야 합니다.

확인할 탐색 시나리오

라우팅은 링크 한 번만 눌러 보고 끝내지 않습니다.

  1. Link와 NavLink로 이동했을 때 URL, active 상태, 렌더링 결과가 일치하는지 확인합니다.
  2. 브라우저 뒤로·앞으로 이동에서 이전 location과 화면이 복원되는지 확인합니다.
  3. /products/456?tab=spec#buy를 주소창에 직접 입력하고 새로고침해도 entry 문서와 route match가 정상인지 확인합니다.
  4. /products/new가 products/:id가 아니라 정적 route에 매칭되는지 확인합니다.
  5. 앱에 없는 URL의 fallback UI와 실제 HTTP 상태를 각각 확인합니다.
  6. 키보드 이동, 현재 링크 알림, route 변경 뒤 제목·초점·스크롤·loading·error 알림을 확인합니다.

React Router 코드를 읽을 때는 컴포넌트 이름을 외우기보다 location 입력 → 최적 route branch → 중첩 element tree와 내부 history 이동 → 직접 문서 진입의 두 흐름을 분리하면 책임 경계가 선명해집니다. 예제의 파일과 라우터를 연결하기

이 절의 App.jsx는 라우트 자체에 집중하고, main.jsx에서 라우터를 한 번 제공합니다. 앞의 파일을 따라 만들 때는 Home, About, NewProduct, NotFound도 실제 파일로 준비해야 합니다. 예를 들어 홈은 다음처럼 시작하고, 나머지도 각 파일에서 자기 화면 이름을 반환하는 컴포넌트로 만듭니다.

src/pages/Home.jsx
export default function Home() {
  return <h1>홈</h1>;
}

About.jsx에는 About, NewProduct.jsx에는 NewProduct, NotFound.jsx에는 NotFound를 같은 방식으로 기본 export합니다. 별도 API는 필요하지 않으며 라우트가 고른 화면의 이름을 구별하는 준비입니다. ProductDetail.jsx와 RootLayout.jsx, Navbar.jsx에는 앞의 실제 코드를 사용합니다. index.html의 루트 요소와 Vite 진입 파일은 첫 React 앱에서 사용한 것을 유지합니다.

앞의 설치 명령은 실행 시점의 패키지를 선택합니다. 이 절의 고정된 예제 기준과 대조할 때에는 실습 프로젝트에서 npm install [email protected]처럼 판본을 명시하고 lockfile을 확인합니다. 뒤의 블로그 완성 실습에서는 App 자체가 BrowserRouter를 제공하므로 이 절의 main.jsx 래퍼를 그대로 중복해서 붙이지 않습니다.

CheckoutForm은 이동 시점을 설명하는 일부 코드입니다. saveOrder의 구현·입력 검증·실패 안내가 없는 상태로 그 함수를 실행할 수는 없습니다. 호출이 거절되면 await 다음의 이동은 수행되지 않으며, 실제 주문 실습에는 실패 안내와 재시도 정책을 별도로 구현해야 합니다.

branch와 화면 입력을 구별하는 연습

앞의 상품 데이터와 route tree를 그대로 사용합니다. /products/new, /products/456?tab=spec#buy, /products/999, /missing에서 부모와 선택된 자식, 상세 데이터 또는 오류 상태를 각각 적어 보세요. 그다음 두 번째 주소에서 앞의 정렬 버튼을 누르면 tab=spec이 보존되는지도 판단합니다.

상품 URL의 해설

/products/new는 정적 branch로 RootLayout과 NewProduct를 렌더합니다. /products/456?tab=spec#buy는 같은 부모 아래 ProductDetail을 선택하고 문자열 "456"으로 후드티를 조회하여 45,000원을 표시합니다. search와 hash는 이 branch를 다른 route로 바꾸지 않습니다.

/products/999도 상세 branch가 맞습니다. 숫자 문자열 형식은 통과하지만 데이터에 없으므로 상세 컴포넌트의 제품 없음 상태가 나옵니다. /missing은 부모 아래 *의 NotFound입니다. 두 실패의 소유자를 섞지 않습니다.

정렬 버튼의 setSearchParams({ sort: "price" })는 search 전체를 새 값으로 교체하므로 tab=spec은 남지 않습니다. 다른 key를 유지해야 하는 필터는 다음 절처럼 현재 값을 복제하고 바꾸려는 key만 갱신합니다. setter 호출 자체가 어느 보존 정책을 보장한다고 가정하지 않습니다.

문자열 ID와 history 항목을 추적하는 연습

위 코드와 두 제품만 사용합니다. 다음을 예측하고 판단한 경계를 적어 보세요.

  1. /products/00123과 /products/abc는 같은 오류 화면일까요? 각각 route 매칭, 형식 검사, 데이터 조회 중 어디까지 진행할까요?
  2. history의 현재 항목이 /products/123이라고 가정합니다. Link로 /products/456에 이동한 뒤 navigate("/orders/complete", { replace: true })를 수행했습니다. 이 마지막 주소도 같은 SPA가 처리하며 이동에 오류가 없다고 가정하면, 뒤로 가기는 어디로 갈까요? 현재 예제 route tree가 마지막 주소를 어떤 UI로 표시하는지도 따로 답합니다.
문자열 조회와 PUSH·REPLACE의 해설

00123은 동적 route에 매칭되고 숫자 문자 형식도 통과하지만, 객체에 그 문자열 key가 없어 “제품을 찾을 수 없습니다”를 표시합니다. abc도 동적 route에는 매칭되지만 형식 검사에서 “올바르지 않은 제품 ID입니다”를 표시하므로 조회까지 진행하지 않습니다. 정적 route가 없는 두 주소를 *가 먼저 처리한다고 생각하지 않습니다.

PUSH 뒤의 항목은 /products/123 → /products/456이고, REPLACE는 두 번째 항목만 /orders/complete로 바꿉니다. 뒤로 가기는 /products/123입니다. 다만 앞의 route tree에는 orders/complete가 없으므로 현재 화면은 NotFound입니다. history 이동 성공과 주문 완료 화면의 route 정의는 별개 조건입니다. 직접 새로고침했을 때의 문서 응답도 host 설정을 별도로 확인해야 합니다.