중첩 라우트와 레이아웃
라우팅에서 다룬 것은 단순한 형태였습니다. 페이지마다 Route 하나씩, 각각의 path와 element를 가진 구조였습니다. 실제 앱은 곧 이 구조를 벗어나게 됩니다. 왜냐하면 URL이 중첩되기 때문입니다. /host/income 같은 경로는 두 부분으로 나뉘는데, 보통 더 깊은 경로의 페이지는 더 얕은 경로의 페이지 대부분을 유지합니다. 같은 네비게이션 바, 같은 섹션 탭이 있고, 아래에만 새로운 내용이 바뀝니다. 중첩 라우트는 이를 React Router로 표현하는 방식입니다. Route 요소를 서로 중첩시키면, 라우터는 일치하는 전체 체인을 한 번에 렌더링합니다. 부모가 자식을 감싸는 형태입니다.
그래서 "중첩 라우트"는 두 가지 관련된 의미를 가집니다. URL 세그먼트가 중첩되고, UI도 함께 중첩됩니다. 페이지의 일부는 더 깊이 네비게이션하면서도 유지되고, 내부 부분만 바뀝니다. 두 번째 특징이 진정한 가치입니다.
Outlet으로 라우트 중첩하고 자식 렌더링하기
Route는 자체 종료 요소로 작동할 수도 있고, 자식을 가질 수도 있습니다. 자식은 다른 Route 요소여야 합니다. 자식 라우트가 일치하면, React Router는 부모의 element를 먼저 렌더링하고, 부모는 Outlet을 렌더링해서 자식의 element가 어디에 나타날지 결정합니다.
import { BrowserRouter, Routes, Route } from 'react-router-dom'
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="vans" element={<Vans />} />
</Route>
</Routes>
</BrowserRouter>
)
}import { Outlet } from 'react-router-dom'
import Header from './Header'
export default function Layout() {
return (
<>
<Header />
<Outlet />
</>
)
}Outlet은 부모의 element에서 일치하는 자식이 렌더링될 위치에 구멍을 뚫어줍니다. 컴포지션에서 children prop이 하는 역할과 같습니다. 부모는 공유되는 구조를 렌더링하고 변동하는 부분이 들어갈 자리를 표시합니다. 다른 점은 React Router가 그 구멍을 채우는데, 현재 URL과 일치하는 자식 라우트를 선택한다는 것입니다. Outlet 없으면 부모의 element가 페이지를 차지하고 일치하는 자식은 어디도 렌더링되지 않는데, 중첩 페이지가 빈 화면으로 나타날 때 먼저 확인해야 할 것입니다.
위의 외부 라우트는 **레이아웃 라우트**입니다. element는 있고 path는 없습니다. 경로 없는 라우트는 자식이 일치하는 모든 URL과 일치하므로, Layout이 모든 페이지를 감싼다는 뜻입니다. 바로 헤더와 푸터 같은 사이트 전체 UI가 들어갈 자리입니다. Routes 옆에 렌더링된 헤더도 같은 일을 하지만, 앱에 정확히 하나의 레이아웃만 있을 때만 가능합니다. 레이아웃 라우트는 /host 같은 섹션이 아래 페이지를 건드리지 않으면서 다른 UI로 바뀔 수 있게 합니다. 피해야 할 선택지는 모든 페이지 컴포넌트 안에 헤더를 반복하는 것입니다.
경로를 가진 레이아웃
레이아웃 라우트는 path도 가질 수 있는데, 이렇게 하면 공유되는 UI의 범위가 앱의 한 섹션으로 제한됩니다. 과정의 VanLife 프로젝트에는 자체 보조 네비게이션을 가진 /host 섹션이 있으므로, 호스트 페이지는 첫 번째 레이아웃 안에 중첩된 두 번째 레이아웃을 얻습니다.
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />} />
<Route path="host" element={<HostLayout />}>
<Route path="income" element={<Income />} />
<Route path="reviews" element={<Reviews />} />
</Route>
</Route>HostLayout은 호스트 네비게이션과 Outlet을 렌더링하므로, /host/income에서 라우터는 Layout, 그 outlet에 HostLayout, 호스트 레이아웃의 outlet에 Income을 렌더링합니다. 세 개의 라우트가 동시에 일치하고, 각 부모는 다음 것을 감쌉니다. 디자인이 요구하는 만큼 깊이 레이아웃을 쌓을 수 있습니다.
인덱스 라우트
위 설정에 한 가지 빈틈이 있습니다. HostLayout은 /host에서 렌더링되지만, 그 outlet에 보여줄 것이 없습니다. 모든 자식이 경로에 세그먼트를 추가하기 때문입니다. 해결책은 인덱스 라우트입니다. path 대신 index prop을 사용하면, 그 자식은 부모의 outlet에서 부모 자신의 경로에 렌더링됩니다.
<Route index element={<Dashboard />} />이제 /host는 호스트 네비게이션을 보여주고 그 안에 대시보드를 표시하고, /host/income은 대시보드를 소득 페이지로 바꿉니다. index를 "기본 자식"으로 생각하면 됩니다. 더 깊은 세그먼트가 추가되기 전에 outlet에 속할 것입니다. 최상위 레벨에서도 작동하는데, <Route index element={<Home />} />은 루트 레이아웃 안의 홈 페이지이고, 위 예제에서 path="/"로 쓴 것과 같은 라우트입니다.
상대 경로와 상대 링크
위의 자식 경로를 보세요. income에는 슬래시가 없습니다. 슬래시로 시작하는 경로는 절대 경로이고, 사이트의 루트에서 측정됩니다. 슬래시 없는 경로는 부모 라우트에 상대적이므로, 중첩된 income은 /host/income을 의미합니다. 이렇게 하면 깊은 라우트 트리를 유지하기 쉬워집니다. host를 한 곳에서 admin으로 이름을 바꾸면 모든 하위 경로가 따라갑니다.
링크도 같은 방식입니다. 라우트된 element 안에서 렌더링된 Link는 상대 경로를 그것을 렌더링한 라우트에 대해 해석하므로, HostLayout 안의 네비게이션은 완전히 /host 접두사를 빼버릴 수 있습니다.
import { Link, Outlet } from 'react-router-dom'
export default function HostLayout() {
return (
<>
<nav>
<Link to=".">Dashboard</Link>
<Link to="income">Income</Link>
<Link to="reviews">Reviews</Link>
</nav>
<Outlet />
</>
)
}두 개의 특수한 값은 파일 시스템 경로에서 의미를 가져옵니다. to="." 는 현재 라우트로 링크하므로, 대시보드 링크가 하드코딩하지 않고도 /host 자체를 목표로 합니다. to=".." 는 위로 올라가는데, "목록으로 돌아가기" 링크에 유용합니다. 한 가지 미묘한 점이 있는데, URL 세그먼트 하나가 아니라 라우트 하나를 올라가기 때문입니다. 한 라우트가 여러 세그먼트를 소유할 때부터 중요해지는데, 라우트 params와 location에서 실제 뒤로 가기 버튼에 이를 활용합니다.
NavLink와 상위 요소 일치
Link 요소를 NavLink로 바꾸면 현재 페이지를 스타일링할 수 있고, 과정에서 호스트 네비게이션에서 이를 하므로, 상위 요소 일치가 작동합니다. 라우터는 현재 URL의 모든 상위 요소를 일치한 것으로 계산하므로, to="." 대시보드 링크는 /host/income과 /host/reviews에서도 활성으로 표시됩니다. 그 링크에 end를 추가하면 /host 아래 어디서든 스타일이 없어집니다.
useOutletContext로 데이터 전달하기
레이아웃은 종종 자식이 필요한 데이터를 소유합니다. 과정의 HostVanDetail은 한 대의 밴을 가져오고 상태에 유지한 다음, 탭 네비게이션과 정보, 가격 책정 또는 사진 페이지가 나타나는 Outlet을 렌더링합니다. Props는 Outlet을 통과할 수 없습니다. 레이아웃이 그 컴포넌트를 직접 렌더링하지 않기 때문입니다. React Router의 답은 Outlet의 context prop이고, 다른 쪽에서는 useOutletContext로 읽습니다.
<Outlet context={{ currentVan }} />import { useOutletContext } from 'react-router-dom'
export default function HostVanInfo() {
const { currentVan } = useOutletContext()
return <h4>Name: {currentVan.name}</h4>
}context에 전달하는 어떤 값이든 useOutletContext가 현재 outlet을 채우는 자식에서 반환하는 것입니다. 객체를 전달하고 받는 쪽에서 구조 분해하는 것이 일반적인 형태이고, 나중에 더 많은 값을 추가할 여지를 남깁니다. Context에 익숙하다면, 이것도 익숙할 것입니다. outlet은 페이지의 정확히 한 구멍으로 범위가 제한된 provider로 작동합니다.
중첩할지 말지
중첩은 라우트가 UI를 공유할 때 의미가 있습니다. 이것이 기준입니다. URL만으로는 중첩으로 유도할 수 있습니다. /vans와 /vans/:id는 세그먼트를 공유하니까 라우트도 공유하지 않을까요? 하지만 VanLife에서 그 두 페이지는 사이트 전체 UI 너머로는 아무것도 공유하지 않습니다. 중첩하면 어색한 형태가 강요됩니다. path는 있고 element는 없는 부모 라우트가 생기고, 이것은 단순히 Outlet을 렌더링하는 것으로 폴백되며, index 자식이 목록을 렌더링합니다. 이는 더 많은 구조이면서 이점이 없습니다.
공유되는 UI를 화면에 유지할 필요가 있으면, 중첩하고 부모에 Outlet을 가진 레이아웃 element를 제공하세요. 유일한 동기가 반복되는 경로 세그먼트를 피하는 것이라면, 두 개의 평탄한 라우트가 더 간단하고 명확한 선택입니다.
버전 참고
여기의 예제는 react-router-dom에서 import하는데, v6과 v7에서 모두 작동합니다. 라우팅에서 v7이 패키지에 대해 변경한 내용을 다룹니다. 중첩 라우트 개념은 두 버전에서 동일합니다.
Outlet을 놓습니다. element는 있고 경로는 없는 라우트는 모든 것을 감싸는 레이아웃이고, index 라우트는 부모 자신의 주소에서 보여지는 기본 자식입니다.
두 페이지가 화면에서 아무것도 공유하지 않으면, 중첩할 필요가 없습니다.
다음: 라우트 params와 location. 한 라우트가 많은 페이지를 제공합니다.

