본문으로 건너뛰기

안동민 개발노트

본문 시작

레이아웃 컴포넌트

RootLayout과 중첩 layout.tsx로 헤더·사이드바 같은 공통 UI를 유지하고 페이지 콘텐츠와 책임을 나눕니다.

Next.js App Router의 주요 기능 중 하나가 레이아웃(Layouts)입니다.

레이아웃은 여러 페이지가 공유하는 UI를 한 곳에서 정의하게 해 코드 중복을 줄이고 일관된 사용자 경험을 유지하도록 돕습니다.

이번 절에서는 layout.tsx 개념을 더 깊이 이해하고 실제 프로젝트 적용 방법까지 살펴보겠습니다.

아래 표는 layout.tsx가 어떤 UI를 유지하고, page.tsx와 어떻게 역할을 나누는지 먼저 정리합니다.

layout.tsx는 공유 UI를 감싸고 page.tsx는 URL별 고유 화면을 채운다

레이아웃은 페이지 위아래에 반복되는 구조를 한 곳에서 유지하고, children 자리에 현재 페이지를 끼워 넣는다.

요소맡는 역할유지되는 범위확인할 규칙
RootLayouthtml/body와 전체 공통 UI앱의 모든 페이지src/app/layout.tsx에 html/body 포함
Nested Layout특정 세그먼트 공통 UI해당 폴더와 하위 폴더children을 렌더링해야 하위 page가 보임
page.tsxURL별 고유 화면해당 경로에서만 교체폴더 안에 page.tsx가 있어야 URL 접근
children하위 layout 또는 page 자리레이아웃 안쪽 영역누락하면 페이지 내용이 사라짐
공유 UIheader, footer, sidebar라우트 이동 중 계속 보임중복 코드를 layout으로 올림

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

레이아웃은 특정 라우트 세그먼트와 그 하위 라우트에서 공유되는 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/layout.tsx를 추가했을 때 루트 레이아웃과 중첩 레이아웃이 어떻게 쌓이는지 정리한 것입니다.

중첩 레이아웃은 root layout 안에 세그먼트 layout을 다시 끼운다

dashboard/layout.tsx를 만들면 /dashboard 아래 페이지는 root layout과 dashboard layout을 함께 통과한다.

경로적용되는 layoutchildren 자리에 들어가는 것화면 결과
/src/app/layout.tsxsrc/app/page.tsx헤더, 홈 화면, 푸터
/aboutsrc/app/layout.tsxsrc/app/about/page.tsx헤더, 소개 화면, 푸터
/dashboardroot + dashboard/layout.tsxdashboard/page.tsx헤더, 사이드바, 대시보드 홈, 푸터
/dashboard/settingsroot + dashboard/layout.tsxsettings/page.tsx헤더, 사이드바, 설정 화면, 푸터
layout 위치 변경상위 폴더로 올리면 범위 확대더 많은 page가 영향 받음의도하지 않은 공유 UI를 주의

특정 라우트 세그먼트에만 적용되는 별도의 레이아웃을 정의할 수도 있습니다.

이것이 바로 중첩 레이아웃입니다.

예를 들어, 대시보드 페이지와 그 하위 페이지들(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 내용이 렌더링됩니다.

라우트 이동 시 어떤 컴포넌트가 유지되고 어떤 컴포넌트가 교체되는지는 아래 흐름으로 정리할 수 있습니다.

라우트 이동 시 layout은 유지되고 page만 바뀌는 범위를 먼저 본다

같은 layout 아래에서 이동하면 공유 UI와 그 안의 클라이언트 상태가 유지될 수 있다.

이동유지되는 UI교체되는 UI확인할 신호
/dashboard → /dashboard/settingsroot layout, dashboard layoutpage.tsx 영역사이드바는 유지되고 본문만 바뀜
/about → /dashboardroot layoutabout page에서 dashboard layout+page로 교체대시보드 사이드바가 새로 나타남
/dashboard/settings → /root layoutdashboard layout과 settings page 제거사이드바가 사라짐
같은 layout 안 이동layout 내부 상태children 영역검색어, 펼침 메뉴 같은 상태 유지 가능
template 사용 시layout보다 자주 재생성template 아래 영역이동마다 초기화가 필요할 때 선택

레이아웃의 데이터 페칭

레이아웃은 기본적으로 서버 컴포넌트이므로, 서버에서 데이터를 미리 가져와 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 15 이후 fetch 응답은 기본적으로 요청 사이에 캐시되지 않습니다.

다만 같은 서버 렌더링 작업 안에서 URL과 옵션이 같은 fetch는 중복 요청이 메모이제이션될 수 있습니다.

요청 사이에서도 응답을 재사용하려면 cache: 'force-cache'나 재검증 정책을 명시합니다.

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

레이아웃 데이터는 공유 범위가 넓을 때, 페이지 데이터는 화면별로 다를 때 둔다

layout은 서버 컴포넌트가 기본이라 공통 데이터를 서버에서 가져오기에 좋지만, 너무 넓은 범위에 두면 불필요한 의존이 생긴다.

데이터두기 좋은 위치이유주의 신호
사용자 이름/권한root 또는 dashboard layout여러 하위 페이지가 공통 사용모든 페이지가 기다리는 병목
대시보드 메뉴dashboard/layout.tsx대시보드 하위에서 반복다른 섹션까지 노출되면 위치가 높음
설정 상세 값settings/page.tsx해당 화면에서만 필요layout에 두면 다른 페이지도 영향
로딩 UIloading.tsx 또는 Suspense대기 위치를 세그먼트별로 제어전체 화면이 과하게 멈춤
클라이언트 상태작은 client component이벤트와 상태가 필요한 부분만 hydratelayout 전체를 use client로 올림

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

layout, template, page는 유지 범위와 재생성 시점으로 선택한다

세 파일은 모두 UI를 만들지만 유지되는 시간과 맡는 책임이 다르다.

파일언제 쓰나유지/교체 방식대표 실수
layout.tsx공유 UI와 상태 유지가 필요할 때같은 세그먼트 이동 중 유지children 누락
template.tsx이동마다 새 인스턴스가 필요할 때라우트 전환 때 재생성layout과 같은 용도로 남용
page.tsxURL별 고유 화면이 필요할 때해당 URL에서만 렌더공통 UI를 page마다 복사
loading.tsx데이터 대기 UI가 필요할 때세그먼트 단위 대체 UI너무 넓은 구간에 둬 전체가 가려짐
error.tsx세그먼트 오류 복구가 필요할 때가까운 오류 경계에서 처리use client 누락

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

중첩 레이아웃을 통해 복잡한 UI도 효율적으로 구성할 수 있습니다.

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

아래 표는 레이아웃 컴포넌트를 프로젝트에 넣을 때 결정해야 할 파일 위치와 런타임 경계를 정리합니다.

레이아웃 파일 위치는 적용 범위와 런타임 경계를 동시에 결정한다

파일을 한 단계 위로 올리면 더 많은 페이지가 영향을 받고, use client를 위에 두면 더 넓은 영역이 브라우저 번들로 간다.

결정할 것권장 위치좋은 신호나쁜 신호
전체 공통 UIsrc/app/layout.tsx모든 페이지에 필요한 header/footer특정 섹션 메뉴까지 root에 둠
섹션 공통 UIsrc/app/dashboard/layout.tsx대시보드 안에서만 sidebar 유지about에도 대시보드 메뉴가 보임
상호작용 메뉴layout 안의 작은 client component상태 필요한 부분만 use clientlayout.tsx 전체를 client로 전환
페이지 고유 데이터각 page.tsx화면별 fetch 범위가 작음layout fetch가 모든 하위 화면을 지연
오류/로딩 처리가장 가까운 세그먼트 파일문제 영역만 대체 UI로 감쌈앱 전체가 오류 화면으로 덮임

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

RootLayout과 중첩 레이아웃은 공유 범위와 데이터 책임이 다르다

둘 다 children을 감싸지만, 어디까지 적용되는지와 어떤 데이터를 가져올지가 다르다.

비교 항목RootLayoutNested Layout판단 기준
적용 범위앱 전체특정 폴더와 하위 라우트모든 페이지에 필요한가
필수 태그html, body 필요일반 컴포넌트 구조root layout만 문서 껍데기 담당
공유 UI전역 header, font, provider섹션 nav, sidebar, tab섹션 밖에서도 보여야 하는가
데이터전역 설정, 세션 기본값섹션 공통 데이터하위 페이지가 모두 쓰는가
위험너무 많은 책임 집중범위를 잘못 잡아 UI 누수필요한 최소 폴더에 둔다