안동민 개발노트 아이콘

안동민 개발노트

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 등)에는 공통된 사이드바가 필요할 수 있습니다.

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

layout.tsx
page.tsx
layout.tsx
page.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를 구성하는 것이 가능합니다. 이는 해당 레이아웃이 적용되는 모든 페이지에서 공통적으로 필요한 데이터를 효율적으로 로드하는 데 유용합니다.

아래 데이터 페칭 예시는 http://localhost:4000 로컬 Mock API가 실행 중인 상황을 전제로 합니다.

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

// 서버 컴포넌트에서 데이터 페칭
async function getSharedData() {
  // 실제 API 호출 로직
  const response = await fetch('http://localhost:4000/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 16에서는 fetch 함수가 자동으로 요청을 캐싱하는 기능이 포함되어 있습니다. 동일한 데이터가 여러 레이아웃이나 페이지에서 요청될 때, 한 번만 네트워크 요청을 보내도록 최적화됩니다.

레이아웃에 둘 데이터와 페이지에 둘 데이터를 구분할 때는 사용 범위와 상태 유지 여부를 함께 보는 것이 좋습니다.

마지막으로 layout.tsx, template.tsx, page.tsx를 선택하는 기준을 한 번에 비교해 보겠습니다.

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

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

이 다이어그램은 레이아웃 컴포넌트를 사용할 때 Next.js 프로젝트에 넣을 때 결정해야 할 파일 위치와 런타임 경계를 정리합니다.

마지막으로 RootLayout과 중첩 레이아웃이 어떤 UI를 유지하고 어디서 데이터를 가져오는지 비교합니다.