본문 바로가기

라우트 파라미터와 쿼리 문자열

pathname의 동적 세그먼트와 search 값을 구분하고, URL을 공유·복원 가능한 화면 상태의 원본으로 다룹니다.

URL은 주소인 동시에 사용자가 복사하고, 새로고침하고, 뒤로 가기로 되돌릴 수 있는 외부 상태입니다. React Router에서는 이 URL을 한 덩어리로 보지 않고 pathname, search, hash처럼 역할이 다른 부분으로 읽습니다.

/products/42?category=electronics&q=키보드를 예로 들면 /products/42는 route branch를 고르는 pathname이고, ? 뒤는 선택된 화면의 보기 조건인 search입니다. 다만 “경로는 항상 리소스 ID, 쿼리는 항상 선택 사항”이라는 규칙은 문법이 아니라 흔한 애플리케이션 설계 관례입니다. 어떤 값이 필수인지와 무엇을 식별하는지는 각 앱의 URL 계약으로 정해야 합니다.

공유와 복원 계약에 맞춰 화면 값을 저장합니다

REACT ROUTING · URL & RESPONSIBILITY

공유와 복원 계약에 맞춰 화면 값을 저장합니다

pathname은 route branch 선택에, search는 선택된 화면의 필터·검색 입력에 참여합니다. navigation state와 component state는 복사한 URL의 일부가 아닙니다.

실제 제품 검색 · 주소만으로 같은 목록을 복원

/products
?category=electronics&q=키보드
category: electronics
query: 키보드
id "2" · 키보드 / 결과 1개

category가 unknown이면 화면에서는 전체 범주로 해석하지만 주소를 자동으로 고치지는 않습니다. 비밀값은 주소·history·로그에 노출될 수 있는 URL에 넣지 않습니다.

화면 값의 위치별 router 동작과 애플리케이션 계약
위치 router 동작 흔한 계약 읽기와 값
pathname · /products/42 path 패턴과 비교해 route branch 선택에 참여 계층·리소스 식별에 자주 사용하지만 앱이 결정 useParams() · 문자열 또는 undefined

search

?q=키보드

pathname branch를 바꾸지 않고 선택된 화면의 입력 검색·필터·정렬·페이지에 자주 사용하지만 앱이 결정 useSearchParams() · 문자열·반복 key 가능
navigation state 특정 history entry에 연결되고 주소에는 표시되지 않음 전환 출처처럼 복사한 링크에 없어도 되는 값 useLocation().state · 직접 진입 때 없을 수 있음
component state route 매칭과 분리된 React 상태 열린 메뉴처럼 공유·새로고침 복원이 불필요한 값 useState() · component 생명주기에 따름

pathname · route branch 입력

/products/:productId의 동적 값은 useParams()로 읽습니다. 리소스 식별에 쓰는 것은 흔한 관례이지 문법 보장은 아닙니다.

search · 선택된 화면의 입력

?q=키보드는 useSearchParams()로 읽고 갱신합니다. 필수 여부와 기본값은 앱 계약입니다.

navigation state · entry에 연결

useLocation().state로 읽는 특정 history entry의 값입니다. 주소에 보이지 않으므로 복사한 링크나 직접 진입에서 복원을 가정하지 않습니다.

component state · 로컬 상호작용

useState()로 관리하며 component 생명주기에 따릅니다. 열린 메뉴처럼 공유와 새로고침 복원이 필요 없는 일시적 UI 상태에 적합합니다.

공유·직접 진입·새로고침·뒤로 가기로 같은 화면을 복원해야 한다면 pathname 또는 search를 검토합니다. URL 값은 모두 외부 문자열이므로 형식·허용 범위를 검증하고 비밀값은 넣지 않습니다.


동적 세그먼트로 경로 값 읽기

Route의 path에서 :productId처럼 콜론으로 시작하는 부분이 동적 세그먼트입니다. 아래 패턴에서 /products/42가 매치되면 productId 값은 문자열 "42"입니다. /products는 이 상세 route와 매치되지 않으므로 목록 route를 따로 둡니다.

src/App.jsx (일부)
import { Route, Routes } from 'react-router';
import ProductDetail from './pages/ProductDetail';
import ProductList from './pages/ProductList';

export default function AppRoutes() {
  return (
    <Routes>
      <Route path="/products" element={<ProductList />} />
      <Route path="/products/:productId" element={<ProductDetail />} />
      <Route path="*" element={<p>요청한 화면을 찾을 수 없습니다.</p>} />
    </Routes>
  );
}

useParams()는 현재 match에서 잡힌 동적 세그먼트를 읽습니다. 자식 route는 부모 route의 params도 상속합니다. 값은 디코딩된 문자열이며 route 구조에 따라 undefined일 수 있으므로, 숫자나 UUID처럼 사용할 형식과 실제 데이터 존재 여부를 따로 확인해야 합니다.

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

const productData = {
  '123': { name: 'React 신발', description: 'React 개발자를 위한 신발' },
  '456': { name: '리액트 후드티', description: 'React 로고 후드티' },
};

export default function ProductDetail() {
  const { productId } = useParams();
  const hasValidShape = productId !== undefined && /^\d+$/.test(productId);
  const product = hasValidShape ? productData[productId] : undefined;

  if (!product) {
    return (
      <main>
        <h1>제품을 찾을 수 없습니다</h1>
        <p>제품 번호의 형식과 존재 여부를 확인해 주세요.</p>
      </main>
    );
  }

  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p>제품 ID: {productId}</p>
    </main>
  );
}

route가 매치되는 것과 서버에 리소스가 존재하는 것은 별개의 판단입니다. 또한 브라우저에서 param 형식을 검사하더라도 서버는 인증·인가와 데이터 검증을 다시 수행해야 합니다. 클라이언트의 * route가 보여 주는 “찾을 수 없음” 화면도 호스트가 직접 요청에 반환하는 HTTP 상태 코드와 같은 개념은 아닙니다.


useSearchParams로 보기 조건 읽고 갱신하기

search는 ?category=electronics&q=키보드처럼 ? 뒤에 오는 직렬화된 값입니다. React Router의 useSearchParams()는 현재 값인 URLSearchParams와 이를 갱신하는 setter를 반환합니다.

  • searchParams.get('q')는 첫 번째 값을 문자열 또는 null로 반환합니다.
  • 같은 key를 여러 번 허용하는 계약이라면 searchParams.getAll('tag')처럼 모두 읽습니다.
  • setSearchParams(next)는 단순한 로컬 상태 변경이 아니라 새 search를 향한 router navigation을 시작합니다.
  • 반환된 searchParams 객체의 참조는 안정적이지만 객체 자체는 변경 가능합니다. 직접 바꿔 놓고 setter를 생략하면 화면 값과 주소가 어긋날 수 있으므로, 갱신할 때는 복제본을 만든 뒤 한 번의 setter 호출로 커밋합니다.

목록은 URL에서 읽은 값으로 렌더링할 때마다 계산할 수 있습니다. 이처럼 기존 props·상태에서 계산 가능한 값을 다시 useState에 저장하고 useEffect로 동기화할 필요는 없습니다.

src/pages/ProductList.jsx
import { Link, useSearchParams } from 'react-router';

const allProducts = [
  { id: '1', name: '노트북', category: 'electronics' },
  { id: '2', name: '키보드', category: 'electronics' },
  { id: '3', name: '책상', category: 'furniture' },
  { id: '4', name: '의자', category: 'furniture' },
];

const categoryOptions = [
  { value: '', label: '전체' },
  { value: 'electronics', label: '전자제품' },
  { value: 'furniture', label: '가구' },
];
const allowedCategories = new Set(categoryOptions.map(({ value }) => value));

export default function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  const rawCategory = searchParams.get('category') ?? '';
  const category = allowedCategories.has(rawCategory) ? rawCategory : '';
  const query = searchParams.get('q') ?? '';
  const normalizedQuery = query.trim().toLocaleLowerCase();

  const filteredProducts = allProducts.filter((product) => {
    const matchesCategory = !category || product.category === category;
    const matchesQuery = product.name.toLocaleLowerCase().includes(normalizedQuery);
    return matchesCategory && matchesQuery;
  });

  function updateParam(name, value, options) {
    const next = new URLSearchParams(searchParams);
    if (value) next.set(name, value);
    else next.delete(name);
    setSearchParams(next, options);
  }

  return (
    <main>
      <h1>제품 목록</h1>

      <fieldset>
        <legend>카테고리</legend>
        {categoryOptions.map((option) => (
          <button
            key={option.value || 'all'}
            type="button"
            aria-pressed={category === option.value}
            onClick={() => updateParam('category', option.value)}
          >
            {option.label}
          </button>
        ))}
      </fieldset>

      <label htmlFor="product-search">제품 검색</label>
      <input
        id="product-search"
        type="search"
        value={query}
        onChange={(event) =>
          updateParam('q', event.target.value, { replace: true })
        }
      />

      <p role="status" aria-live="polite">
        검색 결과 {filteredProducts.length}개
      </p>
      <ul>
        {filteredProducts.map((product) => (
          <li key={product.id}>
            <Link to={`/products/${product.id}`}>{product.name}</Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

이 예제는 허용된 category만 사용하고 나머지는 안전한 기본값으로 해석합니다. 이 정규화는 렌더 입력을 정하는 일이며 주소를 자동으로 다시 쓰지는 않습니다. canonical URL이 필요하다면 loader나 명시적인 사용자 동작에서 검증된 값으로 replace navigation을 수행할 수 있습니다.

이 절의 <Routes> 예제는 Declarative Mode이므로 컴포넌트가 hooks로 URL 값을 읽습니다. Data 또는 Framework Mode에서 loader를 사용하는 앱이라면 loader의 params와 new URL(request.url).searchParams에서 같은 값을 읽고, 형식·허용 범위를 검증한 뒤 데이터 요청을 구성합니다. 두 mode의 API 위치는 달라도 URL을 신뢰하지 않고 파싱한다는 경계는 같습니다.

카테고리처럼 사용자가 하나의 선택을 확정하는 동작은 기본 PUSH로 남겨 뒤로 가기에서 이전 선택을 복원하게 할 수 있습니다. 반대로 검색어의 매 키 입력을 모두 history에 쌓지 않도록 예제는 { replace: true }를 사용합니다. 어느 동작을 PUSH와 REPLACE로 처리할지는 제품의 뒤로 가기 계약에 맞춰 정합니다.

여러 key를 한 동작으로 적용하기

updateParam('category', 'furniture')와 updateParam('q', '의자')를 같은 이벤트에서 연달아 호출하면, 두 호출이 같은 렌더의 searchParams를 복제할 수 있습니다. 두 번째 navigation이 첫 변경을 덮어쓸 수 있으며, setter의 함수형 인자도 React의 state setter처럼 같은 tick의 변경을 누적하는 큐가 아닙니다. “필터 적용”처럼 하나의 사용자 동작은 변경들을 한 복제본에 모아 한 번 커밋합니다. 다음 함수를 앞의 ProductList 안에 추가해 버튼에서 호출할 수 있습니다.

function applyFilters(nextCategory, nextQuery) {
  if (!allowedCategories.has(nextCategory)) return;

  const next = new URLSearchParams(searchParams);
  if (nextCategory) next.set('category', nextCategory);
  else next.delete('category');
  if (nextQuery) next.set('q', nextQuery);
  else next.delete('q');
  setSearchParams(next);
}

이 함수는 UI가 제공하는 문자열 두 개를 받습니다. tab 같은 다른 key는 보존하고 category의 중복 값은 set으로 하나로 정리합니다. 하나의 확정 동작을 기본 PUSH로 남기므로 뒤로 가기는 중간 필터가 아니라 적용 전 조건으로 돌아갑니다. 여러 번의 setter 호출 결과를 기다렸다 합치는 코드가 아닙니다.

URL에서 렌더와 navigation으로 이어지는 search params 흐름 — 상태 추적

원 search에서 검증된 category/query로 목록을 계산하고 새 URLSearchParams 한 번의 navigation 뒤 UI가 달라진다. 다른 key 보존/중복 category 정리/뒤로 복원을 실제 주소 값으로 추적할 수 있다.

URL에서 목록을 계산하고 이벤트에서 search를 갱신합니다

REACT ROUTING · URL & RESPONSIBILITY

URL에서 목록을 계산하고 이벤트에서 search를 갱신합니다

렌더 중에는 URL을 읽어 controls·결과를 파생합니다. 다음 사용자 이벤트에서 params 복제본을 수정하고 setter를 한 번 호출하면 navigation 뒤의 URL을 다시 읽습니다.

실제 검색 · 다른 key를 보존하며 q만 변경

?category=electronics&q=키보드
→ 키보드 1개
q 지우기 → ?category=electronics
→ 전자제품 2개
category 선택 → PUSH
q 매 입력 → REPLACE

updateParam은 현재 params를 복제하고 지정한 key만 set/delete합니다. 미지원 category의 화면 기본값 해석은 URL 쓰기와 별개입니다.

search params의 읽기, 파생 렌더, 갱신 흐름현재 location search를 URLSearchParams로 읽고 정규화와 검증을 거쳐 목록을 파생하고 controls와 결과를 렌더링한다. 사용자 입력은 새 URLSearchParams에 반영한 뒤 setSearchParams를 한 번 호출하여 navigation하고, 갱신된 URL이 다시 흐름의 입력이 된다. 현재 URL · 원본 상태?category=electronics&q=키보드search params 읽기searchParams.get(...) · getAll(...)정규화 · 검증기본값 · whitelist · 반복 key 정책렌더 입력 파생allProducts.filter(...)controls · 결과 렌더로컬 복사 없이 같은 URL 값을 표시다음 사용자 이벤트 · 커밋new URLSearchParams(searchParams)setSearchParams(next, options)NAVIGATION
search params의 읽기, 파생 렌더, 갱신 흐름현재 location search를 URLSearchParams로 읽고 정규화와 검증을 거쳐 목록을 파생하고 controls와 결과를 렌더링한다. 사용자 입력은 새 URLSearchParams에 반영한 뒤 setSearchParams를 한 번 호출하여 navigation하고, 갱신된 URL이 다시 흐름의 입력이 된다.현재 URL · 원본 상태?category=electronics&q=키보드search params 읽기searchParams.get(...)getAll(...)정규화 · 검증기본값 · whitelist반복 key 정책렌더 입력 파생allProducts.filter(...)controls · 결과 렌더로컬 복사 없이같은 URL 값을 표시다음 이벤트 · 커밋new URLSearchParams(searchParams)setSearchParams(next, options)NAVIGATION
  1. 현재 URL을 원본으로 둡니다

    ?category=electronics&q=키보드가 직접 진입·공유·뒤로 가기의 입력입니다.

  2. 필요한 값을 읽습니다

    get은 첫 값, 반복 key 계약에는 getAll을 사용합니다.

  3. 정규화하고 검증합니다

    빈 값·기본값·허용 목록·반복 key 정책을 도메인 값으로 바꿉니다.

  4. 렌더 입력을 파생합니다

    URL에서 계산 가능한 목록을 별도 state와 Effect로 복제하지 않습니다.

  5. controls와 결과를 렌더합니다

    입력 값, pressed 상태, 결과 수가 모두 같은 URL 상태를 반영합니다.

  6. 이벤트에서 복제하고 커밋합니다

    새 params에 변경을 모아 setter를 한 번 호출하면 navigation 후 갱신된 URL부터 다시 읽습니다.

History는 사용자 동작 단위로

카테고리 확정은 기본 PUSH로 이전 선택을 남길 수 있습니다. 검색어 매 키 입력은 { replace: true }로 같은 entry를 교체할 수 있습니다.

안전한 갱신 경계

searchParams는 안정된 참조지만 변경 가능한 객체입니다. 복제본을 수정하고 setSearchParams로 커밋하며 URL에 비밀값을 넣지 않습니다.

setter는 search를 향한 router navigation입니다. Data·Framework Mode의 loader도 params와 request URL의 search를 검증해 데이터를 구성하며, navigation에서 loader와 화면 작업이 다시 실행될 수 있습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 설명?category=electronics&q=키보드가 직접 진입·공유·뒤로 가기의 입력입니다.
원도표 조건·주석현재 URL을 원본으로 둡니다. ?category=electronics&q=키보드가 직접 진입·공유·뒤로 가기의 입력입니다.
원도표 설명get은 첫 값, 반복 key 계약에는 getAll을 사용합니다.
원도표 조건·주석필요한 값을 읽습니다. get은 첫 값, 반복 key 계약에는 getAll을 사용합니다.
원도표 조건·주석정규화하고 검증합니다. 빈 값·기본값·허용 목록·반복 key 정책을 도메인 값으로 바꿉니다.
원도표 조건·주석렌더 입력을 파생합니다. URL에서 계산 가능한 목록을 별도 state와 Effect로 복제하지 않습니다.
원도표 조건·주석controls와 결과를 렌더합니다. 입력 값, pressed 상태, 결과 수가 모두 같은 URL 상태를 반영합니다.
원도표 조건·주석이벤트에서 복제하고 커밋합니다. 새 params에 변경을 모아 setter를 한 번 호출하면 navigation 후 갱신된 URL부터 다시 읽습니다.
원도표 설명카테고리 확정은 기본 PUSH로 이전 선택을 남길 수 있습니다. 검색어 매 키 입력은 { replace: true }로 같은 entry를 교체할 수 있습니다.
원도표 설명searchParams는 안정된 참조지만 변경 가능한 객체입니다. 복제본을 수정하고 setSearchParams로 커밋하며 URL에 비밀값을 넣지 않습니다.
원도표 설명setter는 search를 향한 router navigation입니다. Data·Framework Mode의 loader도 params와 request URL의 search를 검증해 데이터를 구성하며, navigation에서 loader와 화면 작업이 다시 실행될 수 있습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 searchcategory: electronics / q: 키보드category=electronics와 q=키보드로 현재 조건을 읽는다URL이 조건의 원본이며 별도로 중복 state를 두지 않는다
파생 목록result: id 2·키보드 / count: 1검증과 정규화 뒤 id 2의 키보드 한 개를 표시한다unknown category는 화면의 기본 해석이며 주소의 자동 교정은 아니다
한 번의 갱신다른 key: 보존 / history: 확정 동작 PUSH새 URLSearchParams에 조건을 함께 반영하고 한 번 navigation한다뒤로 가기는 중간 setter가 아닌 적용 전 조건으로 돌아간다

URL에서 렌더와 navigation으로 이어지는 search params 흐름 — 상태 추적

원 search에서 검증된 category/query로 목록을 계산하고 새 URLSearchParams 한 번의 navigation 뒤 UI가 달라진다. 다른 key 보존/중복 category 정리/뒤로 복원을 실제 주소 값으로 추적할 수 있다.

URL에서 목록을 계산하고 이벤트에서 search를 갱신합니다

REACT ROUTING · URL & RESPONSIBILITY

URL에서 목록을 계산하고 이벤트에서 search를 갱신합니다

렌더 중에는 URL을 읽어 controls·결과를 파생합니다. 다음 사용자 이벤트에서 params 복제본을 수정하고 setter를 한 번 호출하면 navigation 뒤의 URL을 다시 읽습니다.

실제 검색 · 다른 key를 보존하며 q만 변경

?category=electronics&q=키보드
→ 키보드 1개
q 지우기 → ?category=electronics
→ 전자제품 2개
category 선택 → PUSH
q 매 입력 → REPLACE

updateParam은 현재 params를 복제하고 지정한 key만 set/delete합니다. 미지원 category의 화면 기본값 해석은 URL 쓰기와 별개입니다.

search params의 읽기, 파생 렌더, 갱신 흐름현재 location search를 URLSearchParams로 읽고 정규화와 검증을 거쳐 목록을 파생하고 controls와 결과를 렌더링한다. 사용자 입력은 새 URLSearchParams에 반영한 뒤 setSearchParams를 한 번 호출하여 navigation하고, 갱신된 URL이 다시 흐름의 입력이 된다. 현재 URL · 원본 상태?category=electronics&q=키보드search params 읽기searchParams.get(...) · getAll(...)정규화 · 검증기본값 · whitelist · 반복 key 정책렌더 입력 파생allProducts.filter(...)controls · 결과 렌더로컬 복사 없이 같은 URL 값을 표시다음 사용자 이벤트 · 커밋new URLSearchParams(searchParams)setSearchParams(next, options)NAVIGATION
search params의 읽기, 파생 렌더, 갱신 흐름현재 location search를 URLSearchParams로 읽고 정규화와 검증을 거쳐 목록을 파생하고 controls와 결과를 렌더링한다. 사용자 입력은 새 URLSearchParams에 반영한 뒤 setSearchParams를 한 번 호출하여 navigation하고, 갱신된 URL이 다시 흐름의 입력이 된다.현재 URL · 원본 상태?category=electronics&q=키보드search params 읽기searchParams.get(...)getAll(...)정규화 · 검증기본값 · whitelist반복 key 정책렌더 입력 파생allProducts.filter(...)controls · 결과 렌더로컬 복사 없이같은 URL 값을 표시다음 이벤트 · 커밋new URLSearchParams(searchParams)setSearchParams(next, options)NAVIGATION
  1. 현재 URL을 원본으로 둡니다

    ?category=electronics&q=키보드가 직접 진입·공유·뒤로 가기의 입력입니다.

  2. 필요한 값을 읽습니다

    get은 첫 값, 반복 key 계약에는 getAll을 사용합니다.

  3. 정규화하고 검증합니다

    빈 값·기본값·허용 목록·반복 key 정책을 도메인 값으로 바꿉니다.

  4. 렌더 입력을 파생합니다

    URL에서 계산 가능한 목록을 별도 state와 Effect로 복제하지 않습니다.

  5. controls와 결과를 렌더합니다

    입력 값, pressed 상태, 결과 수가 모두 같은 URL 상태를 반영합니다.

  6. 이벤트에서 복제하고 커밋합니다

    새 params에 변경을 모아 setter를 한 번 호출하면 navigation 후 갱신된 URL부터 다시 읽습니다.

History는 사용자 동작 단위로

카테고리 확정은 기본 PUSH로 이전 선택을 남길 수 있습니다. 검색어 매 키 입력은 { replace: true }로 같은 entry를 교체할 수 있습니다.

안전한 갱신 경계

searchParams는 안정된 참조지만 변경 가능한 객체입니다. 복제본을 수정하고 setSearchParams로 커밋하며 URL에 비밀값을 넣지 않습니다.

setter는 search를 향한 router navigation입니다. Data·Framework Mode의 loader도 params와 request URL의 search를 검증해 데이터를 구성하며, navigation에서 loader와 화면 작업이 다시 실행될 수 있습니다.

이 예의 조건

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

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 설명?category=electronics&q=키보드가 직접 진입·공유·뒤로 가기의 입력입니다.
원도표 조건·주석현재 URL을 원본으로 둡니다. ?category=electronics&q=키보드가 직접 진입·공유·뒤로 가기의 입력입니다.
원도표 설명get은 첫 값, 반복 key 계약에는 getAll을 사용합니다.
원도표 조건·주석필요한 값을 읽습니다. get은 첫 값, 반복 key 계약에는 getAll을 사용합니다.
원도표 조건·주석정규화하고 검증합니다. 빈 값·기본값·허용 목록·반복 key 정책을 도메인 값으로 바꿉니다.
원도표 조건·주석렌더 입력을 파생합니다. URL에서 계산 가능한 목록을 별도 state와 Effect로 복제하지 않습니다.
원도표 조건·주석controls와 결과를 렌더합니다. 입력 값, pressed 상태, 결과 수가 모두 같은 URL 상태를 반영합니다.
원도표 조건·주석이벤트에서 복제하고 커밋합니다. 새 params에 변경을 모아 setter를 한 번 호출하면 navigation 후 갱신된 URL부터 다시 읽습니다.
원도표 설명카테고리 확정은 기본 PUSH로 이전 선택을 남길 수 있습니다. 검색어 매 키 입력은 { replace: true }로 같은 entry를 교체할 수 있습니다.
원도표 설명searchParams는 안정된 참조지만 변경 가능한 객체입니다. 복제본을 수정하고 setSearchParams로 커밋하며 URL에 비밀값을 넣지 않습니다.
원도표 설명setter는 search를 향한 router navigation입니다. Data·Framework Mode의 loader도 params와 request URL의 search를 검증해 데이터를 구성하며, navigation에서 loader와 화면 작업이 다시 실행될 수 있습니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
현재 searchcategory: electronics / q: 키보드category=electronics와 q=키보드로 현재 조건을 읽는다URL이 조건의 원본이며 별도로 중복 state를 두지 않는다
파생 목록result: id 2·키보드 / count: 1검증과 정규화 뒤 id 2의 키보드 한 개를 표시한다unknown category는 화면의 기본 해석이며 주소의 자동 교정은 아니다
한 번의 갱신다른 key: 보존 / history: 확정 동작 PUSH새 URLSearchParams에 조건을 함께 반영하고 한 번 navigation한다뒤로 가기는 중간 setter가 아닌 적용 전 조건으로 돌아간다

URL 상태 계약 점검하기

pathname과 search에 무엇을 둘지는 “필수 대 선택” 한 줄로 결정할 수 없습니다. 다음 질문을 기준으로 계약을 검토합니다.

질문설계 판단
이 값을 바꾸면 다른 route branch를 선택해야 하는가?pathname의 정적·동적 segment 후보
같은 화면을 필터·정렬·검색·페이지 이동한 상태인가?search 후보
링크를 복사하거나 새로고침해도 복원되어야 하는가?pathname 또는 search에 직렬화
잠깐 열린 메뉴처럼 공유할 필요가 없는가?컴포넌트 state 또는 navigation state 검토
값이 없거나 잘못되었을 때의 기본값은 무엇인가?parse·normalize·validate 규칙 명시
같은 key가 반복될 수 있는가?단일 값이면 get, 다중 값이면 getAll 정책
뒤로 가기가 어느 사용자 동작을 되돌려야 하는가?navigation별 PUSH·REPLACE 정책

주소창에 있는 값은 신뢰할 수 없는 외부 문자열입니다. percent-encoding, 빈 값, 예상하지 못한 값, 반복 key, 매우 긴 입력을 테스트하고, 화면에서 숫자·열거형 등 도메인 타입으로 변환한 뒤 사용합니다. 경로와 쿼리는 주소창·브라우저 기록·서버 및 분석 로그 등에 노출될 수 있으므로 비밀번호, token 같은 비밀값을 넣지 않습니다.

쿼리의 +는 URLSearchParams가 읽을 때 공백으로 해석합니다. 따라서 q=C++를 직접 이어 붙이면 문자열 "C "가 됩니다. 문자 +를 검색하려면 q=C%2B%2B를 사용하거나, new URLSearchParams({ q: "C++" }).toString()으로 직렬화합니다. 직렬화는 문자를 안전하게 표현하는 일이며 허용값 검증을 대신하지 않습니다.

다음 시나리오를 직접 확인하면 URL과 UI의 계약이 어긋나는 문제를 빨리 찾을 수 있습니다.

  1. /products?category=electronics&q=키보드로 직접 진입했을 때 controls와 결과가 같은 값을 나타내는가?
  2. 카테고리를 바꿨을 때 q가 보존되고, 검색어를 지웠을 때 q만 삭제되는가?
  3. 검색어 입력 뒤 뒤로 가기가 제품이 의도한 단위로 동작하는가?
  4. category=unknown, 빈 값, 반복 key, 한글과 공백이 포함된 값이 정해 둔 정책대로 처리되는가?
  5. 필터 controls에 label·pressed state가 있고 결과 변경이 보조 기술에 전달되는가?
  6. 상세 URL의 param 형식, 리소스 존재, 서버 인가를 서로 다른 경계에서 검사하는가?

React는 route schema나 배포 서버의 direct-entry fallback을 정하지 않습니다. React Router도 URL 값을 애플리케이션 도메인 타입으로 자동 검증하지 않습니다. 따라서 route 정의, 호스트 설정, parse·validation, 데이터 접근 권한을 각각의 책임 경계에서 설계해야 합니다.


라우트 파라미터와 쿼리 문자열은 모두 URL 상태입니다. pathname은 route branch 선택에 참여하고 search는 선택된 pathname 화면의 입력으로 읽히지만, 식별자·필수값·기본값·history 동작은 애플리케이션 계약입니다. 공유·새로고침·뒤로 가기로 복원되어야 하는 값은 URL에 두고, 읽을 때마다 외부 문자열로 검증하며, URL에서 계산할 수 있는 결과를 중복 state로 만들지 않는 것이 핵심입니다.

다음 절에서는 중첩 route와 Outlet을 사용해 선택된 부모·자식 element를 함께 구성합니다. 필터의 중간 결과 확인하기

/products?category=electronics&q=키보드에서는 허용된 카테고리가 electronics, 정규화한 검색어가 키보드입니다. 첫 조건은 제품 1·2를 남기고 두 번째 조건까지 만족하는 제품은 2 하나입니다. 두 조건을 OR가 아니라 AND로 결합한 결과입니다. 검색어를 지우면 category는 그대로 남고 제품 1·2가 표시됩니다.

이 목록의 ID 1~4와 앞의 상세 데이터 ID 123·456은 서로 다른 작은 예제입니다. 두 파일을 하나의 실습 앱으로 붙이려면 목록 링크의 ID와 상세 조회 데이터를 같은 계약으로 맞춰야 합니다. 현재 목록의 /products/2 링크는 route에는 맞더라도 앞의 productData에는 없으므로 제품 없음 상태가 됩니다. 라우팅 성공과 데이터 연결 성공을 따로 확인합니다.

주소를 바꿔 결과와 history를 예측하기

목록의 네 제품과 updateParam을 그대로 사용합니다.

  1. category=furniture&q=%20%20에서는 어떤 ID가 남을까요?
  2. category=unknown&q=키보드에서는 controls와 결과가 어떻게 보이며 주소의 unknown도 자동 삭제될까요?
  3. category=electronics&category=furniture&q=키보드&tab=spec에서는 어떤 category를 읽을까요? 그 상태에서 검색어를 빈 문자열로 바꾸면 어떤 key가 남고 history에는 새 항목이 생길까요?
파싱·보존·history의 해설

첫 검색어는 디코딩하면 공백 두 개이고 trim() 뒤 빈 문자열입니다. 모든 이름이 빈 문자열을 포함하므로 카테고리 조건만 남아 ID 3·4가 표시됩니다.

두 번째는 미지원 category를 전체로 해석하고 검색어에 맞는 ID 2를 표시합니다. 화면에 쓸 입력을 정규화했을 뿐 URL을 다시 쓴 것이 아니므로 unknown은 주소에 남습니다.

세 번째는 get('category')가 첫 값인 electronics를 읽어 ID 2를 남깁니다. 검색어를 지우면 복제본에서 q만 삭제하고 두 category 값과 tab=spec은 유지합니다. 검색어 핸들러는 { replace: true }를 전달하므로 현재 history 항목을 교체합니다. 이후 category 버튼으로 값을 확정하면 set이 같은 key의 중복을 하나로 정리하고 기본 PUSH 이동을 합니다.

한 번의 적용과 문자열 직렬화 연습

현재 search가 category=electronics&q=키보드&tab=spec일 때 applyFilters('furniture', '의자')를 한 번 실행합니다. 남는 key와 결과 ID, 이전 history 항목을 적어 보세요. 이어서 q=C++와 q=C%2B%2B의 get('q') 결과도 비교합니다.

일괄 적용과 더하기 문자의 해설

다음 search는 category=furniture&q=의자&tab=spec에 해당합니다. 두 조건의 AND를 만족하는 ID는 4이며 tab=spec은 보존됩니다. PUSH로 하나의 항목을 만들므로 뒤로 가기는 category=electronics&q=키보드&tab=spec이라는 적용 전 조건으로 돌아갑니다. 두 key를 서로 다른 setter로 바꿔 중간 상태를 남기는 결과를 보장하지 않습니다.

q=C++의 값은 "C ", q=C%2B%2B의 값은 "C++"입니다. 디코딩한 문자열과 주소에 표시할 직렬화 문자열을 구별합니다. 어느 값이 허용되는 검색어인지는 앱의 별도 계약입니다.