중첩 라우트와 레이아웃 구성
대시보드의 계층적 URL을 폴더로 구성하고 중첩 레이아웃으로 공통 헤더와 사이드바를 유지합니다.
이전 절에서는 App Router의 파일 기반 라우팅과
page.tsx가 UI를 렌더링하는 구조를 확인했습니다.
또 동적 라우트로 가변 URL을 처리하는 방법도 살펴봤습니다.
이번 절에서는 이를 확장해 중첩 라우트(Nested Routes)를 구현하는 방법을 다룹니다.
중첩 라우트는 UI의 특정 부분이 다른 UI 안에 포함되는 계층적인 구조를 만들 때 사용됩니다.
이는 웹 애플리케이션에서 매우 흔한 패턴으로, 예를 들어 대시보드 내의 여러 서브 페이지들이 공통된 사이드바나 헤더를 공유하는 경우가 대표적입니다.
아래 표는 URL 폴더, layout.tsx, page.tsx가 중첩 화면에서 어떤 책임을 나누는지 먼저 정리합니다.
폴더가 깊어질수록 URL도 깊어지고, 각 레벨의 layout은 자기 아래 page를 children으로 받는다.
| 요소 | 파일 위치 | 맡는 책임 | 화면에서 유지되는 것 |
|---|---|---|---|
| Root layout | src/ app/ layout.tsx | 앱 전체 공통 껍데기 | 헤더, 푸터, 전역 스타일 |
| Dashboard layout | src/ app/ dashboard/ layout.tsx | 대시보드 공통 UI | 사이드바, 섹션 제목 |
| Dashboard home | src/ app/ dashboard/ page.tsx | / dashboard 고유 화면 | 대시보드 홈 콘텐츠 |
| Overview page | src/ app/ dashboard/ overview/ page.tsx | / dashboard/ overview 고유 화면 | overview 콘텐츠 |
| Settings page | src/ app/ dashboard/ settings/ page.tsx | / dashboard/ settings 고유 화면 | settings 콘텐츠 |
중첩 라우트의 기본 원리
아래 표는 폴더가 깊어질수록 URL과 레이아웃 중첩이 어떻게 함께 깊어지는지 정리한 것입니다.
중첩 구조를 읽을 때는 URL, 적용 layout, 교체되는 page를 한 줄로 맞춰 보면 된다.
| URL | 적용 layout | children에 들어가는 page | 결과 화면 |
|---|---|---|---|
| / | root layout | src/ app/ page.tsx | 헤더, 홈, 푸터 |
| / about | root layout | about/ page.tsx | 헤더, 소개, 푸터 |
| / dashboard | root + dashboard layout | dashboard/ page.tsx | 헤더, 사이드바, 홈, 푸터 |
| / dashboard/ overview | root + dashboard layout | overview/ page.tsx | 헤더, 사이드바, 개요, 푸터 |
| / dashboard/ settings | root + dashboard layout | settings/ page.tsx | 헤더, 사이드바, 설정, 푸터 |
App Router에서 중첩 라우트는 폴더 구조와 layout.tsx 파일의 조합으로 구현됩니다.
- 폴더의 중첩:
app디렉터리 내부에 폴더를 중첩하여 생성하는 것 자체가 URL 경로의 중첩을 의미합니다. 예를 들어,app/dashboard/settings는/dashboard/settings경로를 나타냅니다. - 레이아웃의 중첩: 각 폴더에
layout.tsx파일을 정의하면, 해당 레이아웃은 그 폴더와 모든 하위 폴더의page.tsx또는 다른layout.tsx를 감싸게 됩니다. 이때, 상위 폴더의 레이아웃이 하위 폴더의 레이아웃을 감싸는 형태로 레이아웃이 중첩됩니다.
다음 다이어그램처럼 실제 렌더링 흐름은
app/layout.tsx가 가장 바깥에서 감싸고,
그 안에서 dashboard/layout.tsx가 대시보드 공통 UI를 유지한 채
dashboard/page.tsx 또는 하위 page.tsx만 바뀌는 구조입니다.
대시보드 안에서 이동하면 바깥 layout은 유지되고 가장 안쪽 page만 바뀐다.
| 조립 순서 | 파일 | 남는 UI | 바뀌는 UI |
|---|---|---|---|
| 1 | src/ app/ layout.tsx | html, body, header, footer | 거의 바뀌지 않음 |
| 2 | src/ app/ dashboard/ layout.tsx | 대시보드 사이드바 | dashboard 밖으로 나갈 때 제거 |
| 3 | dashboard/ page.tsx | 없음 | / dashboard 본문 |
| 4 | dashboard/ overview/ page.tsx | root + dashboard layout | overview 본문 |
| 5 | dashboard/ settings/ page.tsx | root + dashboard layout | settings 본문 |
이러한 중첩 구조는 다음과 같은 이점을 제공합니다.
- 코드 재사용성: 공통 UI 요소를 레이아웃에 정의하여 여러 페이지에서 재사용할 수 있습니다.
- 일관된 UI/UX: 웹사이트 전체 또는 특정 섹션의 디자인과 레이아웃을 일관되게 유지할 수 있습니다.
- 성능 최적화: 레이아웃 컴포넌트는 라우트 이동 시 상태를 유지하므로, 전체 페이지를 다시 렌더링할 필요 없이 변경된 부분만 효율적으로 업데이트할 수 있습니다.
중첩 라우트 구현 실습: 대시보드 구조 만들기
이전 장에서 만들었던 대시보드 예시를 확장하여, 중첩 라우트와 레이아웃의 관계를 명확히 이해해 봅시다.
목표 구조이 모든 페이지들은 공통적으로 대시보드 레이아웃(사이드바 등)을 공유하고, 이 대시보드 레이아웃은 다시 최상위 루트 레이아웃 안에 중첩될 것입니다.
루트 레이아웃 (src/app/layout.tsx) 확인/수정:
모든 페이지에 적용되는 최상위 레이아웃입니다.
이미 3장 3절에서 다음과 같이 구성했을 것입니다.
import './globals.css';
import Link from 'next/link';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<header style={{ backgroundColor: '#f0f0f0', padding: '10px', borderBottom: '1px solid #ddd' }}>
<nav>
<Link href="/">홈</Link> | {' '}
<Link href="/about">소개</Link> | {' '}
<Link href="/dashboard">대시보드</Link>
</nav>
<h1>나 혼자 Next.js</h1>
</header>
<main style={{ padding: '20px' }}>
{children} {/* 여기에 중첩된 레이아웃 또는 페이지 콘텐츠가 들어옵니다 */}
</main>
<footer style={{ backgroundColor: '#e0e0e0', padding: '10px', marginTop: '20px', borderTop: '1px solid #ddd' }}>
<p>© 2024 나 혼자 Next.js. All rights reserved.</p>
</footer>
</body>
</html>
);
}대시보드 레이아웃 (src/app/dashboard/layout.tsx) 생성/수정:
/dashboard 경로와 그 하위 모든 경로에 적용될 레이아웃입니다.
import Link from 'next/link';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div style={{ display: 'flex', minHeight: 'calc(100vh - 180px)', border: '1px solid #ccc', borderRadius: '8px', overflow: 'hidden' }}>
<aside style={{ width: '200px', padding: '20px', backgroundColor: '#f9f9f9', borderRight: '1px solid #eee' }}>
<h2 style={{ marginBottom: '15px', color: '#333' }}>대시보드 메뉴</h2>
<ul style={{ listStyle: 'none', padding: 0 }}>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard">대시보드 홈</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/overview">개요</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/analytics">분석</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/settings">설정</Link>
</li>
</ul>
</aside>
<section style={{ flexGrow: 1, padding: '20px', backgroundColor: '#fff' }}>
{children} {/* 대시보드 하위 페이지 콘텐츠가 여기에 렌더링됩니다 */}
</section>
</div>
);
}- 이 레이아웃은
RootLayout의children으로 렌더링됩니다. - 자체적으로 사이드바와 메인 콘텐츠 영역을 가집니다.
childrenprop은/dashboard폴더 내의page.tsx또는 그 하위 폴더의layout.tsx또는page.tsx콘텐츠를 받습니다.
대시보드 하위 페이지 생성:
이제 dashboard 폴더 안에 각각의 페이지를 생성합니다.
-
src/app/dashboard/page.tsx(대시보드 홈)src/app/dashboard/page.tsx export default function DashboardHomePage() { return ( <div> <h3>환영합니다, 대시보드 홈입니다!</h3> <p>여기에 대시보드의 주요 정보를 요약하여 표시합니다.</p> </div> ); } -
src/app/dashboard/overview/page.tsx:src/app/dashboard안에overview폴더를 만들고 그 안에page.tsx를 생성합니다.src/app/dashboard/overview/page.tsx export default function DashboardOverviewPage() { return ( <div> <h3>대시보드 개요</h3> <p>이 페이지는 전체 시스템의 개요를 보여줍니다.</p> </div> ); } -
src/app/dashboard/analytics/page.tsx:src/app/dashboard안에analytics폴더를 만들고 그 안에page.tsx를 생성합니다.src/app/dashboard/analytics/page.tsx export default function DashboardAnalyticsPage() { return ( <div> <h3>대시보드 분석</h3> <p>사용자 데이터 및 트래픽 분석 정보를 제공합니다.</p> </div> ); } -
src/app/dashboard/settings/page.tsx:src/app/dashboard안에settings폴더를 만들고 그 안에page.tsx를 생성합니다.src/app/dashboard/settings/page.tsx export default function DashboardSettingsPage() { return ( <div> <h3>대시보드 설정</h3> <p>계정 및 대시보드 관련 설정을 변경할 수 있습니다.</p> </div> ); }
실습 확인:
개발 서버(npm run dev)를 실행한 후, 다음 URL들을 방문하며 중첩된 레이아웃이 어떻게 적용되는지 확인해 보세요.
http://localhost:3000/(루트 레이아웃만)http://localhost:3000/about(루트 레이아웃만)http://localhost:3000/dashboard(루트 레이아웃 + 대시보드 레이아웃 + 대시보드 홈 페이지)http://localhost:3000/dashboard/overview(루트 레이아웃 + 대시보드 레이아웃 + 개요 페이지)http://localhost:3000/dashboard/analytics(루트 레이아웃 + 대시보드 레이아웃 + 분석 페이지)http://localhost:3000/dashboard/settings(루트 레이아웃 + 대시보드 레이아웃 + 설정 페이지)
각 대시보드 관련 페이지에서 상단의 헤더와 하단의 푸터(루트 레이아웃)가 유지되면서, 왼쪽에 대시보드 메뉴(대시보드 레이아웃)가 나타나고, 오른쪽에 해당 페이지의 콘텐츠가 변경되는 것을 볼 수 있습니다.
레이아웃의 장점과 고려사항
장점- UI 일관성: 웹사이트의 특정 섹션에 일관된 디자인과 기능을 적용하기 용이합니다.
- 성능: 레이아웃은 페이지 이동 시 리렌더링되지 않으므로, 레이아웃 내의 클라이언트 컴포넌트 상태가 유지되고, 데이터 페칭도 한 번만 발생하여 성능 이점을 얻을 수 있습니다.
- 코드 관리: 공통 UI 로직을 한 곳에 모아 관리함으로써 코드 중복을 줄이고 유지보수성을 높입니다.
- 데이터 페칭: 레이아웃에서 데이터를 페칭할 경우, 해당 레이아웃이 적용되는 모든 하위 페이지에서 그 데이터가 필요해야 합니다. 특정 페이지에서만 필요한 데이터는 해당 페이지에서 페칭하는 것이 더 효율적일 수 있습니다.
- 클라이언트 컴포넌트 사용: 레이아웃은 기본적으로 서버 컴포넌트이므로, 상호작용이 필요한 UI(예: 클릭 이벤트, 상태 관리)는 레이아웃 내부에 클라이언트 컴포넌트를 정의하고 이를 가져와 사용해야 합니다.
- 리렌더링 범위: 레이아웃은 라우트 변경 시에도 상태를 유지하지만,
childrenprop으로 전달되는 페이지 콘텐츠는 변경될 때마다 새로 렌더링됩니다. 이 점을 이해하고 컴포넌트를 설계해야 합니다.
중첩 구조를 설계할 때는 공통 UI, 데이터, 상태가 어느 레벨에 속하는지 먼저 나누면 유지 범위가 명확해집니다.
너무 높은 layout에 올리면 필요 없는 페이지까지 영향을 받고, 너무 낮게 두면 중복이 생긴다.
| 공유 대상 | 두기 좋은 위치 | 좋은 신호 | 나쁜 신호 |
|---|---|---|---|
| 전역 header/ footer | root layout | 모든 페이지에서 필요 | 특정 섹션 메뉴까지 전역에 둠 |
| 대시보드 sidebar | dashboard layout | 대시보드 하위에서만 유지 | about 페이지에도 메뉴가 보임 |
| 대시보드 사용자 정보 | dashboard layout | 하위 페이지가 공통 사용 | 모든 앱 페이지가 기다림 |
| 페이지별 상세 데이터 | 각 page.tsx | 필요한 화면에서만 조회 | layout fetch가 모든 하위 페이지를 지연 |
| 상호작용 상태 | 작은 client component | 상태 필요한 일부만 hydrate | layout 전체를 use client로 전환 |
라우트 계층을 실제 파일로 옮길 때는 각 레벨이 맡는 UI, 데이터, 오류 경계를 함께 배치하면 구조가 더 안정적입니다.
폴더를 만들 때 page만 보지 말고 layout, loading, error가 필요한 범위도 같이 본다.
| 레벨 | 대표 파일 | 맡는 경계 | 확인할 점 |
|---|---|---|---|
| app | layout.tsx | 문서 껍데기와 전역 UI | html/ body 포함 |
| dashboard | layout.tsx | 대시보드 공통 UI와 데이터 | children 렌더링 |
| dashboard/ overview | page.tsx | 개요 화면 콘텐츠 | URL과 폴더 일치 |
| dashboard/ settings | page.tsx | 설정 화면 콘텐츠 | 사이드바는 layout에서 유지 |
| 문제 영역 | error.tsx/ loading.tsx | 가까운 세그먼트 대체 UI | 너무 넓은 범위를 덮지 않음 |
중첩 라우트와 레이아웃은 Next.js App Router의 핵심적인 디자인 패턴입니다.
이를 통해 복잡한 웹 애플리케이션의 UI를 체계적이고 효율적으로 구성할 수 있습니다.
각 레이아웃의 역할과 중첩 방식을 명확히 이해한다면, 확장 가능하고 유지보수하기 쉬운 프로젝트를 만들 수 있을 것입니다.
중첩 라우트의 판단 흐름을 URL 깊이, 레이아웃 유지 범위, 데이터 배치 기준으로 다시 묶었습니다.
대시보드처럼 여러 하위 화면이 있는 구조는 먼저 유지할 껍데기와 바뀔 본문을 나누면 된다.
| 질문 | 판단 기준 | 선택 | 확인 위치 |
|---|---|---|---|
| URL이 하위 구조인가 | / dashboard/ settings | 폴더 중첩 | src/ app/ dashboard/ settings |
| 공통 UI가 있는가 | 대시보드 메뉴가 계속 보임 | dashboard/ layout.tsx | sidebar 유지 여부 |
| 데이터가 공통인가 | 하위 page 모두 필요 | layout에서 조회 | 불필요한 fetch 범위 |
| 본문만 바뀌는가 | children 영역만 교체 | page.tsx 분리 | route 이동 후 유지 UI |
| 문제가 어디서 발생하나 | 섹션 단위 오류/ 로딩 | 가까운 error/ loading | 전체 앱이 덮이는지 |
마지막으로 중첩 라우트가 대시보드 같은 화면 구조에서 URL과 레이아웃을 어떻게 맞추는지 정리합니다.
실습이 끝났는지 보려면 각 URL이 열리고, 사이드바는 유지되고, 오른쪽 본문만 바뀌는지 확인한다.
| 사이드바 링크 | URL | page 파일 | 완료 신호 |
|---|---|---|---|
| 대시보드 홈 | / dashboard | dashboard/ page.tsx | 사이드바 + 홈 요약 |
| 개요 | / dashboard/ overview | overview/ page.tsx | 사이드바 + 개요 본문 |
| 분석 | / dashboard/ analytics | analytics/ page.tsx | 사이드바 + 분석 본문 |
| 설정 | / dashboard/ settings | settings/ page.tsx | 사이드바 + 설정 본문 |
| 공통 확인 | 모든 dashboard 하위 URL | dashboard/ layout.tsx | 헤더/ 푸터/ 사이드바 유지 |