중첩 라우트와 레이아웃 구성
대시보드의 계층적 URL을 폴더로 구성하고 중첩 레이아웃으로 공통 헤더와 사이드바를 유지합니다.
이전 절에서는 App Router의 파일 기반 라우팅과
page.tsx가 UI를 렌더링하는 구조를 확인했습니다.
또 동적 라우트로 가변 URL을 처리하는 방법도 살펴봤습니다.
앞 절의 동적 라우트와 이번 대시보드 경로는 서로 다른 파일 가지입니다. 앞 장에서 만든 Next.js 프로젝트의 루트 레이아웃을 유지한 채 아래 dashboard 파일을 추가합니다. URL이 어느 page.tsx를 선택하는지 먼저 찾고, 그 파일에서 위로 올라가며 layout.tsx만 모으면 화면을 감싸는 순서를 알 수 있습니다.
이번 절에서는 이를 확장해 중첩 라우트(Nested Routes)를 구현하는 방법을 다룹니다.
중첩 라우트는 UI의 특정 부분이 다른 UI 안에 포함되는 계층적인 구조를 만들 때 사용됩니다.
이는 웹 애플리케이션에서 매우 흔한 패턴으로, 예를 들어 대시보드 내의 여러 서브 페이지들이 공통된 사이드바나 헤더를 공유하는 경우가 대표적입니다.
중첩 라우트의 기본 원리
App Router에서 중첩 라우트는 폴더 구조와 layout.tsx 파일의 조합으로 구현됩니다.
- 폴더의 중첩:
app디렉터리의 일반 라우트 폴더를 중첩하면 URL 세그먼트도 중첩됩니다. 라우트 그룹(group)이나 슬롯@slot은 URL에 포함되지 않습니다. 예를 들어,app/dashboard/settings는/dashboard/settings경로를 나타냅니다. - 레이아웃의 중첩: 각 폴더에
layout.tsx파일을 정의하면, 해당 레이아웃은 그 폴더와 모든 하위 폴더의page.tsx또는 다른layout.tsx를 감싸게 됩니다. 이때, 상위 폴더의 레이아웃이 하위 폴더의 레이아웃을 감싸는 형태로 레이아웃이 중첩됩니다.
/dashboard/settings에서는 공통 레이아웃 안에 설정 페이지가 들어갑니다. dashboard/page.tsx는 이 하위 페이지의 조상 컴포넌트가 아닙니다.
NEXT · CH4-2
설정 본문을 감싸는 것은 두 layout입니다
/dashboard/settings의 설정 페이지는 RootLayout과 DashboardLayout의 children 안에 들어갑니다. dashboard/page.tsx가 하위 페이지를 감싸지는 않습니다.
상자는 UI 포함 관계입니다. 사이드바는 DashboardLayout 안에서 settings 본문과 나란히 배치됩니다. 공유 layout의 상태 유지에는 클라이언트 탐색 조건이 필요합니다.
이러한 중첩 구조는 다음과 같은 이점을 제공합니다.
- 코드 재사용성: 공통 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(루트 레이아웃 + 대시보드 레이아웃 + 설정 페이지)
각 대시보드 관련 페이지에서 상단의 헤더와 하단의 푸터(루트 레이아웃)가 유지되면서, 왼쪽에 대시보드 메뉴(대시보드 레이아웃)가 나타나고, 오른쪽에 해당 페이지의 콘텐츠가 변경되는 것을 볼 수 있습니다.
예시: 같은 사이드바 아래에서 선택되는 본문만 바뀌기
/dashboard/overview를 열면 RootLayout의 헤더·푸터, DashboardLayout의 사이드바, overview 본문이 함께 구성됩니다. 사이드바의 설정 Link로 이동하면 설정 본문이 선택됩니다. 공유 레이아웃 아래의 클라이언트 이동이라는 조건에서 공통 영역을 재사용합니다. /about으로 이동하면 대시보드 경로 밖이므로 사이드바를 감싸는 DashboardLayout도 현재 화면 구성에서 빠집니다. 각 페이지의 출력은 형제이며, 대시보드 홈 본문이 다른 본문 위에 한 번 더 붙는 구조가 아닙니다.
같은 프로젝트에 다음 파일을 추가하려고 합니다. settings 아래에는 page.tsx만 있고 추가 layout.tsx는 없다고 가정합니다. 주소와 감싸는 레이아웃을 예측하세요. 기존 settings/page.tsx의 내용도 자동으로 위에 나타날까요?
export default function SettingsHistoryPage() {
return <h3>설정 변경 이력</h3>;
}선택된 page에서 조상 layout만 찾아 풀이하기
주소는 /dashboard/settings/history입니다. 현재 본문은 history의 page.tsx이고, DashboardLayout과 RootLayout이 안쪽부터 바깥쪽으로 감쌉니다. settings/page.tsx는 /dashboard/settings에서 선택되는 다른 페이지이므로 history를 감싸거나 자동으로 합쳐지지 않습니다. 여러 설정 페이지가 공통 탭을 공유해야 한다면 그 공통 UI를 settings/layout.tsx로 나누는 선택을 검토합니다. 이는 파일의 역할로 예측한 결과이며 이 문서에서 실제 클라이언트 상태나 네트워크 요청 횟수를 측정한 기록은 아닙니다. 레이아웃의 children 계약을 같은 기준으로 읽습니다.
레이아웃의 장점과 고려사항
장점- UI 일관성: 웹사이트의 특정 섹션에 일관된 디자인과 기능을 적용하기 용이합니다.
- 성능: 공유 레이아웃을 재사용하는 클라이언트 이동에서는 그 안의 클라이언트 상태도 유지됩니다. 새로고침이나 다른 루트 레이아웃으로의 이동까지 같은 수명을 보장하지 않으며, 데이터 함수가 영구히 한 번만 실행된다는 뜻도 아닙니다.
- 코드 관리: 공통 UI 로직을 한곳에 모아 관리함으로써 코드 중복을 줄이고 유지보수성을 높입니다.
- 데이터 페칭: 공통 사이드바처럼 레이아웃 자체가 쓰는 데이터는 그 레이아웃에서 조회합니다. 특정 페이지에서만 필요한 데이터는 해당 페이지에 두어 다른 화면의 대기를 늘리지 않도록 합니다.
- 클라이언트 컴포넌트 사용: 레이아웃은 기본적으로 서버 컴포넌트이므로, 상호작용이 필요한 UI(예: 클릭 이벤트, 상태 관리)는 레이아웃 내부에 클라이언트 컴포넌트를 정의하고 이를 가져와 사용해야 합니다.
- 리렌더링 범위:
children으로 들어오는 라우트 콘텐츠는 이동에 따라 바뀝니다. 유지되는 Client Component도 자신의 상태나 context 변경으로 리렌더될 수 있습니다.