iBetter Books
수정

Ch 02. 모든 페이지를 감싸는 틀 — layout.tsx

액자와 그림

미술관을 생각해봅니다. 그림마다 다른 이야기를 담고 있지만, 모든 그림은 같은 형태의 액자 안에 걸려 있습니다. 액자는 바뀌지 않습니다. 그림만 교체됩니다.

layout.tsx가 바로 그 액자입니다. 페이지는 그림입니다. 사용자가 /about에서 /posts로 이동해도 헤더와 푸터는 그대로입니다. 가운데 내용만 바뀝니다.

layout.tsx의 역할

layout.tsxpage.tsx와 함께 존재하는 특별한 파일입니다. 같은 폴더 안의 page.tsx와, 하위 폴더의 모든 페이지를 감쌉니다.

가장 중요한 것은 app/layout.tsx입니다. 이것이 루트 레이아웃이고, 애플리케이션의 모든 페이지를 감쌉니다. HTML 문서의 <html><body> 태그는 반드시 여기서 선언해야 합니다.

// 파일: app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Blog',
  description: '소설처럼 읽는 Next.js 블로그',
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <header>
          <nav>
            <a href="/">홈</a>
            <a href="/about">소개</a>
            <a href="/posts">블로그</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer>
          <p>© 2025 My Blog. All rights reserved.</p>
        </footer>
      </body>
    </html>
  );
}
```text

### `children` prop의 의미

`children`은 현재 레이아웃 안에서 렌더링될 페이지 또는 하위 레이아웃입니다. 사용자가 `/about`에 접속하면 `children`에는 `app/about/page.tsx`의 내용이 들어옵니다. `/posts`에 접속하면 `app/posts/page.tsx`의 내용이 들어옵니다.

레이아웃은 고정되어 있고, `children`만 바뀝니다. 이 덕분에 페이지를 이동해도 헤더와 푸터가 깜빡이거나 다시 마운트되지 않습니다.

### `metadata`로 타이틀과 설명 설정하기

`metadata`를 export하면 페이지의 `<title>`, `<meta description>` 등을 설정할 수 있습니다.

루트 레이아웃에서 기본값을 설정하고, 각 페이지에서 덮어쓸 수 있습니다.

```tsx
// 파일: app/posts/page.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: '블로그 목록 | My Blog',
  description: '최신 블로그 게시글을 확인하세요.',
};

export default function PostsPage() {
  return (
    <div>
      <h1>블로그</h1>
    </div>
  );
}
```text

동적으로 타이틀을 생성하고 싶다면 `generateMetadata` 함수를 사용합니다.

```tsx
// 파일: app/posts/[slug]/page.tsx
import type { Metadata } from 'next';

interface Props {
  params: Promise<{ slug: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>
    r.json()
  );

  return {
    title: `${post.title} | My Blog`,
    description: post.excerpt,
  };
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>
    r.json()
  );

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

다음 챕터에서는

다음 챕터에서는 레이아웃이 여러 겹으로 중첩되는 것을 살펴봅니다. 대시보드처럼 특정 섹션에만 사이드바가 있는 경우, 레이아웃을 어떻게 계층적으로 구성하는지 알아봅니다.