라우트 params과 location
목록 페이지와 상세 페이지는 라우팅 앱의 전형적인 쌍입니다. /vans는 모든 van을 보여주고, 하나를 클릭하면 /vans/2로 열려 해당 van의 상세 정보를 표시합니다. 모든 van마다 별도의 라우트를 작성한다면 데이터가 바뀔 때마다 라우터를 수정해야 합니다. Route params은 이 문제를 하나의 라우트 정의로 해결합니다. 경로의 placeholder가 그 자리의 어떤 값이든 받아서 저장하고, 페이지가 그 값을 읽어 올바른 데이터를 가져옵니다. 이 장에서는 이 패턴과 관련된 트릭을 다룹니다. 네비게이션과 함께 추가 정보를 전달하는 방법으로, 상세 페이지가 목록에 적용된 필터 같은 것들을 기억할 수 있게 합니다.
동적 세그먼트와 useParams
동적 세그먼트는 콜론으로 시작하는 경로 섹션입니다. 글자 그대로의 텍스트와 일치하는 대신, 그 위치의 어떤 값이든 일치시키고 선택한 이름 아래에 값을 저장합니다.
<Route path="/vans/:id" element={<VanDetail />} />이제 /vans/1, /vans/42, /vans/anything 모두 VanDetail을 렌더링합니다. 콜론은 id를 경로 내의 변수로 표시합니다. 함수 매개변수처럼 생각해 보세요. 라우트 정의는 한 번만 작성되고, URL이 인수를 제공합니다. 목록 페이지는 보통 라우팅의 Link 컴포넌트를 사용해 데이터를 매핑하면서 id를 삽입해 각 항목을 자신의 URL로 연결합니다.
{vans.map(van => (
<Link key={van.id} to={`/vans/${van.id}`}>
<h3>{van.name}</h3>
</Link>
))}반대편에서는 useParams 훅이 일치한 경로의 동적 세그먼트마다 하나의 속성을 가진 객체를 반환하며, 콜론 뒤의 이름으로 키를 정합니다.
import { useParams } from 'react-router-dom'
export default function VanDetail() {
const params = useParams()
// /vans/2에서 → { id: "2" }
return <h1>Van #{params.id}</h1>
}경로는 /vans/:id/:type처럼 여러 세그먼트를 가질 수 있으며, 각각은 자신의 키로 나타납니다. 일치한 param은 항상 문자열입니다. 숫자처럼 보여도 URL에서 바로 나오기 때문입니다.
param을 위한 데이터 가져오기
param은 보통 fetch의 키입니다. 상세 페이지는 URL에서 id를 가져오고 그 하나의 레코드를 요청하며, 데이터 가져오기에서 완전히 다루는 effect 기반 패턴을 따릅니다.
import { useState, useEffect } from 'react'
import { useParams } from 'react-router-dom'
export default function VanDetail() {
const { id } = useParams()
const [van, setVan] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
let active = true
setVan(null)
setError(null)
fetch(`/api/vans/${id}`)
.then(res => {
if (!res.ok) throw new Error(`Request failed: ${res.status}`)
return res.json()
})
.then(data => { if (active) setVan(data.vans) })
.catch(err => { if (active) setError(err) })
return () => { active = false }
}, [id])
if (error) return <h2>죄송합니다. 해당 van을 불러올 수 없습니다.</h2>
return van ? <h1>{van.name}</h1> : <h2>로딩 중...</h2>
}의존성 배열은 빈 상태로 유지되지 않고 id를 포함합니다. 앱이 한 상세 페이지에서 다른 상세 페이지로 연결되는 경우, 예를 들어 "비슷한 van" 섹션이 있다면, React Router는 컴포넌트를 언마운트하지 않으면서 URL과 param을 교체합니다. 빈 배열이라면 refetch를 건너뛰고 이전 van을 계속 표시합니다. id에 의존하면 effect를 다시 실행하고, 실행 맨 위에서 van을 비우는 것이 "로딩 중..."을 다시 화면에 가져오는 부분입니다. 이 없이는 van 5의 요청 전체 기간 동안 van 2의 이름이 렌더링된 상태로 유지됩니다.
res.ok 확인, catch, active 정리 플래그는 데이터 가져오기가 권장하는 것과 같은 보호이며, param 기반 페이지는 대부분보다 더 필요합니다. id는 누구나 수정할 수 있는 주소 표시줄에서 오고, 상세 페이지를 빠르게 클릭하는 것은 이미 떠난 van의 응답이 보고 있는 van보다 나중에 도착하는 방식입니다.
Link를 통한 state 전달
Param은 URL의 데이터를 페이지로 가져옵니다. 때로 페이지는 방문자가 어디서 왔는지 알고 싶어 합니다. 이 코스의 VanLife 프로젝트에서는 목록 페이지를 예를 들어 럭셔리 van만으로 필터링할 수 있습니다. 하나를 클릭하고 "모든 van으로 돌아가기"를 누르면, 일반 뒤로 가기 링크는 필터링되지 않은 목록에 도달합니다. 필터가 손실되어 여러 필터가 관련되면 빠르게 짜증이 납니다.
한 가지 해결책은 쿼리 문자열을 상세 페이지의 URL에 복사하는 것이며, 검색 params는 URL에 실린 이런 state가 올바른 경우를 다룹니다. 링크를 다른 사람과 공유할 때 생존합니다. 정보가 현재 방문자를 위한 UX 편의성일 때, React Router는 더 가벼운 채널을 제공합니다. Link는 state prop을 허용하고, 전달한 모든 것은 URL에 나타나지 않으면서 네비게이션과 함께 이동합니다.
<Link
to={`/vans/${van.id}`}
state={{ search: `?${searchParams.toString()}`, type: typeFilter }}
>
<h3>{van.name}</h3>
</Link>searchParams와 typeFilter는 목록 페이지 자신의 useSearchParams 값이며, 검색 params에서 다음에 완전히 다루므로, 링크는 전체 쿼리 문자열과 현재 필터 이름을 전달합니다. 모든 직렬화 가능한 값이 작동하지만, 명명된 속성이 있는 객체가 가장 읽기 좋습니다.
useLocation으로 읽기
목적지 페이지는 useLocation 훅으로 그 state를 읽으며, 이는 현재 위치를 설명하는 객체를 반환합니다. pathname, search (현재 URL 자신의 쿼리 문자열), 그리고 들어오는 Link가 전달한 모든 것을 보유하는 state입니다.
import { Link, useLocation } from 'react-router-dom'
export default function VanDetail() {
const location = useLocation()
const search = location.state?.search || ''
const type = location.state?.type || 'all'
return (
<Link to={`..${search}`} relative="path">
← {type} van으로 돌아가기
</Link>
)
}두 개의 fallback이 중요한 부분입니다. 북마크, 공유 링크, 입력된 URL에서 /vans/2에 직접 도착한 누군가는 location.state가 null로 설정되어 도착합니다. 아무 Link가 보내지 않았기 때문입니다. 그러면 location.state.search를 읽으면 throw되므로, optional chaining과 fallback이 페이지를 작동시킵니다. 뒤로 가기 링크는 일반 목록으로 가고 텍스트는 "모든 van으로 돌아가기"를 읽습니다. state가 있으면, 링크는 정확한 쿼리 문자열을 복원하고 텍스트는 "럭셔리 van으로 돌아가기" 또는 무엇이든 필터였던 것이 됩니다.
relative="path" prop은 ..가 라우트 트리의 한 수준 대신 URL 세그먼트 하나를 올라가게 하는데, 상세 라우트가 URL이 시사하는 것과 다른 부모를 가질 때 중요합니다. 중첩 라우트가 그 구별을 설명합니다.
param 기반 페이지의 sad path
이 코스는 에러 처리를 happy path와 sad path로 나눕니다. happy path는 모든 요청이 성공한다고 가정하고, sad path는 실패하는 요청을 계획합니다. 로딩과 에러 메커니즘 자체는 데이터 가져오기에 속합니다. 결과와 에러 모두 여전히 비어있는 것에서 파생된 로딩, catch에서 설정된 error state, 그리고 누락된 데이터에서 충돌하지 않고 피드백을 렌더링하는 early return입니다.
이 장이 더하는 것은 param 기반 페이지가 sad path를 곱한다는 것입니다. URL은 사용자 편집 가능한 입력이므로, /vans/999는 한 자리 거리에 있으며 서버는 존재하지 않는 id에 대해 404로 응답합니다. 위의 res.ok 확인이 그 응답을 van 모양의 객체 대신 에러 state와 메시지로 변환하는 것입니다. 그리고 위에서 다룬 대로, 네비게이션 state는 없을 수 있습니다. param을 통해 네트워크에 도달하는 페이지는 param이 잘못될 수 있고, 요청이 실패할 수 있으며, state는 null일 수 있다고 가정하고, 각 경우에 그럴듯한 무언가를 렌더링해야 합니다.
버전 note
React Router v5.1 이전에는 라우트 정보가 props로만 도착했습니다. 컴포넌트는 props.match.params와 props.location을 읽었습니다. v5.1은 useParams와 useLocation 훅을 추가했고, v6은 prop 기반 API를 제거하여, 훅을 유일한 방법으로 남겼습니다. 라우터 아래의 모든 컴포넌트는 깊이 렌더링되어도 이들을 호출할 수 있습니다.
/vans/:id는 어떤 값이든 채울 수 있는 빈칸을 만들므로, 하나의 라우트가 목록의 모든 항목을 위한 상세 페이지를 제공합니다. 페이지 내부에서 useParams는 올바른 데이터를 가져올 수 있도록 그 값을 전달합니다. 링크는 또한 state prop을 통해 약간의 추가 정보 패키지를 전달할 수 있으며, useLocation은 반대편에서 이를 읽습니다. 누군가 북마크에서 도착했을 수 있으므로 패키지가 없을 수 있다는 것을 기억하고, 항상 fallback을 준비해 두세요.
다음: 검색 params - 필터와 정렬 순서가 URL에 살아있는 곳입니다.

