라우트 그룹 활용
URL을 바꾸지 않는 라우트 그룹으로 화면군을 조직하고 독립 레이아웃·병렬 슬롯·인터셉팅 모달을 구성합니다.
App Router의 강력함은 단순히 URL 경로를 파일 시스템으로 매핑하는 것을 넘어, 개발자가 더 유연하고 체계적으로 라우트 구조를 관리할 수 있도록 지원하는 데 있습니다.
그 중심에 있는 기능 중 하나가 바로 라우트 그룹(Route Groups)입니다.
라우트 그룹은 특정 폴더들을 URL 경로에 영향을 주지 않으면서 논리적으로 묶거나, 특정 그룹에만 고유한 레이아웃을 적용하고 싶을 때 사용됩니다.
이는 복잡한 애플리케이션에서 라우트 구조를 깔끔하게 유지하고, 다양한 레이아웃 요구사항을 효율적으로 처리할 수 있게 해줍니다.
라우트 그룹이란 무엇인가요?
라우트 그룹은 폴더 이름을 괄호(())로 감싸서 정의합니다.
예를 들어 (marketing), (auth)와 같이 생성할 수 있습니다.
- URL 경로에 영향 없음: 라우트 그룹 폴더는 실제 URL 경로에는 나타나지 않습니다. 즉,
app/(marketing)/about/page.tsx는/about경로에 매핑됩니다. - 논리적 그룹화: 개발자가 관련된 라우트들을 한데 모아 관리하기 위한 목적으로 사용됩니다. 예를 들어,
(marketing),(dashboard),(auth)등 기능별로 그룹화할 수 있습니다. - 독립적인 레이아웃 적용: 가장 중요한 활용 사례 중 하나는 특정 라우트 그룹에만 고유한 레이아웃을 적용하는 것입니다. 예를 들어, 웹사이트의 마케팅 페이지들은 특정 레이아웃을 사용하고, 대시보드 페이지들은 또 다른 레이아웃을 사용할 때 유용합니다.
layout.tsx와 함께 사용: 라우트 그룹 안에layout.tsx파일을 정의하면, 해당 그룹 내의 모든 페이지와 중첩 라우트에 그 레이아웃이 적용됩니다.
라우트 그룹 활용 실습
4장 2절에서 대시보드 레이아웃을 만들어 보았습니다.
이제 마케팅 관련 페이지들(예: 홈, 소개, 문의)에는 또 다른, 독립적인 레이아웃을 적용하고 싶다고 가정해 봅시다.
목표 구조- 마케팅 섹션: 홈 (
/), 소개 (/about), 문의 (/contact)- 이 페이지들은
RootLayout을 공유하지만, 만약(marketing)그룹 내부에 별도의 레이아웃을 추가한다면 그 레이아웃이 중첩될 수 있습니다.
- 이 페이지들은
- 대시보드 섹션: 대시보드 홈 (
/dashboard), 개요 (/dashboard/overview) 등- 이 페이지들은
(dashboard)그룹 내의DashboardLayout을 공유하며, 이 또한RootLayout내부에 중첩됩니다.
- 이 페이지들은
src/app/(marketing) 라우트 그룹 생성:
src/app 디렉터리 안에 (marketing)이라는 이름의 새 폴더를 생성합니다.
그리고 기존에 src/app/page.tsx와 src/app/about/page.tsx가 있다면 이 파일들을 (marketing) 폴더 안으로 이동시킵니다.
또한 src/app/(marketing)/contact/page.tsx를 만듭니다. 이동한 기존 파일은 원래 위치에 중복해서 남기지 않습니다.
-
src/app/(marketing)/contact/page.tsx내용src/app/(marketing)/contact/page.tsx export default function ContactPage() { return ( <div> <h3>문의하기</h3> <p>문의 사항은 프로젝트에서 안내한 공식 채널을 통해 접수할 수 있습니다.</p> </div> ); }
src/app/(dashboard) 라우트 그룹 생성:
src/app/(dashboard) 폴더를 만들고 기존 src/app/dashboard 폴더 전체를 그 안으로 이동합니다.
최종 경로는 src/app/(dashboard)/dashboard입니다.
괄호가 붙은 (dashboard)는 URL에서 빠지지만, 안쪽의 일반 dashboard 폴더가 /dashboard 세그먼트를 유지합니다.
루트 레이아웃의 <nav> 링크 업데이트:
src/app/layout.tsx 파일의 내비게이션 링크들을 업데이트하여 새로 만든 contact 페이지로 가는 링크도 추가합니다.
// ...
<header style={{ backgroundColor: '#f0f0f0', padding: '10px', borderBottom: '1px solid #ddd' }}>
<nav>
<Link href="/">홈</Link> | {' '}
<Link href="/about">소개</Link> | {' '}
<Link href="/contact">문의</Link> | {' '} {/* 새 링크 추가 */}
<Link href="/dashboard">대시보드</Link>
</nav>
<h1>나 혼자 Next.js</h1>
</header>
// ...실습 확인:
개발 서버(npm run dev)를 실행한 후, 다음 URL들을 방문하며 URL 경로가 어떻게 유지되는지 확인해 보세요.
http://localhost:3000/(여전히 홈 페이지)http://localhost:3000/about(여전히 소개 페이지)http://localhost:3000/contact(새로 만든 문의 페이지)http://localhost:3000/dashboard(여전히 대시보드 홈)
라우트 그룹 이름 (marketing), (dashboard)는 URL에 나타나지 않고, 일반 폴더인 contact, dashboard만 URL 세그먼트가 됩니다.
이것이 라우트 그룹의 가장 기본적인 역할입니다.
라우트 그룹별 독립적인 레이아웃 적용
이제 그룹별 레이아웃을 적용해 보겠습니다. 이 실습에는 src/app/layout.tsx가 있으므로 그룹 레이아웃은 그 안에 중첩됩니다. 최상위 레이아웃을 따로 두는 복수 루트 구성과는 다르며, 복수 루트 사이의 이동은 전체 페이지 로드를 일으킵니다.
만약 마케팅 페이지들에는 특정 헤더나 푸터가 대시보드 페이지와 다르게 필요하다면, (marketing) 그룹 내부에 layout.tsx를 정의할 수 있습니다.
src/app/(marketing)/layout.tsx 생성:
src/app/(marketing) 폴더 안에 layout.tsx 파일을 생성하고 다음 내용을 작성합니다.
export default function MarketingLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div style={{ border: '2px dashed #0070f3', padding: '20px', margin: '20px 0', borderRadius: '10px' }}>
<h2 style={{ color: '#0070f3' }}>마케팅 섹션 (그룹 레이아웃)</h2>
{children}
</div>
);
}- 이
MarketingLayout은(marketing)그룹 내의 모든 페이지(홈, 소개, 문의)에 적용됩니다. - 이 레이아웃은
RootLayout의children으로 렌더링되고, 이 레이아웃의children으로 다시 해당 페이지의page.tsx콘텐츠가 렌더링되는 중첩 구조를 가집니다.
실습 확인: 개발 서버를 다시 확인하고 다음 URL들을 방문해 보세요.
http://localhost:3000/http://localhost:3000/abouthttp://localhost:3000/contact
이 세 페이지에서는 이제 최상위 헤더/푸터(RootLayout) 안에, 파란색 점선 테두리를 가진 마케팅 섹션 영역(MarketingLayout)이 보이고 그 안에 페이지 콘텐츠가 나타날 것입니다.
반면, src/app/(dashboard)/dashboard/layout.tsx는 http://localhost:3000/dashboard에 계속 적용됩니다.
이처럼 라우트 그룹을 사용하면 URL 경로를 깔끔하게 유지하면서도, 필요에 따라 다양한 레이아웃을 유연하게 적용할 수 있습니다.
이는 특히 대규모 애플리케이션이나 여러 섹션이 독립적인 디자인을 가질 때 매우 유용합니다.
주의사항
동일한 URL 세그먼트를 가진 여러 라우트 그룹을 만들 수도 있습니다.
예를 들어, (auth)/login/page.tsx와 (admin)/login/page.tsx는 모두 /login 경로로 매핑됩니다.
이 경우 Next.js는 어떤 page.tsx를 렌더링해야 할지 모호해지므로 에러가 발생합니다.
따라서 각 라우트 그룹의 최종 URL 경로는 고유해야 합니다.
라우트 그룹은 레이아웃이나 폴더 구조를 위한 논리적 그룹화에 사용하고, 실제 URL 충돌이 발생하지 않도록 주의해야 합니다.
라우트 그룹은 Next.js App Router의 구조화 및 유연성을 높이는 데 핵심적인 역할을 합니다.
이를 잘 활용하면 복잡한 애플리케이션도 효율적으로 관리하고 확장할 수 있습니다.
예시: 그룹을 지운 뒤 남는 URL로 충돌 판단하기
(marketing)/contact/page.tsx에서 그룹 부분만 제외하면 /contact가 남습니다. (support)/contact/page.tsx도 같은 URL이므로 두 그룹 이름이 달라도 독립 라우트 두 개로 사용할 수 없습니다. 반면 (support)/help/page.tsx는 /help이므로 충돌하지 않습니다. 이 실습처럼 공통 src/app/layout.tsx가 위에 있으면 각 그룹 레이아웃은 그 안에 중첩됩니다. 그룹마다 별도 루트를 두어 공통 최상위 레이아웃이 없는 구성의 이동 수명까지 같은 것으로 일반화하지 않습니다.
병렬 라우트 (Parallel Routes): @slot으로 UI를 독립 스트림으로 분리하기
이하 병렬 라우트는 src/app/dashboard를 사용하는 별도 구성 예시입니다. 앞에서 대시보드를 (dashboard) 그룹으로 옮긴 프로젝트에 적용한다면 해당 그룹 안의 dashboard를 수정하고, 같은 URL의 두 폴더를 함께 만들지 마세요.
NEXT · CH4-4
세 슬롯을 세 layout props로 받습니다
기본 page는 children, @analytics는 analytics, @team은 team입니다. 슬롯 이름은 URL에 포함되지 않고 layout이 세 UI를 함께 배치합니다.
그림은 파일과 prop의 대응이며 실제 두 패널의 좌우 배치는 본문의 grid 코드가 정합니다. 두 슬롯의 page 구현은 생략했습니다. 직접 접근·새로고침의 미일치 슬롯에는 default.tsx가 필요하며 없으면 404입니다.
대시보드에서 분석 패널과 팀 활동 패널을 동시에 보여주고 싶다면, 영역별 탐색·로딩·오류 상태를 라우트 단위로 관리할 때 병렬 라우트를 사용할 수 있습니다. 단순히 두 컴포넌트를 나란히 놓는 것만으로는 병렬 라우트가 필요하지 않습니다.
핵심 아이디어@analytics,@team처럼@접두사의 슬롯 폴더를 만들면, 레이아웃이 각 슬롯을 독립적으로 렌더링할 수 있습니다.- 슬롯마다
loading.tsx,error.tsx,default.tsx를 따로 둘 수 있어 장애 격리가 쉬워집니다.
아래 코드는 레이아웃 조립과 분석 슬롯의 상태 파일을 보여줍니다. @analytics/page.tsx, @team/page.tsx의 데이터·화면 구현과 팀 슬롯의 상태 파일 내용은 생략했습니다. 파일 트리의 이름만 만들어 전체 패널이 완성되는 예제는 아닙니다.
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div>
{children}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<section>{analytics}</section>
<section>{team}</section>
</div>
</div>
);
}export default function DashboardPage() {
return <h1>대시보드 개요</h1>;
}children도 이름 없는 기본 슬롯이므로 /dashboard를 구성할 page.tsx가 필요합니다.
레이아웃은 이 기본 슬롯과 @analytics, @team 슬롯을 함께 받아 원하는 위치에 배치합니다.
export default function LoadingAnalytics() {
return <p>분석 데이터를 불러오는 중...</p>;
}'use client';
export default function AnalyticsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<p>분석 패널 오류: {error.message}</p>
<button onClick={reset}>다시 시도</button>
</div>
);
}export default function AnalyticsDefault() {
return <p>분석 패널 기본 화면</p>;
}예시: 생략했던 두 패널 page를 채워 함께 확인하기
파일 트리와 layout 코드만으로는 두 패널 본문이 만들어지지 않습니다. 아래 세 파일을 앞의 같은 대시보드 가지에 추가하면 외부 데이터 없이도 슬롯과 prop의 대응을 확인할 수 있습니다. 대시보드를 (dashboard)로 옮긴 프로젝트라면 모든 파일도 그 그룹 안의 같은 dashboard에 둡니다. 두 위치에 같은 URL의 대시보드를 함께 만들지 않습니다.
export default function AnalyticsPage() {
return <h2>분석 패널: 방문 120회</h2>;
}export default function TeamPage() {
return <h2>팀 패널: 활동 3건</h2>;
}export default function TeamDefault() {
return <p>팀 패널 기본 화면</p>;
}이 숫자는 로딩·DB 조회가 없는 고정 표시용 예시입니다. /dashboard에서는 기존 기본 page와 두 패널 page가 각각 children, analytics, team으로 전달되어 함께 보입니다. 현재 숫자를 보고 세 슬롯이 같은 데이터 요청을 공유한다고 추론하지 않습니다. 이 최소 확인에는 팀 슬롯의 오류·로딩 파일이 필수는 아니며, 실제 비동기 작업을 붙일 때 각 경계의 내용을 채웁니다.
독립 탐색 조건으로 @team/settings/page.tsx만 추가하고 /dashboard/settings로 클라이언트 이동했다고 가정합니다. 분석 슬롯에는 그 하위 경로가 없습니다. 이동 전 분석 화면이 보이던 상태와 그 URL에서 새로고침한 상태의 분석 슬롯 결과를 각각 예측하세요. 기본 children에도 해당 경로가 없다는 조건이라면 어느 기본 파일을 더 검토해야 할까요?
현재 활성 슬롯과 새로 복원할 슬롯을 구별하기
클라이언트 이동은 미일치 분석 슬롯의 현재 활성 화면을 유지할 수 있습니다. 새로고침은 그 활성 이력을 복원할 수 없으므로 @analytics/default.tsx를 사용하며, 해당 기본 파일도 없으면 404가 됩니다. 기본 children도 이 독립 조건에서 미일치하므로 부모 dashboard의 default.tsx를 검토합니다. 앞의 실제 대시보드에 settings/page.tsx가 이미 있다면 children은 매칭되므로 그 경우까지 미일치로 판단하지 않습니다. 같은 주소여도 파일 구성과 이동 문맥이 모두 필요합니다. 병렬 라우트의 활성 상태와 default 규약에 근거한 예측이며 이 문서에서 실제 요청을 실행한 결과는 아닙니다.
병렬 라우트 Pitfall 체크리스트
- 슬롯 폴더명은 반드시
@slotName형태여야 합니다. - 클라이언트 이동은 URL과 매칭되지 않는 다른 슬롯의 활성 화면을 유지할 수 있습니다. 직접 접근·새로고침에서는 미일치 슬롯에
default.tsx가 필요하고, 없으면 404가 됩니다.children도 미일치할 수 있는 구성이라면 별도의 기본 화면이 필요합니다. - 슬롯별
loading.tsx,error.tsx를 분리해 장애 전파 범위를 줄입니다. - 슬롯 간 의존 데이터를 최소화해 한 패널 실패가 전체 레이아웃 실패로 번지는 상황을 피합니다.
인터셉팅 라우트 (Intercepting Routes): 모달로 상세 화면 띄우기
같은 사진 URL을 여는 두 경로
feed의 클라이언트 Link 이동은 배경을 유지한 modal이고 같은 URL 직접 진입·새로고침은 전체 photo입니다. feed에서 modal을 연 뒤 새로고침하는 실제 상태 변화로 URL과 탐색 문맥의 차이를 설명할 수 있습니다.
NEXT · CH4-4
같은 /photo/42도 탐색 문맥에 따라 다릅니다
피드의 Link 탐색은 기존 피드 위에 모달을, 직접 접근이나 새로고침은 전체 사진 페이지를 보여줍니다. @modal은 라우트 세그먼트로 세지 않습니다.
본문은 구성 예제입니다. feed의 Link 목록과 오버레이 CSS는 별도로 작성합니다. 닫기는 router.back()이며 default.tsx의 null이 모든 클라이언트 이동에서 모달을 닫는 규칙은 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 피드의 클라이언트 이동은 원래 피드 위에 모달 경로를 렌더하고 직접 접근이나 새로고침은 전체 사진 페이지를 렌더한다.
- 동일한 photo/42 URL이 피드 링크 이동이면 모달, 직접 접근이나 새로고침이면 전체 페이지가 된다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 같은 사진 URL을 여는 두 경로 |
| 4 | 피드의 Link 탐색은 기존 피드 위에 모달을, 직접 접근이나 새로고침은 전체 사진 페이지를 보여 줍니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| feed에서 출발 feed가 현재 배경으로 표시됩니다. | 현재 화면: feed · photo: 아직 열기 전 | 클라이언트 이동의 출발 문맥이 준비됩니다. |
| Link로 modal 열기 feed 안의 client 이동은 배경을 유지한 modal photo를 엽니다. | URL: photo 경로 · 배경: feed 유지 · 출력: modal | URL 이동 뒤에도 feed 문맥이 남습니다. |
| 같은 URL 새로고침 현재 photo URL을 새로고침하면 전체 photo 페이지로 들어갑니다. | URL: 같은 photo · 출력: 전체 photo · 조건: 새로고침·직접 진입 | 탐색 문맥이 사라져 표시 형태가 바뀝니다. |
같은 사진 URL을 여는 두 경로
feed의 클라이언트 Link 이동은 배경을 유지한 modal이고 같은 URL 직접 진입·새로고침은 전체 photo입니다. feed에서 modal을 연 뒤 새로고침하는 실제 상태 변화로 URL과 탐색 문맥의 차이를 설명할 수 있습니다.
NEXT · CH4-4
같은 /photo/42도 탐색 문맥에 따라 다릅니다
피드의 Link 탐색은 기존 피드 위에 모달을, 직접 접근이나 새로고침은 전체 사진 페이지를 보여줍니다. @modal은 라우트 세그먼트로 세지 않습니다.
본문은 구성 예제입니다. feed의 Link 목록과 오버레이 CSS는 별도로 작성합니다. 닫기는 router.back()이며 default.tsx의 null이 모든 클라이언트 이동에서 모달을 닫는 규칙은 아닙니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- 피드의 클라이언트 이동은 원래 피드 위에 모달 경로를 렌더하고 직접 접근이나 새로고침은 전체 사진 페이지를 렌더한다.
- 동일한 photo/42 URL이 피드 링크 이동이면 모달, 직접 접근이나 새로고침이면 전체 페이지가 된다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 같은 사진 URL을 여는 두 경로 |
| 4 | 피드의 Link 탐색은 기존 피드 위에 모달을, 직접 접근이나 새로고침은 전체 사진 페이지를 보여 줍니다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| feed에서 출발 feed가 현재 배경으로 표시됩니다. | 현재 화면: feed · photo: 아직 열기 전 | 클라이언트 이동의 출발 문맥이 준비됩니다. |
| Link로 modal 열기 feed 안의 client 이동은 배경을 유지한 modal photo를 엽니다. | URL: photo 경로 · 배경: feed 유지 · 출력: modal | URL 이동 뒤에도 feed 문맥이 남습니다. |
| 같은 URL 새로고침 현재 photo URL을 새로고침하면 전체 photo 페이지로 들어갑니다. | URL: 같은 photo · 출력: 전체 photo · 조건: 새로고침·직접 진입 | 탐색 문맥이 사라져 표시 형태가 바뀝니다. |
피드 목록(/feed)에서 카드 클릭 시 전체 페이지 이동 대신 모달 상세를 띄우고, 새로고침/직접 접근에서는 전체 페이지(/photo/[id])를 보여주고 싶을 때 Intercepting Routes가 유용합니다.
(..)는 파일 시스템 폴더 수가 아니라 라우트 세그먼트를 셉니다. @modal은 세그먼트가 아니므로 feed/@modal/(..)photo에서 한 단계 위는 루트의 photo입니다.
아래는 라우팅과 닫기 동작에 집중한 구성 예시입니다. feed/page.tsx에서 /photo/42 같은 경로로 이동하는 Link 목록과 .modal-backdrop, .modal-card의 오버레이 CSS는 별도로 작성해야 합니다.
export default function FeedLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<>
{children}
{modal}
</>
);
}import PhotoModalClient from './PhotoModalClient';
export default async function PhotoModalPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <PhotoModalClient id={id} />;
}'use client';
import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
export default function PhotoModalClient({ id }: { id: string }) {
const router = useRouter();
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const previouslyFocused = document.activeElement instanceof HTMLElement
? document.activeElement
: null;
const previousOverflow = document.body.style.overflow;
closeButtonRef.current?.focus();
document.body.style.overflow = 'hidden';
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') router.back();
if (event.key === 'Tab') {
event.preventDefault();
closeButtonRef.current?.focus();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = previousOverflow;
previouslyFocused?.focus();
};
}, [router]);
return (
<div
className="modal-backdrop"
role="presentation"
onMouseDown={(event) => {
if (event.target === event.currentTarget) router.back();
}}
>
<div
className="modal-card"
role="dialog"
aria-modal="true"
aria-labelledby="photo-modal-title"
>
<h2 id="photo-modal-title">사진 상세 (모달) #{id}</h2>
<button ref={closeButtonRef} type="button" onClick={() => router.back()}>
닫기
</button>
</div>
</div>
);
}export default async function PhotoDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <h1>사진 상세 전체 페이지 #{id}</h1>;
}export default function FeedModalDefault() {
return null; // 모달이 없을 때 빈 슬롯
}예시: 피드 Link와 오버레이를 채워 이동 문맥 비교하기
앞의 모달·전체 페이지 코드는 유지하고, 생략했던 피드 파일을 다음처럼 작성합니다. 기본 Link의 클라이언트 이동을 사용하며 전체 페이지 새로고침을 강제하는 설정은 추가하지 않습니다.
import Link from 'next/link';
export default function FeedPage() {
return (
<section>
<h1>사진 피드</h1>
<ul>
<li><Link href="/photo/42">사진 42 보기</Link></li>
<li><Link href="/photo/84">사진 84 보기</Link></li>
</ul>
</section>
);
}루트 레이아웃에서 이미 가져오는 src/app/globals.css에는 다음 학습용 오버레이 규칙을 추가합니다. 기존 전역 규칙을 지우는 내용이 아니며, 포커스 대상이 닫기 버튼 하나인 현재 모달에 한정합니다.
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 16px;
background: rgb(0 0 0 / 0.45);
}
.modal-card {
width: min(100%, 28rem);
max-height: calc(100dvh - 32px);
overflow: auto;
padding: 24px;
border-radius: 12px;
background: white;
color: #111;
}먼저 /feed를 직접 열고 사진 42 Link를 누릅니다. 주소는 /photo/42로 바뀌고 피드가 남은 모달 문맥을 확인합니다. 닫기 버튼으로 뒤로 돌아온 뒤 /photo/42를 주소창에서 직접 열거나 그 주소에서 새로고침하여 전체 페이지 문맥과 비교합니다. 이 확인은 클릭·뒤로가기·새로고침을 구별하는 절차이며 현재 클라우드 교재 검수에서 이 학습 앱의 실제 화면을 실행한 기록은 아닙니다.
독립 조건에서 피드 Link로 사진 84를 연 뒤 새로고침했다면 어떤 page가 현재 화면을 담당할까요? @modal을 폴더 한 단계로 세어 (..)(..)photo로 바꿔야 할까요? default.tsx의 null만으로 모든 이후 클라이언트 이동의 닫힘을 보장할 수도 있을까요?
도착 주소와 이동 문맥을 함께 풀이하기
새로고침한 /photo/84는 전체 photo/[id]/page.tsx를 선택합니다. @modal은 URL 세그먼트가 아니므로 현재 feed에서 루트 photo로 향하는 (..)photo가 맞습니다. null 기본 화면은 미일치 슬롯을 복원하는 경우에 쓰고, 모든 클라이언트 이동의 닫힘을 자동 보장하지 않습니다. 현재 예제의 닫기는 router.back()이며 다시 앞으로 이동할 때의 모달 문맥도 별도로 확인합니다. 인터셉팅 라우트 규약의 세그먼트 기준과 직접 접근 조건을 연결합니다.
인터셉팅 라우트 Pitfall 체크리스트
(.),(..),(...)경로 깊이를 잘못 쓰면 의도치 않은 세그먼트를 가로챕니다.default.tsx의null은 직접 접근·새로고침에서 미일치 모달 슬롯을 비우는 기본값입니다. 모든 클라이언트 이동에서 모달을 닫는 규칙은 아니며, 이 예제의 닫기 동작은router.back()입니다.- 현재 포커스 처리는 닫기 버튼 하나만 있는 모달에 한정합니다. 입력·다른 버튼을 추가하면 전체 포커스 순환과 배경 상호작용 차단을 함께 구현해야 합니다. ESC 닫기와 배경 스크롤 복원도 확인합니다.
- 공유 가능한 URL이 필요하면 전체 페이지 라우트(
/photo/[id])를 반드시 유지합니다.