React Router와 URL 상태

목록·상세·404 경로를 나누고 검색 조건과 직접 URL 진입을 검증하기

URL도 화면 상태다

목록에서 한 사람을 누른 뒤 주소를 복사했는데 다른 사람이 열면 첫 화면만 나온다면, 선택 상태가 브라우저 메모리에만 있었던 것입니다. 상세 대상은 URL 경로에, 공유할 검색 조건은 query string에 두면 새로고침·뒤로 가기·링크 공유가 같은 정보를 가리킵니다.

이 문서는 React Router의 Declarative 모드를 사용합니다. Next.js의 파일 라우팅, Router의 Framework 모드와 설정을 섞지 않습니다. 앞에서 만든 Vite React 프로젝트에 이어서 작업합니다.

경로와 데이터 계약

경로화면확인할 실패
/아기사자 목록검색 결과 없음
/members/1ID가 1인 구성원 상세형식은 맞지만 없는 ID
/members/abc잘못된 ID 안내숫자로 바꿀 수 없는 값
/anything404 안내정의되지 않은 경로

경로가 존재한다는 것과 데이터가 존재한다는 것은 다릅니다. /members/:id가 일치해도 조회 결과가 없으면 상세 화면에서 별도로 처리해야 합니다.

실행: 목록·상세·404 연결하기

프로젝트 폴더에서 npm install react-router를 실행합니다. 아래는 현재 Declarative API를 사용하는 예제입니다. 설치한 실제 버전과 lock 파일을 함께 남깁니다.

src/App.jsx 전체를 다음으로 바꿉니다. Vite가 생성한 src/main.jsx는 그대로 둡니다. 이 예제에서 BrowserRouter는 한 번만 선언합니다.

JavaScript
import { BrowserRouter, Link, Route, Routes, useParams, useSearchParams } from "react-router";
const members = [  { id: 1, name: "아기사자 A", track: "프론트엔드" },  { id: 2, name: "아기사자 B", track: "기획·디자인" },];
function MemberList() {  const [params, setParams] = useSearchParams();  const query = params.get("q") ?? "";  const filtered = members.filter(member => member.name.includes(query.trim()));  return (    <main>      <h1>구성원</h1>      <label htmlFor="query">이름 검색</label>      <input id="query" value={query} onChange={event => {        const next = new URLSearchParams(params);        const value = event.target.value;        if (value) next.set("q", value);        else next.delete("q");        setParams(next, { replace: true });      }} />      <p aria-live="polite">{filtered.length}</p>      {filtered.length === 0 ? <p>검색어를 바꿔 보세요.</p> : (        <ul>{filtered.map(member => (          <li key={member.id}>            <Link to={"/members/" + member.id}>{member.name}</Link>          </li>        ))}</ul>      )}    </main>  );}
function MemberDetail() {  const { id = "" } = useParams();  const member = /^\d+$/.test(id)    ? members.find(item => item.id === Number(id))    : undefined;  if (!member) return <NotFound />;  return (    <main>      <h1>{member.name}</h1>      <p>{member.track}</p>      <Link to="/">전체 목록</Link>    </main>  );}
function NotFound() {  return <main><h1>찾을 수 없습니다</h1><Link to="/">목록으로</Link></main>;}
export default function App() {  return (    <BrowserRouter>      <Routes>        <Route path="/" element={<MemberList />} />        <Route path="/members/:id" element={<MemberDetail />} />        <Route path="*" element={<NotFound />} />      </Routes>    </BrowserRouter>  );}

검색할 때는 글자마다 방문 기록이 쌓이지 않도록 replace: true를 사용합니다. 상세 이동은 새 기록을 만들어 뒤로 가기로 검색 화면을 복원합니다. “전체 목록” 링크는 검색을 초기화하는 별도 행동입니다.

Link는 앱 안의 경로 이동에 사용합니다. 외부 사이트, 다운로드, 새 문서 요청이 필요한 링크는 일반 a를 사용합니다. 클릭 가능한 div로 이동을 재구현하지 않습니다.

세션 실습

90분 세션에서 15분은 URL 설계, 35분은 위 예제 실행, 25분은 자신의 명단·검색 조건으로 확장, 15분은 서로의 링크 검증에 사용합니다. 실습 시간은 운영 제안이며 VOD 재생 시간은 별도입니다.

  1. /?q=A를 직접 열고 A만 표시되는지 확인합니다.
  2. 상세로 이동한 뒤 브라우저 뒤로 가기를 누릅니다. 검색어와 결과가 복원되어야 합니다.
  3. /members/999, /members/abc, /unknown을 직접 엽니다.
  4. 트랙 필터를 추가할 때 기존 q를 지우지 않고 track query만 변경합니다.
  5. 개발 서버와 배포 서버에서 상세 URL을 새로고침합니다.

배포한 SPA에서 상세 새로고침만 404라면 앱 코드에 도달하기 전에 호스팅 서버가 파일을 찾는 경우입니다. 사용하는 호스팅의 SPA fallback 규칙을 확인합니다. API나 정적 자산 요청까지 무조건 index.html로 바꾸지 않습니다.

완료 기준과 자가 점검

  • 검색 URL을 다른 창에 붙여도 같은 필터 결과가 보입니다.
  • 모든 상세 링크는 키보드로 열 수 있습니다.
  • 없는 데이터와 없는 경로에 복구 링크가 있습니다.
  • 질문: URL parameter와 query string 중 검색어를 query에 둔 이유는 무엇인가요?
  • 질문: 이 예제의 404 화면이 서버의 HTTP 404 상태까지 자동으로 보장하나요? SPA의 화면 분기와 서버 응답 상태는 별개입니다.

더 읽어보기

연결된 PBL 미션과 VOD

주차·미션참고 VOD 범위
7주 · React Router리액트 실무 5·6장

7–9주의 VOD 표시는 외부 상호작용·서버 연동·앱 제작 범위를 함께 안내합니다. Router·TypeScript·Supabase의 세부 API는 각 공식 문서로 보완합니다.

강좌 안내: 참고 강좌 1 · 참고 강좌 2. 이 문서는 영상 전체를 옮긴 전사 자료가 아닙니다. 세부 내용은 위 공식 문서에서 확인합니다.