본문으로 건너뛰기

안동민 개발노트

본문 시작

중첩 라우트와 레이아웃 구성

대시보드의 계층적 URL을 폴더로 구성하고 중첩 레이아웃으로 공통 헤더와 사이드바를 유지합니다.

이전 절에서는 App Router의 파일 기반 라우팅과 page.tsx가 UI를 렌더링하는 구조를 확인했습니다.

또 동적 라우트로 가변 URL을 처리하는 방법도 살펴봤습니다.

이번 절에서는 이를 확장해 중첩 라우트(Nested Routes)를 구현하는 방법을 다룹니다.

중첩 라우트는 UI의 특정 부분이 다른 UI 안에 포함되는 계층적인 구조를 만들 때 사용됩니다.

이는 웹 애플리케이션에서 매우 흔한 패턴으로, 예를 들어 대시보드 내의 여러 서브 페이지들이 공통된 사이드바나 헤더를 공유하는 경우가 대표적입니다.

아래 표는 URL 폴더, layout.tsx, page.tsx가 중첩 화면에서 어떤 책임을 나누는지 먼저 정리합니다.

중첩 라우트는 URL 깊이와 layout 유지 범위를 함께 설계한다

폴더가 깊어질수록 URL도 깊어지고, 각 레벨의 layout은 자기 아래 page를 children으로 받는다.

요소파일 위치맡는 책임화면에서 유지되는 것
Root layoutsrc/ app/ layout.tsx앱 전체 공통 껍데기헤더, 푸터, 전역 스타일
Dashboard layoutsrc/ app/ dashboard/ layout.tsx대시보드 공통 UI사이드바, 섹션 제목
Dashboard homesrc/ app/ dashboard/ page.tsx/ dashboard 고유 화면대시보드 홈 콘텐츠
Overview pagesrc/ app/ dashboard/ overview/ page.tsx/ dashboard/ overview 고유 화면overview 콘텐츠
Settings pagesrc/ app/ dashboard/ settings/ page.tsx/ dashboard/ settings 고유 화면settings 콘텐츠

중첩 라우트의 기본 원리

아래 표는 폴더가 깊어질수록 URL과 레이아웃 중첩이 어떻게 함께 깊어지는지 정리한 것입니다.

폴더가 깊어지면 URL도 깊어지고 layout은 바깥에서 안쪽으로 중첩된다

중첩 구조를 읽을 때는 URL, 적용 layout, 교체되는 page를 한 줄로 맞춰 보면 된다.

URL적용 layoutchildren에 들어가는 page결과 화면
/root layoutsrc/ app/ page.tsx헤더, 홈, 푸터
/ aboutroot layoutabout/ page.tsx헤더, 소개, 푸터
/ dashboardroot + dashboard layoutdashboard/ page.tsx헤더, 사이드바, 홈, 푸터
/ dashboard/ overviewroot + dashboard layoutoverview/ page.tsx헤더, 사이드바, 개요, 푸터
/ dashboard/ settingsroot + dashboard layoutsettings/ 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만 바뀌는 구조입니다.

렌더링은 root layout, segment layout, page 순서로 조립된다

대시보드 안에서 이동하면 바깥 layout은 유지되고 가장 안쪽 page만 바뀐다.

조립 순서파일남는 UI바뀌는 UI
1src/ app/ layout.tsxhtml, body, header, footer거의 바뀌지 않음
2src/ app/ dashboard/ layout.tsx대시보드 사이드바dashboard 밖으로 나갈 때 제거
3dashboard/ page.tsx없음/ dashboard 본문
4dashboard/ overview/ page.tsxroot + dashboard layoutoverview 본문
5dashboard/ settings/ page.tsxroot + dashboard layoutsettings 본문

이러한 중첩 구조는 다음과 같은 이점을 제공합니다.

  • 코드 재사용성: 공통 UI 요소를 레이아웃에 정의하여 여러 페이지에서 재사용할 수 있습니다.
  • 일관된 UI/UX: 웹사이트 전체 또는 특정 섹션의 디자인과 레이아웃을 일관되게 유지할 수 있습니다.
  • 성능 최적화: 레이아웃 컴포넌트는 라우트 이동 시 상태를 유지하므로, 전체 페이지를 다시 렌더링할 필요 없이 변경된 부분만 효율적으로 업데이트할 수 있습니다.

중첩 라우트 구현 실습: 대시보드 구조 만들기

이전 장에서 만들었던 대시보드 예시를 확장하여, 중첩 라우트와 레이아웃의 관계를 명확히 이해해 봅시다.

목표 구조
layout.tsx
layout.tsx
page.tsx
page.tsx
page.tsx
page.tsx

이 모든 페이지들은 공통적으로 대시보드 레이아웃(사이드바 등)을 공유하고, 이 대시보드 레이아웃은 다시 최상위 루트 레이아웃 안에 중첩될 것입니다.

루트 레이아웃 (src/app/layout.tsx) 확인/수정: 모든 페이지에 적용되는 최상위 레이아웃입니다.

이미 3장 3절에서 다음과 같이 구성했을 것입니다.

src/app/layout.tsx
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>&copy; 2024 나 혼자 Next.js. All rights reserved.</p>
        </footer>
      </body>
    </html>
  );
}

대시보드 레이아웃 (src/app/dashboard/layout.tsx) 생성/수정: /dashboard 경로와 그 하위 모든 경로에 적용될 레이아웃입니다.

src/app/dashboard/layout.tsx
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>
  );
}
설명
  • 이 레이아웃은 RootLayoutchildren으로 렌더링됩니다.
  • 자체적으로 사이드바와 메인 콘텐츠 영역을 가집니다.
  • children prop은 /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(예: 클릭 이벤트, 상태 관리)는 레이아웃 내부에 클라이언트 컴포넌트를 정의하고 이를 가져와 사용해야 합니다.
  • 리렌더링 범위: 레이아웃은 라우트 변경 시에도 상태를 유지하지만, children prop으로 전달되는 페이지 콘텐츠는 변경될 때마다 새로 렌더링됩니다. 이 점을 이해하고 컴포넌트를 설계해야 합니다.

중첩 구조를 설계할 때는 공통 UI, 데이터, 상태가 어느 레벨에 속하는지 먼저 나누면 유지 범위가 명확해집니다.

공통 UI, 데이터, 상태는 필요한 가장 낮은 layout에 둔다

너무 높은 layout에 올리면 필요 없는 페이지까지 영향을 받고, 너무 낮게 두면 중복이 생긴다.

공유 대상두기 좋은 위치좋은 신호나쁜 신호
전역 header/ footerroot layout모든 페이지에서 필요특정 섹션 메뉴까지 전역에 둠
대시보드 sidebardashboard layout대시보드 하위에서만 유지about 페이지에도 메뉴가 보임
대시보드 사용자 정보dashboard layout하위 페이지가 공통 사용모든 앱 페이지가 기다림
페이지별 상세 데이터각 page.tsx필요한 화면에서만 조회layout fetch가 모든 하위 페이지를 지연
상호작용 상태작은 client component상태 필요한 일부만 hydratelayout 전체를 use client로 전환

라우트 계층을 실제 파일로 옮길 때는 각 레벨이 맡는 UI, 데이터, 오류 경계를 함께 배치하면 구조가 더 안정적입니다.

라우트 계층의 각 레벨은 UI, 데이터, 오류 경계를 함께 배치해야 안정적이다

폴더를 만들 때 page만 보지 말고 layout, loading, error가 필요한 범위도 같이 본다.

레벨대표 파일맡는 경계확인할 점
applayout.tsx문서 껍데기와 전역 UIhtml/ body 포함
dashboardlayout.tsx대시보드 공통 UI와 데이터children 렌더링
dashboard/ overviewpage.tsx개요 화면 콘텐츠URL과 폴더 일치
dashboard/ settingspage.tsx설정 화면 콘텐츠사이드바는 layout에서 유지
문제 영역error.tsx/ loading.tsx가까운 세그먼트 대체 UI너무 넓은 범위를 덮지 않음

중첩 라우트와 레이아웃은 Next.js App Router의 핵심적인 디자인 패턴입니다.

이를 통해 복잡한 웹 애플리케이션의 UI를 체계적이고 효율적으로 구성할 수 있습니다.

각 레이아웃의 역할과 중첩 방식을 명확히 이해한다면, 확장 가능하고 유지보수하기 쉬운 프로젝트를 만들 수 있을 것입니다.

중첩 라우트의 판단 흐름을 URL 깊이, 레이아웃 유지 범위, 데이터 배치 기준으로 다시 묶었습니다.

중첩 라우트 판단은 URL 깊이, 공유 UI, 데이터 위치, 교체 범위 순서로 좁힌다

대시보드처럼 여러 하위 화면이 있는 구조는 먼저 유지할 껍데기와 바뀔 본문을 나누면 된다.

질문판단 기준선택확인 위치
URL이 하위 구조인가/ dashboard/ settings폴더 중첩src/ app/ dashboard/ settings
공통 UI가 있는가대시보드 메뉴가 계속 보임dashboard/ layout.tsxsidebar 유지 여부
데이터가 공통인가하위 page 모두 필요layout에서 조회불필요한 fetch 범위
본문만 바뀌는가children 영역만 교체page.tsx 분리route 이동 후 유지 UI
문제가 어디서 발생하나섹션 단위 오류/ 로딩가까운 error/ loading전체 앱이 덮이는지

마지막으로 중첩 라우트가 대시보드 같은 화면 구조에서 URL과 레이아웃을 어떻게 맞추는지 정리합니다.

대시보드 중첩 라우트는 URL, 사이드바 링크, page 파일을 같은 표로 맞춘다

실습이 끝났는지 보려면 각 URL이 열리고, 사이드바는 유지되고, 오른쪽 본문만 바뀌는지 확인한다.

사이드바 링크URLpage 파일완료 신호
대시보드 홈/ dashboarddashboard/ page.tsx사이드바 + 홈 요약
개요/ dashboard/ overviewoverview/ page.tsx사이드바 + 개요 본문
분석/ dashboard/ analyticsanalytics/ page.tsx사이드바 + 분석 본문
설정/ dashboard/ settingssettings/ page.tsx사이드바 + 설정 본문
공통 확인모든 dashboard 하위 URLdashboard/ layout.tsx헤더/ 푸터/ 사이드바 유지