Ch 02. 모든 페이지를 감싸는 틀 — layout.tsx
액자와 그림
미술관을 생각해봅니다. 그림마다 다른 이야기를 담고 있지만, 모든 그림은 같은 형태의 액자 안에 걸려 있습니다. 액자는 바뀌지 않습니다. 그림만 교체됩니다.
layout.tsx가 바로 그 액자입니다. 페이지는 그림입니다. 사용자가 /about에서 /posts로 이동해도 헤더와 푸터는 그대로입니다. 가운데 내용만 바뀝니다.
layout.tsx의 역할
layout.tsx는 page.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>
);
}
다음 챕터에서는
다음 챕터에서는 레이아웃이 여러 겹으로 중첩되는 것을 살펴봅니다. 대시보드처럼 특정 섹션에만 사이드바가 있는 경우, 레이아웃을 어떻게 계층적으로 구성하는지 알아봅니다.