안동민 개발노트 아이콘

안동민 개발노트

4장 : 라우팅 심화

App Router 구조 이해하기

이전 절에서는 App Router의 파일 기반 라우팅과 page.tsx가 UI를 렌더링하는 구조를 확인했습니다. 또 동적 라우트로 가변 URL을 처리하는 방법도 살펴봤습니다.

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

중첩 라우트는 UI의 특정 부분이 다른 UI 안에 포함되는 계층적인 구조를 만들 때 사용됩니다. 이는 웹 애플리케이션에서 매우 흔한 패턴으로, 예를 들어 대시보드 내의 여러 서브 페이지들이 공통된 사이드바나 헤더를 공유하는 경우가 대표적입니다.


중첩 라우트의 기본 원리

아래 다이어그램은 이 절의 핵심 흐름을 역할과 상태 전환 중심으로 정리한 것입니다.

App Router에서 중첩 라우트는 폴더 구조와 layout.tsx 파일의 조합으로 구현됩니다.

  • 폴더의 중첩: app 디렉터리 내부에 폴더를 중첩하여 생성하는 것 자체가 URL 경로의 중첩을 의미합니다. 예를 들어, app/dashboard/settings/dashboard/settings 경로를 나타냅니다.
  • 레이아웃의 중첩: 각 폴더에 layout.tsx 파일을 정의하면, 해당 레이아웃은 그 폴더와 모든 하위 폴더의 page.tsx 또는 다른 layout.tsx를 감싸게 됩니다. 이때, 상위 폴더의 레이아웃이 하위 폴더의 레이아웃을 감싸는 형태로 레이아웃이 중첩됩니다.

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

  • 코드 재사용성: 공통 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="/"><a></a></Link> | {' '}
            <Link href="/about"><a>소개</a></Link> | {' '}
            <Link href="/dashboard"><a>대시보드</a></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"><a>대시보드 홈</a></Link>
          </li>
          <li style={{ marginBottom: '10px' }}>
            <Link href="/dashboard/overview"><a>개요</a></Link>
          </li>
          <li style={{ marginBottom: '10px' }}>
            <Link href="/dashboard/analytics"><a>분석</a></Link>
          </li>
          <li style={{ marginBottom: '10px' }}>
            <Link href="/dashboard/settings"><a>설정</a></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으로 전달되는 페이지 콘텐츠는 변경될 때마다 새로 렌더링됩니다. 이 점을 이해하고 컴포넌트를 설계해야 합니다.

중첩 라우트와 레이아웃은 Next.js App Router의 핵심적인 디자인 패턴입니다. 이를 통해 복잡한 웹 애플리케이션의 UI를 체계적이고 효율적으로 구성할 수 있습니다. 각 레이아웃의 역할과 중첩 방식을 명확히 이해한다면, 확장 가능하고 유지보수하기 쉬운 프로젝트를 만들 수 있을 것입니다.