안동민 개발노트

안동민 개발노트

페이지 컴포넌트 작성레이아웃 컴포넌트 생성 및 중첩템플릿 컴포넌트 활용로딩 UI 구현
본문 시작
  1. 홈
  2. 문서
  3. Next.js
  4. 5장 : 페이지 및 레이아웃 컴포넌트
  5. 레이아웃 컴포넌트 생성 및 중첩
  1. Next.js
  2. 레이아웃 컴포넌트 생성 및 중첩

레이아웃 컴포넌트 생성 및 중첩

children을 받는 레이아웃을 여러 세그먼트에 중첩해 유지되는 UI 범위와 데이터 공유 경계를 설계합니다.

Next.js App Router에서 레이아웃 컴포넌트는 단순히 UI를 공유하는 것을 넘어, 애플리케이션의 구조와 성능을 최적화하는 데 핵심적인 역할을 합니다.

3장에서 레이아웃의 기본 개념과 최상위 레이아웃, 중첩 레이아웃 사용법을 간략히 살펴보았습니다.

이번 절에서는 레이아웃 컴포넌트를 더 깊이 이해하고, 실제 프로젝트에서 효율적으로 생성하고 중첩하는 고급 기법을 다룹니다.


레이아웃 컴포넌트의 본질과 역할

레이아웃 컴포넌트는 app 디렉터리 내의 특정 라우트 세그먼트 폴더 안에 위치한 layout.tsx 파일입니다.

이 파일은 해당 라우트와 그 모든 하위 라우트에 적용되는 공통 UI를 정의합니다.

레이아웃의 핵심 역할
  • UI 재사용 및 일관성 유지: 헤더, 푸터, 사이드바, 내비게이션 바 등 웹사이트의 여러 부분에서 반복되는 UI 요소를 한 곳에서 관리하여 코드 중복을 방지하고 디자인 일관성을 유지합니다.
  • 상태 유지: 공유 레이아웃 아래의 클라이언트 이동에서는 레이아웃을 재사용해 그 안의 Client Component 상태를 유지합니다. 새로고침이나 해당 레이아웃을 벗어나는 이동까지 같은 수명을 보장하지는 않습니다.
  • 서버 컴포넌트 기본값: layout.tsx 파일은 기본적으로 서버 컴포넌트로 동작합니다. 따라서 레이아웃에서 데이터를 페칭하거나, 서버 전용 로직을 안전하게 실행할 수 있습니다.
  • 성능 최적화: 서버 컴포넌트의 코드를 클라이언트 번들에 포함하지 않고 공통 구조를 재사용할 수 있습니다. 데이터 캐시 적용 여부와 실제 속도는 조회 방식·설정에 따라 판단합니다.

하위 라우트의 UI를 표시하려면 children prop을 받아 원하는 위치에 렌더링해야 합니다.

이 children prop을 통해 하위 레이아웃 또는 최종 페이지 컴포넌트의 콘텐츠가 렌더링됩니다.

// 모든 레이아웃 컴포넌트의 기본 형태
export default function MyLayout({ children }: { children: React.ReactNode }) {
  return (
    <>
      {/* 레이아웃에 고유한 공통 UI */}
      <nav>공통 내비게이션</nav>
      {children} {/* 여기에 하위 콘텐츠가 렌더링됩니다 */}
      <footer>공통 푸터</footer>
    </>
  );
}

레이아웃 중첩의 작동 방식

App Router에서 레이아웃은 폴더 구조에 따라 자동으로 중첩됩니다.

각 라우트 세그먼트(폴더)에 layout.tsx 파일이 있으면, 해당 폴더의 레이아웃은 그 부모 폴더의 레이아웃 안에 중첩되어 렌더링됩니다.

예시: 중첩 레이아웃 계층
layout.tsx (1) RootLayout
layout.tsx (2) DashboardLayout
page.tsx (3) DashboardPage
layout.tsx (4) SettingsLayout (선택 사항)
page.tsx (5) SettingsPage
page.tsx

위 구조에서 /dashboard/settings 경로로 접근하면, 다음과 같은 순서로 레이아웃이 중첩되어 최종 페이지가 렌더링됩니다.

RootLayout: 최상위 HTML 구조(<html>, <body>)를 제공하고, 전역 스타일 등을 포함합니다.

이 레이아웃의 children으로 다음 레이아웃이 들어갑니다.

DashboardLayout: 대시보드 섹션에 특화된 UI(예: 사이드바)를 제공합니다.

이 레이아웃의 children으로 다음 레이아웃/페이지가 들어갑니다.

SettingsLayout (선택 사항): 만약 settings 폴더에 레이아웃이 있다면, 이 레이아웃이 추가로 중첩되어 특정 설정 페이지에만 적용되는 UI를 제공합니다.

이 레이아웃의 children으로 최종 페이지가 들어갑니다.

SettingsPage: 마지막으로 settings 폴더의 page.tsx가 렌더링되어 최종 콘텐츠를 표시합니다.

이러한 계층적 중첩은 애플리케이션의 복잡성에 따라 유연하게 UI를 구성할 수 있게 해줍니다.


레이아웃 컴포넌트 생성 실습

이전에 만들었던 대시보드 예제를 바탕으로, 특정 하위 페이지에만 적용되는 중첩 레이아웃을 추가해 보겠습니다. 라우트 그룹 실습에서 폴더를 옮겼다면 아래 src/app/dashboard 경로를 기존 src/app/(dashboard)/dashboard에 적용하고, 같은 URL의 폴더를 중복 생성하지 않습니다.

목표: /dashboard/profile 경로에 사용자 프로필을 표시하는 페이지를 만들고, 이 페이지에만 적용되는 특정 레이아웃을 추가하여 사이드 메뉴를 더 세분화합니다.

src/app/dashboard/profile 폴더 생성: src/app/dashboard 폴더 안에 profile이라는 새 폴더를 만듭니다.

layout.tsx
page.tsx
...
...

src/app/dashboard/profile/layout.tsx 파일 생성: profile 폴더 안에 layout.tsx 파일을 생성하고 다음 내용을 작성합니다.

src/app/dashboard/profile/layout.tsx
import Link from 'next/link';

export default function ProfileLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div style={{ display: 'flex', gap: '20px', padding: '15px', border: '1px dashed orange', borderRadius: '5px', marginTop: '15px' }}>
      <aside style={{ width: '150px', backgroundColor: '#fffbe6', padding: '10px', borderRadius: '5px' }}>
        <h4>프로필 메뉴</h4>
        <ul style={{ listStyle: 'none', padding: 0 }}>
          <li style={{ marginBottom: '5px' }}>
            <Link href="/dashboard/profile">기본 정보</Link>
          </li>
          <li style={{ marginBottom: '5px' }}>
            <Link href="/dashboard/profile/edit">프로필 편집</Link>
          </li>
          <li style={{ marginBottom: '5px' }}>
            <Link href="/dashboard/profile/security">보안 설정</Link>
          </li>
        </ul>
      </aside>
      <section style={{ flexGrow: 1, backgroundColor: '#fff', padding: '10px' }}>
        {children} {/* 프로필 관련 페이지 콘텐츠가 여기에 들어옵니다 */}
      </section>
    </div>
  );
}
설명
  • 이 ProfileLayout은 주황색 점선 테두리로 표시되어 시각적으로 구분됩니다.
  • ProfileLayout은 DashboardLayout 안에 중첩되어 렌더링될 것입니다. 즉, RootLayout > DashboardLayout > ProfileLayout > page.tsx 순으로 중첩됩니다.

src/app/dashboard/profile/page.tsx 파일 생성: profile 폴더 안에 page.tsx 파일을 생성하고 기본 프로필 정보를 표시하는 내용을 작성합니다.

src/app/dashboard/profile/page.tsx
export default function ProfileHomePage() {
  return (
    <div>
      <h3>내 프로필</h3>
      <p>이곳은 사용자 프로필의 기본 정보를 표시하는 페이지입니다.</p>
      <ul>
        <li>이름: 홍길동</li>
        <li>이메일: [email protected]</li>
      </ul>
    </div>
  );
}

src/app/dashboard/profile/edit/page.tsx 및 src/app/dashboard/profile/security/page.tsx 생성: src/app/dashboard/profile 안에 각각 edit 폴더와 security 폴더를 만들고, 그 안에 page.tsx 파일을 생성합니다.

  • src/app/dashboard/profile/edit/page.tsx
    src/app/dashboard/profile/edit/page.tsx
    export default function ProfileEditPage() {
      return (
        <div>
          <h3>프로필 편집</h3>
          <p>여기서 사용자 정보를 편집할 수 있습니다.</p>
          {/* 실제로는 폼 컴포넌트가 들어갈 자리 */}
        </div>
      );
    }
  • src/app/dashboard/profile/security/page.tsx
    src/app/dashboard/profile/security/page.tsx
    // src/app/dashboard/profile/security/page.tsx
    export default function ProfileSecurityPage() {
      return (
        <div>
          <h3>보안 설정</h3>
          <p>비밀번호 변경, 2단계 인증 등의 보안 설정을 할 수 있습니다.</p>
        </div>
      );
    }

DashboardLayout에 프로필 링크 추가 (선택 사항): src/app/dashboard/layout.tsx 파일의 사이드바에 프로필 링크를 추가하여 접근성을 높일 수 있습니다.

src/app/dashboard/layout.tsx (일부)
// ...
        <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/profile">프로필</Link> {/* 새 링크 추가 */}
          </li>
          <li style={{ marginBottom: '10px' }}>
            <Link href="/dashboard/analytics">분석</Link>
          </li>
          <li style={{ marginBottom: '10px' }}>
            <Link href="/dashboard/settings">설정</Link>
          </li>
        </ul>
// ...

실습 확인: 개발 서버(npm run dev)를 실행한 후, 다음 URL들을 방문하며 레이아웃 중첩을 확인해 보세요.

  • http://localhost:3000/dashboard (루트 + 대시보드 레이아웃)
  • http://localhost:3000/dashboard/profile (루트 + 대시보드 + 프로필 레이아웃 + 프로필 홈 페이지)
  • http://localhost:3000/dashboard/profile/edit (루트 + 대시보드 + 프로필 레이아웃 + 프로필 편집 페이지)

프로필 관련 페이지들에서만 ProfileLayout의 주황색 점선 테두리와 내부 메뉴가 보이는 것을 확인할 수 있습니다.

/dashboard/profile/edit에서는 세 레이아웃과 편집 페이지가 중첩됩니다. 대시보드 홈이나 프로필 홈의 page.tsx가 함께 렌더링되는 구조는 아닙니다.

프로필 편집 화면의 네 계층

프로필 편집 화면의 네 계층

/dashboard/profile/edit의 포함 관계RootLayout과 DashboardLayout 안에 ProfileLayout이 있고 가장 안쪽은 ProfileEditPage이다. DashboardPage와 ProfileHomePage는 이 경로에서 동시에 렌더되지 않는다.RootLayout전역 헤더 · 푸터DashboardLayout대시보드 메뉴ProfileLayout프로필 메뉴ProfileEditPageprofile/edit/page.tsx
/dashboard/profile/edit의 포함 관계세 공통 레이아웃 안에 편집 페이지 하나가 들어간다.RootLayout전역 헤더 · 푸터DashboardLayout대시보드 메뉴ProfileLayout프로필 메뉴ProfileEditPageedit/page.tsx

레이아웃에서 데이터 공유

서버 레이아웃은 자신이 표시하는 공통 UI에 필요한 데이터를 조회할 수 있습니다. 아래 예제는 500ms 지연을 넣은 함수에서 고정 사용자 정보를 받아 사이드바에 이름을 표시합니다. 실제 DB·API 호출은 없습니다.

src/app/dashboard/layout.tsx (데이터 페칭 예시)
import Link from 'next/link';

// 데이터 페칭 함수 (서버에서 실행)
async function getUserInfo() {
  // 실제로는 DB에서 사용자 정보를 가져오거나 API 호출
  // 예시를 위해 딜레이 추가
  await new Promise(resolve => setTimeout(resolve, 500));
  return { username: '김넥스트', email: '[email protected]' };
}

export default async function DashboardLayout({ // async 키워드 추가
  children,
}: {
  children: React.ReactNode;
}) {
  const userInfo = await getUserInfo(); // 서버에서 사용자 정보 페칭

  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>
        <p>환영합니다, {userInfo.username}님!</p> {/* 가져온 데이터 사용 */}
        <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/profile">프로필</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>
  );
}

userInfo는 레이아웃 자체에만 사용되며 children에 자동으로 전달되지 않습니다. 하위 페이지에서도 필요하면 데이터를 전달하는 별도 경계를 설계하거나 그 페이지에서 조회합니다.

현재 getUserInfo는 일반 비동기 함수이므로 다시 호출하면 지연 코드도 다시 실행됩니다. 공유 레이아웃의 클라이언트 이동 중 재사용과 데이터 함수의 캐시는 서로 다른 개념입니다. 같은 서버 렌더에서 동일한 fetch 요청의 중복 제거를 이용하거나 일반 조회 함수를 React cache로 감싸는 방식은 데이터 페칭 장에서 다룹니다.

페이지 컴포넌트 작성

이전 페이지

템플릿 컴포넌트 활용

다음 페이지

이 페이지의 목차

레이아웃 컴포넌트의 본질과 역할레이아웃 중첩의 작동 방식레이아웃 컴포넌트 생성 실습레이아웃에서 데이터 공유