app/layout.tsx는 Next.js 앱에서 필수인 파일입니다. 모든 페이지를 감싸며, <html>과 <body> 태그를 반드시 포함해야 합니다.
// app/layout.tsx - 루트 레이아웃
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang='ko'>
<body>
<header>My Blog</header>
<main>{children}</main>
<footer>Copyright 2026 My Blog</footer>
</body>
</html>
)
}
이 레이아웃은 앱의 모든 페이지에 적용됩니다. 헤더와 푸터는 어떤 페이지로 이동하든 항상 표시됩니다.
하위 폴더에 layout.tsx를 추가하면 자동으로 중첩됩니다:
app/
├── layout.tsx # 루트 레이아웃 (헤더, 푸터)
├── page.tsx # /
└── dashboard/
├── layout.tsx # 대시보드 레이아웃 (사이드바)
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<aside>사이드바</aside>
<section>{children}</section>
</div>
)
}
/dashboard/settings에 접근하면 렌더링 순서는 다음과 같습니다:
루트 레이아웃 (헤더, 푸터)
└── 대시보드 레이아웃 (사이드바)
└── 설정 페이지 (page.tsx)
레이아웃이 자동으로 중첩되어, 별도 설정 없이도 일관된 UI 구조를 만들 수 있습니다.
레이아웃은 하위 페이지가 전환되어도 리렌더링되지 않습니다. 예를 들어, 대시보드 레이아웃 사이드바의 스크롤 위치는 페이지 이동 시에도 유지가 됩니다. 또한, 입력 필드의 사용자값처럼, UI 상태(State)도 유지가 됩니다.
/dashboard → /dashboard/settings 이동 시:
✅ 대시보드 레이아웃: 리렌더링 없음 (상태 유지)
🔄 페이지 컴포넌트만 교체됨
template.tsx라는 별도 파일을 사용하면 페이지 전환 시마다 새로 마운트할 수도 있지만, 이는 공식 문서에서 자세히 확인하실 수 있습니다.
지금까지 배운 내용을 종합하여, 간단한 블로그 프로젝트를 만들어 보겠습니다. 프로젝트 생성은 세션 3 - 프로젝트 생성하기를 참고해 주세요.
app/
├── layout.tsx # 루트 레이아웃: 헤더(내비게이션) + 푸터
├── page.tsx # / (홈)
├── about/
│ └── page.tsx # /about (소개)
└── blog/
├── layout.tsx # 블로그 레이아웃: 사이드바(카테고리)
├── page.tsx # /blog (글 목록)
└── [slug]/
└── page.tsx # /blog/:slug (글 상세)
// app/layout.tsx - 루트 레이아웃
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
import Link from 'next/link';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: 'My Blog',
description: '나의 기술 블로그',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='ko'>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<header>
<nav>
<Link href='/'>홈</Link>
<Link href='/blog'>글 목록</Link>
<Link href='/about'>소개</Link>
</nav>
</header>
<main>{children}</main>
<footer>Copyright 2026 My Blog</footer>
</body>
</html>
);
}
// app/page.tsx - 홈 페이지
export default function Home() {
return (
<div>
<h1>환영합니다!</h1>
<p>프론트엔드 개발을 기록하는 블로그입니다.</p>
</div>
);
}
// app/about/page.tsx - 소개 페이지
export default function About() {
return (
<div>
<h1>소개</h1>
<p>안녕하세요. 저만의 개발 블로그입니다.</p>
</div>
);
}
// app/blog/layout.tsx - 블로그 레이아웃
'use client';
import { useState } from 'react';
export default function BlogLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [isOpen, setIsOpen] = useState(true);
return (
<>
<aside>
<button onClick={() => setIsOpen(!isOpen)}>
카테고리 {isOpen ? '닫기' : '열기'}
</button>
{isOpen && (
<ul>
<li>React</li>
<li>Next.js</li>
<li>TypeScript</li>
</ul>
)}
</aside>
{children}
</>
);
}
// app/blog/page.tsx - 글 목록 페이지
import Link from 'next/link';
const posts = [
{ id: 1, slug: 'nextjs-routing', title: 'Next.js 라우팅 이해하기' },
{ id: 2, slug: 'react-server-components', title: 'React 서버 컴포넌트란?' },
];
export default function Blog() {
return (
<div>
<h1>글 목록</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}
// app/blog/[slug]/page.tsx - 글 상세 페이지
export default async function Post({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return (
<div>
<h1>{slug}</h1>
<p>이 글의 내용이 여기에 표시됩니다.</p>
</div>
);
}
사이드바의 카테고리를 접은 상태에서 블로그 글을 클릭하면 어떻게 될까요? 레이아웃은 리렌더링되지 않으므로, 카테고리가 접힌 상태가 그대로 유지됩니다. 'use client'는 다음 챕터에서 자세히 배우지만, 여기서는 상태(useState)를 사용하기 위해 필요하다는 것만 알아두세요.
참고로 레이아웃을 클라이언트 컴포넌트로 만드는 것이 항상 바람직한 것은 아닙니다. 여기서는 상태 유지를 직접 확인하기 위한 예시이며, 서버/클라이언트 컴포넌트의 선택 기준은 다음 챕터에서 다룹니다.
결과적으로 /blog/nextjs-routing에 접근하면 다음과 같이 렌더링됩니다:
app/layout.tsx) - "홈 · 글 목록 · 소개" 내비게이션이 모든 페이지에 공통 적용됩니다.app/blog/layout.tsx) - 카테고리 목록은 블로그 하위 경로에서만 표시되며, 페이지를 이동해도 레이아웃이 리렌더링되지 않아, 열기/닫기 상태가 유지됩니다.app/blog/[slug]/page.tsx) - URL의 slug 값(nextjs-routing)이 동적으로 주입되어 해당 글의 내용을 렌더링합니다.app/layout.tsx) - 루트 레이아웃에 속하므로 모든 페이지 하단에 표시됩니다.지금까지 배운 내용을 정리해 보겠습니다.
클라이언트에서 모든 렌더링을 처리하는 방식의 장단점을 살펴보았습니다.
Next.js가 CSR의 한계를 어떻게 보완하는지 이해하고, create-next-app으로 프로젝트를 시작했습니다.
폴더와 파일만으로 라우트를 정의하는 방식을 배웠습니다.
공통 UI를 레이아웃으로 분리하고, 중첩 레이아웃으로 확장하는 방법을 배웠습니다.
다음 챕터에서는 서버 컴포넌트와 클라이언트 컴포넌트를 학습합니다.