React Router와 URL 상태
목록·상세·404 경로를 나누고 검색 조건과 직접 URL 진입을 검증하기
URL도 화면 상태다
목록에서 한 사람을 누른 뒤 주소를 복사했는데 다른 사람이 열면 첫 화면만 나온다면, 선택 상태가 브라우저 메모리에만 있었던 것입니다. 상세 대상은 URL 경로에, 공유할 검색 조건은 query string에 두면 새로고침·뒤로 가기·링크 공유가 같은 정보를 가리킵니다.
이 문서는 React Router의 Declarative 모드를 사용합니다. Next.js의 파일 라우팅, Router의 Framework 모드와 설정을 섞지 않습니다. 앞에서 만든 Vite React 프로젝트에 이어서 작업합니다.
경로와 데이터 계약
| 경로 | 화면 | 확인할 실패 |
|---|---|---|
/ | 아기사자 목록 | 검색 결과 없음 |
/members/1 | ID가 1인 구성원 상세 | 형식은 맞지만 없는 ID |
/members/abc | 잘못된 ID 안내 | 숫자로 바꿀 수 없는 값 |
/anything | 404 안내 | 정의되지 않은 경로 |
경로가 존재한다는 것과 데이터가 존재한다는 것은 다릅니다. /members/:id가 일치해도 조회 결과가 없으면 상세 화면에서 별도로 처리해야 합니다.
실행: 목록·상세·404 연결하기
프로젝트 폴더에서 npm install react-router를 실행합니다. 아래는 현재 Declarative API를 사용하는 예제입니다. 설치한 실제 버전과 lock 파일을 함께 남깁니다.
src/App.jsx 전체를 다음으로 바꿉니다. Vite가 생성한 src/main.jsx는 그대로 둡니다. 이 예제에서 BrowserRouter는 한 번만 선언합니다.
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 재생 시간은 별도입니다.
/?q=A를 직접 열고 A만 표시되는지 확인합니다.- 상세로 이동한 뒤 브라우저 뒤로 가기를 누릅니다. 검색어와 결과가 복원되어야 합니다.
/members/999,/members/abc,/unknown을 직접 엽니다.- 트랙 필터를 추가할 때 기존 q를 지우지 않고
trackquery만 변경합니다. - 개발 서버와 배포 서버에서 상세 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. 이 문서는 영상 전체를 옮긴 전사 자료가 아닙니다. 세부 내용은 위 공식 문서에서 확인합니다.