본문으로 건너뛰기

Next.js 5주차 — URL, 슬롯, 화면 전환

대시보드에서 팀 설정을 열어도 옆의 통계는 그대로 남을 수 있습니다. 사진 목록에서는 상세를 모달로 열었다가, 같은 주소를 새로고침하면 전체 상세 페이지가 나오기도 합니다. 이런 화면이 어떤 라우트 구성으로 이어지는지 정리했습니다.

여기에 쓰이는 기능이 Dynamic Routes, Parallel Routes, Intercepting Routes입니다. URL의 값을 받는 부분은 사진 상세로, 여러 화면을 함께 두는 부분은 대시보드로 나눴습니다. 마지막에는 사진 목록 위에 상세 모달을 여는 구조로 연결했습니다.

기능하는 일이번 예시
Dynamic RoutesURL의 바뀌는 구간에서 값을 받음/photo/42의 42
Parallel Routes같은 layout에서 여러 라우트의 UI를 함께 렌더링팀 정보와 방문 통계
Intercepting Routes설정한 내부 이동에서 목적지 내용을 현재 layout 안에 표시목록 위에 사진 상세 모달 열기
5주차 · 역할 구분

5주차의 세 가지 역할

개념을 선택하면 세부 문법이 펼쳐집니다. 선은 포함·조합 관계입니다.

세 기능은 필수 순차 단계가 아니라 각각 사용할 수 있는 기능입니다.

도식 초기 상태
Dynamic RoutesURL에서 값 받기
Parallel Routes라우트 자리 함께 두기
Intercepting Routes현재 화면 안에서 열기
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.

세 기능은 필수 순차 단계가 아니라 각각 사용할 수 있는 기능입니다.

도식 초기 상태
Dynamic RoutesURL에서 값 받기
Parallel Routes라우트 자리 함께 두기
Intercepting Routes현재 화면 안에서 열기
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.
설명용 상태 모형입니다. 실제 Next.js Router나 서버 요청을 실행하지 않습니다.

각 기능은 따로 쓸 수 있습니다. 사진 상세 페이지에는 Dynamic Route만 사용하고, 마지막의 모달 예시에는 Parallel과 Intercepting을 함께 넣었습니다.

1. Dynamic Routes — 사진 ID를 URL에서 받기​

사진 42와 사진 73은 같은 상세 화면을 쓰고 ID만 다릅니다. 주소에서 바뀌는 자리를 [id]로 두면 하나의 page.tsx로 두 경로를 받을 수 있습니다.

app/photo/[id]/page.tsx
│
├─ /photo/42 → id = '42'
└─ /photo/73 → id = '73'

URL을 /로 나눈 한 구간이 segment입니다. /photo/42에서는 photo와 42가 각각 한 구간이고, [id]가 두 번째 구간의 값을 받습니다.

이 값은 페이지의 params prop으로 전달됩니다. 현재 App Router에서는 Promise로 받기 때문에 아래 코드에서는 await해서 읽었습니다. 숫자처럼 보여도 id의 값은 문자열 '42'입니다.

// app/photo/[id]/page.tsx
export default async function Page({ params }: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return <h1>사진 {id}</h1>
}

코드는 주소에서 받은 ID만 표시합니다. 실제 사진 데이터를 조회하는 부분은 생략했습니다.

경로가 여러 구간일 때​

뒤에 붙는 구간 수에 따라 표기도 달라집니다. 아래 세 줄은 같은 위치에 함께 만드는 폴더가 아니라, 선택할 수 있는 서로 다른 규칙입니다.

폴더주소 예시await params의 결과
shop/[slug]/shop/shoes{ slug: 'shoes' }
shop/[...slug]/shop/clothes/tops{ slug: ['clothes', 'tops'] }
shop/[[...slug]]/shop도 포함/shop에서는 { slug: undefined }

[...slug]는 한 개 이상의 구간을 배열로 받습니다. [[...slug]]는 뒤에 구간이 없는 /shop도 매칭합니다. 카테고리처럼 주소의 깊이가 달라지는 경우에 사용할 수 있습니다. (catch-all 규칙)

도식에서 /shop을 선택한 뒤 [...slug]와 [[...slug]]를 바꾸면, 구간이 없는 주소의 매칭 여부를 비교할 수 있습니다.

5주차 · 결과 비교

주소가 params로 들어오는 과정

주소와 폴더 표기를 바꾸면 매칭 여부와 값이 바뀝니다.

await params로 읽은 값입니다. 숫자처럼 보여도 문자열입니다.

도식 초기 상태
폴더app/photo/[id]/page.tsx
입력/photo/42
결과{ "id": "42" }
미리 제공한 ID인가예: 42 또는 73
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.

await params로 읽은 값입니다. 숫자처럼 보여도 문자열입니다.

도식 초기 상태
폴더app/photo/[id]/page.tsx
입력/photo/42
결과{ "id": "42" }
미리 제공한 ID인가예: 42 또는 73
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.
설명용 상태 모형입니다. 실제 Next.js Router나 서버 요청을 실행하지 않습니다.

미리 아는 ID가 있다면​

42와 73처럼 빌드 전에 알 수 있는 ID는 generateStaticParams()로 제공할 수 있습니다. 다음 함수는 앞의 photo/[id]/page.tsx에 함께 두는 예시입니다.

export function generateStaticParams() {
return [{ id: '42' }, { id: '73' }]
}

이 함수는 빌드에 사용할 ID 목록을 제공합니다. 실제 사전 렌더링 범위에는 페이지의 데이터와 렌더링 설정도 영향을 줍니다. [id]는 주소에 변수 자리가 있다는 뜻이지, 매 요청마다 화면 전체를 새로 만든다는 뜻은 아닙니다.

2. Parallel Routes — 팀 정보와 통계를 함께 두기​

대시보드에서는 팀 정보와 방문 통계를 함께 보여준다고 가정했습니다. Parallel Routes는 이렇게 같은 layout에서 여러 라우트의 UI를 함께 렌더링하는 기능입니다.

@team, @analytics처럼 폴더에 이름을 붙이면 각각 별도의 slot이 됩니다. layout 안에서 라우트가 들어갈 자리입니다. 부모 layout은 이를 team, analytics prop으로 받고, 일반 페이지가 들어오는 children도 암묵적인 slot입니다. (슬롯 구성)

app/dashboard/
├─ layout.tsx
├─ page.tsx ← children: 대시보드 안내
├─ settings/page.tsx ← children: 설정 안내
├─ @team/
│ ├─ page.tsx ← 팀 개요
│ └─ settings/page.tsx ← 팀 설정
└─ @analytics/
├─ page.tsx ← 방문 통계
└─ default.tsx ← 복원할 수 없을 때의 안내

children과 팀 슬롯에는 settings 페이지가 있고, 통계 슬롯에는 없습니다. 이 차이는 다음의 이동·새로고침 예시에서 비교합니다. layout은 아래처럼 세 자리를 함께 렌더링합니다. 배치용 CSS는 생략했습니다.

// app/dashboard/layout.tsx
import type { ReactNode } from 'react'

type Props = { children: ReactNode; team: ReactNode; analytics: ReactNode }

export default function Layout({ children, team, analytics }: Props) {
return <>{children}{team}{analytics}</>
}
5주차 · 결과 비교

layout 안의 세 슬롯

팀 슬롯의 상태를 바꿔도 나머지 두 자리는 유지됩니다.

팀 슬롯의 예시 상태만 바뀝니다. 나머지 슬롯의 표시 내용은 그대로입니다.

도식 초기 상태
주소/dashboard
children대시보드 안내
team팀 개요
analytics방문 통계
통계에 settings 페이지없음
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.

팀 슬롯의 예시 상태만 바뀝니다. 나머지 슬롯의 표시 내용은 그대로입니다.

도식 초기 상태
주소/dashboard
children대시보드 안내
team팀 개요
analytics방문 통계
통계에 settings 페이지없음
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.
설명용 상태 모형입니다. 실제 Next.js Router나 서버 요청을 실행하지 않습니다.

@team이라는 이름은 URL에 들어가지 않습니다. app/dashboard/@team/settings/page.tsx는 /dashboard/settings에 연결됩니다. 화면을 좌우로 나누거나 위아래로 놓는 배치는 layout의 JSX와 CSS가 맡습니다.

정보 두 개를 나란히 보여주는 정도라면 일반 컴포넌트로도 충분합니다. 이 예시에서는 팀과 통계가 각자 어느 하위 페이지를 보여주고 있는지까지 라우터가 관리합니다. 슬롯마다 loading과 error도 따로 둘 수 있습니다. (슬롯별 경계)

3. Link로 이동한 뒤, 같은 주소에서 새로고침하면​

/dashboard에서 시작하면 팀 개요와 방문 통계가 함께 나옵니다. 여기서 Link로 /dashboard/settings에 이동하면 본문과 팀 영역은 설정 화면으로 바뀌고, 통계 영역에는 기존 방문 통계가 남습니다.

이런 앱 내부 이동을 Soft Navigation이라고 합니다. Next.js가 슬롯별 활성 페이지를 추적하고 있어서, 새 URL에 매칭되는 페이지가 없는 슬롯도 이전 화면을 유지할 수 있습니다.

5주차 · 결과 비교

같은 설정 주소, 다른 슬롯 결과

Link로 이동한 다음 같은 주소에서 새로고침을 비교합니다.

현재 URL과 직접 매칭됩니다.

도식 초기 상태
주소/dashboard
children대시보드 안내
team팀 개요
analytics방문 통계
통계에 settings 페이지없음
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.

현재 URL과 직접 매칭됩니다.

도식 초기 상태
주소/dashboard
children대시보드 안내
team팀 개요
analytics방문 통계
통계에 settings 페이지없음
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.
설명용 상태 모형입니다. 실제 Next.js Router나 서버 요청을 실행하지 않습니다.
상태children@team@analytics
/dashboard대시보드 안내팀 개요방문 통계
설정 주소로 Link 이동설정 안내팀 설정기존 방문 통계
그 주소에서 새로고침설정 안내팀 설정default.tsx 안내

같은 /dashboard/settings에서 브라우저를 새로고침하면 통계 영역이 달라집니다. 문서를 새로 여는 Hard Navigation에서는 URL에 매칭되지 않는 슬롯의 이전 활성 페이지를 복원할 수 없습니다. 통계 쪽에는 settings가 없으므로, 그 슬롯에 준비한 default.tsx가 표시됩니다.

여기서 말하는 새로고침은 브라우저의 전체 페이지 새로고침입니다. router.refresh()와는 구분했습니다.

loading.tsx는 결과를 기다리는 동안의 화면이고, default.tsx는 복원할 수 없는 슬롯을 채울 대체 화면입니다. 통계 조회가 느려서 default가 나오는 상황은 아닙니다.

children에도 설정 페이지를 만들어뒀기 때문에, 이 예시에서 default로 바뀌는 곳은 @analytics 하나입니다. children까지 URL에 매칭되지 않는 구조라면 그쪽에도 fallback이 필요합니다.

4. Intercepting Routes — 목록 위에 사진 상세 열기​

사진 목록으로 돌아오겠습니다. 목록은 /, 사진 상세는 /photo/42로 두었습니다. 목록에서 사진을 누를 때는 상세를 모달로 열고, 주소를 직접 열 때는 전체 페이지로 보여주는 구성입니다.

Intercepting Routes는 설정한 내부 이동에서 다른 라우트의 내용을 현재 layout 안에 표시합니다. 여기에 Parallel Routes의 @modal 자리를 함께 사용했습니다. (라우트 가로채기)

app/
├─ layout.tsx ← children과 modal을 함께 렌더링
├─ page.tsx ← / : 사진 목록
├─ photo/[id]/page.tsx ← 전체 사진 상세
└─ @modal/
├─ default.tsx ← 매칭되지 않는 경우 null
├─ page.tsx ← /로 내부 이동할 때 null
└─ (.)photo/[id]/page.tsx ← 모달 안의 사진 상세

Parallel은 modal 자리를 만들고, Intercepting은 내부 이동의 상세 화면을 그 자리에 연결합니다. @modal도 앞의 @team처럼 부모 layout에 prop으로 전달됩니다. layout은 children과 modal을 함께 렌더링하고, 겹쳐 보이는 모양이나 닫기·포커스 처리는 모달 컴포넌트에서 구현합니다.

(.)photo는 같은 route segment 수준의 photo를 가로채는 표시입니다. 폴더로는 @modal 안에 들어 있지만, 슬롯 이름은 URL 구간이 아니어서 깊이에 세지 않습니다.

5주차 · 결과 비교

사진 주소와 모달의 이동 기록

사진 열기 → 뒤로 → 앞으로 → 브라우저 새로고침 순서로 비교합니다.

목록에서 사진을 눌러 이동을 시작합니다.

도식 초기 상태
주소/
children사진 목록
modalnull
선택 파일app/page.tsx
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.

목록에서 사진을 눌러 이동을 시작합니다.

도식 초기 상태
주소/
children사진 목록
modalnull
선택 파일app/page.tsx
초기 상태의 읽기용 표입니다. 조작 화면이 열리지 않아도 본문 설명은 남습니다.
설명용 상태 모형입니다. 실제 Next.js Router나 서버 요청을 실행하지 않습니다.
들어온 방식선택하는 페이지화면
/의 목록에서 사진 Link 클릭@modal/(.)photo/[id]/page.tsx목록 위의 사진 모달
/photo/42 직접 접속·새로고침photo/[id]/page.tsx전체 사진 상세

두 경우 모두 주소는 /photo/42입니다. 목록에서 이동할 때는 interception이 적용돼 배경의 목록이 남고, 새 문서로 열 때는 원래 상세 페이지가 표시됩니다. 위처럼 연결한 경로에서의 동작입니다.

다른 위치에서 가로채는 경우
표기route segment 기준
(.)같은 수준
(..)한 수준 위
(..)(..)두 수준 위
(...)app 루트

app/gallery/@modal에서 루트의 photo로 연결한다면 (..)photo입니다. gallery는 한 구간을 차지하고, @modal은 깊이에 들어가지 않습니다. (matcher 규칙)

5. 닫기와 뒤로가기​

목록에서 사진 모달을 연 직후 router.back()을 호출하면 이전 목록으로 돌아가면서 모달이 닫힙니다. 그 뒤 같은 이동 기록에서 앞으로 가면 다시 열립니다. back()과 forward()는 이 이동 기록을 따라갑니다.

Link로 /에 이동해서 닫는 경우에는 @modal/page.tsx가 null을 반환하도록 둡니다. Soft Navigation에서는 매칭되지 않는 슬롯의 이전 화면이 남을 수 있으니, 홈 주소에 매칭되는 빈 페이지로 모달 자리를 바꿉니다. default.tsx만으로 모든 내부 이동에서 모달이 닫히는 것은 아닙니다. (모달 닫기)

다른 경로로 이동할 때도 모달을 닫으려면 @modal/[...catchAll]/page.tsx에서 null을 반환하는 패턴으로 확장할 수 있습니다. [...catchAll]은 구간이 하나 이상 있어야 매칭되므로, /에 대응하는 @modal/page.tsx는 별도로 남깁니다.

사진 상세 페이지는 그대로 남겨뒀습니다. URL에서 ID를 받고, 상세를 보여줄 자리를 나눈 뒤, 목록에서 이동할 때만 모달로 연결했습니다. 같은 사진 주소를 직접 열면 전체 상세 페이지가 나오는 구성입니다.

참고​