icon
3장 : App Router 기초

레이아웃 컴포넌트 사용하기

Next.js App Router의 가장 강력하고 유용한 기능 중 하나는 바로 레이아웃(Layouts) 입니다. 레이아웃은 여러 페이지에서 공유되는 UI를 정의하는 방법을 제공하며, 이는 코드 중복을 줄이고 일관된 사용자 경험을 유지하는 데 필수적입니다. 이전 절에서 layout.tsx 파일을 잠깐 언급했지만, 이번 절에서는 레이아웃의 개념을 더 깊이 이해하고 실제 프로젝트에 적용하는 방법을 상세히 알아보겠습니다.


레이아웃이란 무엇인가요?

레이아웃은 특정 라우트 세그먼트와 그 하위 라우트에서 공유되는 UI를 래핑하는(감싸는) React 컴포넌트입니다. 이는 웹사이트의 헤더, 푸터, 사이드바, 내비게이션 바 등 여러 페이지에 걸쳐 동일하게 나타나는 부분들을 한 곳에서 관리할 수 있게 해줍니다.

레이아웃의 주요 특징

  • 공유 UI: 레이아웃 내부에 정의된 UI는 해당 레이아웃이 적용되는 모든 하위 페이지에 자동으로 포함됩니다.
  • 중첩 가능: App Router에서는 레이아웃을 중첩하여 사용할 수 있습니다. 최상위 레이아웃(Root Layout)부터 특정 라우트 세그먼트에만 적용되는 하위 레이아웃까지 계층적으로 구성할 수 있습니다.
  • 상태 유지: 레이아웃 컴포넌트는 라우트 이동 시에도 상태(State)를 유지합니다. 즉, 레이아웃 내부의 클라이언트 컴포넌트 상태는 페이지가 변경되어도 그대로 유지됩니다. 이는 template.tsx와 가장 큰 차이점입니다.
  • 기본적으로 서버 컴포넌트: layout.tsx 파일은 기본적으로 서버 컴포넌트로 동작합니다.

최상위 레이아웃 (RootLayout) 이해하기

Next.js 프로젝트를 생성하면 src/app/layout.tsx 파일이 자동으로 생성됩니다. 이 파일은 애플리케이션의 최상위 레이아웃(Root Layout) 으로, 모든 페이지에 적용되는 가장 기본적인 UI 구조를 정의합니다.

src/app/layout.tsx
// src/app/layout.tsx
import './globals.css'; // 전역 스타일 임포트

export default function RootLayout({
  children, // 필수 prop: 모든 하위 페이지 및 중첩 레이아웃이 여기에 렌더링됩니다.
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <header style={{ backgroundColor: '#f0f0f0', padding: '10px' }}>
          <nav>
            <a href="/"></a> | <a href="/about">소개</a> | <a href="/dashboard">대시보드</a>
          </nav>
          <h1>나 혼자 Next.js</h1>
        </header>
        {children} {/* 여기에 페이지 또는 하위 레이아웃 콘텐츠가 들어옵니다 */}
        <footer style={{ backgroundColor: '#e0e0e0', padding: '10px', marginTop: '20px' }}>
          <p>&copy; 2024 나 혼자 Next.js. All rights reserved.</p>
        </footer>
      </body>
    </html>
  );
}

설명

  • children prop: 이 레이아웃 내부에 렌더링될 하위 콘텐츠(다른 레이아웃 또는 최종 page.tsx 파일)를 나타냅니다. 모든 레이아웃 컴포넌트는 이 children prop을 받아야 합니다.
  • <html>, <body> 태그: 최상위 레이아웃은 반드시 <html><body> 태그를 포함해야 합니다. 이는 웹 페이지의 기본 구조를 형성합니다.
  • 공통 UI 추가: 예시에서는 간단한 <header><footer>를 추가했습니다. 이 부분은 어떤 페이지로 이동하든 항상 동일하게 표시됩니다.

실습 위 코드를 src/app/layout.tsx 파일에 붙여넣고 개발 서버(npm run dev)를 확인해 보세요. 어떤 페이지로 이동하든 상단에 헤더와 하단에 푸터가 항상 표시되는 것을 확인할 수 있습니다.


중첩 레이아웃 (Nested Layouts) 사용하기

특정 라우트 세그먼트에만 적용되는 별도의 레이아웃을 정의할 수도 있습니다. 이것이 바로 중첩 레이아웃입니다. 예를 들어, 대시보드 페이지와 그 하위 페이지들(dashboard/settings, dashboard/profile 등)에는 공통된 사이드바가 필요할 수 있습니다.

  1. dashboard 폴더에 레이아웃 파일 추가: src/app/dashboard 폴더 안에 layout.tsx 파일을 생성합니다.

    my-next-app/
    └── src/
        └── app/
            ├── about/
            ├── dashboard/
            │   ├── layout.tsx  <- 여기에 새 파일 생성
            │   ├── page.tsx
            │   └── settings/
            ├── layout.tsx
            └── page.tsx
  2. src/app/dashboard/layout.tsx 파일 내용 작성

    src/app/dashboard/layout.tsx
    // src/app/dashboard/layout.tsx
    
    export default function DashboardLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <section style={{ display: 'flex', border: '1px solid #ccc', padding: '10px', marginTop: '10px' }}>
          <aside style={{ width: '200px', padding: '10px', backgroundColor: '#f9f9f9', borderRight: '1px solid #eee' }}>
            <h2>대시보드 메뉴</h2>
            <ul>
              <li><a href="/dashboard">대시보드 홈</a></li>
              <li><a href="/dashboard/settings">설정</a></li>
              {/* 추가 메뉴 아이템 */}
            </ul>
          </aside>
          <main style={{ flexGrow: 1, padding: '10px' }}>
            {children} {/* 대시보드 하위 페이지 콘텐츠가 여기에 렌더링됩니다 */}
          </main>
        </section>
      );
    }

설명

  • DashboardLayoutsection 태그로 전체를 감싸고, 왼쪽에 사이드바(aside)와 오른쪽에 메인 콘텐츠 영역(main)을 가집니다.
  • 이 레이아웃 역시 children prop을 받아, /dashboard 경로 또는 그 하위 경로의 page.tsx 콘텐츠가 main 태그 내부에 렌더링되도록 합니다.

실습 src/app/dashboard/layout.tsx 파일을 생성하고 위 코드를 붙여넣은 후, 개발 서버를 확인해 보세요.

  • http://localhost:3000/: 루트 레이아웃만 적용됩니다.
  • http://localhost:3000/about: 루트 레이아웃만 적용됩니다.
  • http://localhost:3000/dashboard: 루트 레이아웃 안에 DashboardLayout이 중첩되어 적용되고, 그 안에 /dashboardpage.tsx 내용이 렌더링됩니다. 즉, 헤더-사이드바-대시보드 콘텐츠-푸터 순서로 보일 것입니다.
  • http://localhost:3000/dashboard/settings: 마찬가지로 루트 레이아웃 안에 DashboardLayout이 중첩되어 적용되고, 그 안에 /dashboard/settingspage.tsx 내용이 렌더링됩니다.

레이아웃의 데이터 페칭

레이아웃은 기본적으로 서버 컴포넌트이므로, 서버에서 데이터를 미리 가져와 UI를 구성하는 것이 가능합니다. 이는 해당 레이아웃이 적용되는 모든 페이지에서 공통적으로 필요한 데이터를 효율적으로 로드하는 데 유용합니다.

src/app/dashboard/layout.tsx
// src/app/dashboard/layout.tsx (데이터 페칭 예시)
import { Suspense } from 'react'; // 로딩 상태 관리를 위한 Suspense 임포트

// 서버 컴포넌트에서 데이터 페칭
async function getSharedData() {
  // 실제 API 호출 로직
  const response = await fetch('https://api.example.com/shared-dashboard-info', {
    cache: 'no-store' // 캐시 사용 여부 설정 (필요에 따라)
  });
  if (!response.ok) {
    // 에러 처리
    throw new Error('데이터를 가져오지 못했습니다.');
  }
  return response.json();
}

export default async function DashboardLayout({ // async 키워드 추가
  children,
}: {
  children: React.ReactNode;
}) {
  const sharedData = await getSharedData(); // 데이터 호출 (서버에서 실행)

  return (
    <section style={{ display: 'flex', border: '1px solid #ccc', padding: '10px', marginTop: '10px' }}>
      <aside style={{ width: '200px', padding: '10px', backgroundColor: '#f9f9f9', borderRight: '1px solid #eee' }}>
        <h2>대시보드 메뉴</h2>
        <p>공유 데이터: {sharedData.message}</p> {/* 가져온 데이터 사용 */}
        <ul>
          <li><a href="/dashboard">대시보드 홈</a></li>
          <li><a href="/dashboard/settings">설정</a></li>
        </ul>
      </aside>
      <main style={{ flexGrow: 1, padding: '10px' }}>
        {/* Suspense를 사용하여 children 로딩 중 대체 UI 표시 가능 */}
        <Suspense fallback={<div>페이지 로딩 중...</div>}>
          {children}
        </Suspense>
      </main>
    </section>
  );
}

참고: Next.js 15에서는 fetch 함수가 자동으로 요청을 캐싱하는 기능이 포함되어 있습니다. 동일한 데이터가 여러 레이아웃이나 페이지에서 요청될 때, 한 번만 네트워크 요청을 보내도록 최적화됩니다.

레이아웃 컴포넌트를 사용하면 애플리케이션의 UI 구조를 체계적으로 관리하고, 코드의 재사용성을 높이며, 일관된 사용자 경험을 제공하는 데 큰 도움이 됩니다. 중첩 레이아웃을 통해 복잡한 UI도 효율적으로 구성할 수 있습니다.

이제 레이아웃 컴포넌트의 중요성과 활용법에 대해 충분히 이해하셨으리라 생각합니다.