Next.js는 파일 시스템 기반 라우팅(file-system based routing)을 사용해요. 무슨 말이냐고요? 우리가 흔히 윈도우나 맥에서 폴더와 파일을 만들듯이, 코드상에서 폴더와 파일을 만드는 것만으로도 웹사이트의 접속 경로(URL 라우트)를 정의할 수 있다는 뜻이에요. 이 페이지에서는 레이아웃과 페이지를 어떻게 만들고, 또 그것들을 서로 어떻게 연결하는지 차근차근 안내해 드릴게요.
💡 강사의 부연 설명:
과거의 React(예: React Router)에서는 라우팅을 위해 별도의 라우터 설정 파일에<Route path="/blog" component={Blog} />처럼 일일이 코드를 작성해야 했어요. 하지만 Next.js의 App 라우터에서는 그럴 필요가 없습니다! 정해진 규칙대로 폴더와 파일만 만들면 알아서 주소가 연결된답니다.
페이지(page)는 특정 경로(route)에 접속했을 때 화면에 그려지는 UI를 말해요. 페이지를 만들려면 app 디렉토리(폴더) 안에 page 파일을 추가하고, React 컴포넌트를 default export로 내보내면 됩니다.
예를 들어, 웹사이트의 가장 첫 메인 화면인 인덱스 페이지(/)를 만들려면 다음과 같이 작성하면 돼요.

//filename="app/page.tsx" switcher
export default function Page() {
return <h1>Hello Next.js!</h1>
}
//filename="app/page.js" switcher
export default function Page() {
return <h1>Hello Next.js!</h1>
}
💡 강사의 꿀팁:
Next.js에서는page.tsx(혹은page.js)라는 이름의 파일만 '진짜 웹페이지'로 인식해서 URL 주소를 만들어줍니다. 즉,app폴더 안에components.tsx라는 파일을 만들어도 그건 그저 컴포넌트일 뿐, 사용자가주소.com/components로 접속할 수는 없어요. 오직page라는 이름만 특별하게 취급받습니다!
레이아웃은 여러 페이지 간에 공유되는 UI를 뜻해요. 사용자가 이 페이지 저 페이지로 이동(네비게이션)할 때, 레이아웃은 자신의 상태(state)를 그대로 유지하고, 상호작용이 가능한 상태로 남아있으며, 다시 렌더링(rerender)되지 않는다는 아주 강력한 특징이 있습니다.
레이아웃을 정의하려면 layout 파일에서 React 컴포넌트를 default export로 내보내면 됩니다. 이때 컴포넌트는 반드시 children이라는 prop을 받아야 해요. 이 children 자리에 바로 우리가 만든 페이지나 또 다른 중첩 레이아웃이 쏙 들어가게 됩니다.
예를 들어, 메인 인덱스 페이지를 자식으로 품는 레이아웃을 만들고 싶다면 app 디렉토리 안에 layout 파일을 이렇게 추가하세요.

//filename="app/layout.tsx" switcher
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{/* Layout UI */}
{/* Place children where you want to render a page or nested layout */}
<main>{children}</main>
</body>
</html>
)
}
//filename="app/layout.js" switcher
export default function DashboardLayout({ children }) {
return (
<html lang="en">
<body>
{/* Layout UI */}
{/* Place children where you want to render a page or nested layout */}
<main>{children}</main>
</body>
</html>
)
}
위에서 만든 레이아웃은 app 디렉토리의 가장 최상단 루트(root)에 정의되어 있기 때문에 루트 레이아웃 (root layout)이라고 불러요. 루트 레이아웃은 애플리케이션에 반드시 있어야 하며(필수), html과 body 태그를 무조건 포함하고 있어야 합니다.
💡 강사의 부연 설명:
모든 웹사이트에는 공통된 헤더(네비게이션 바)나 푸터(바닥글)가 있죠? 이걸 페이지마다 복사해서 붙여넣으면 코드가 너무 지저분해집니다. 이럴 때 레이아웃을 써서 공통 뼈대를 잡아두면, 안쪽에 있는children(즉 내용물)만 쏙쏙 바뀌게 되는 원리예요. 성능 최적화의 핵심입니다!
중첩 라우트(nested route)란 여러 개의 URL 조각(segment)들로 이루어진 경로를 말해요. 예를 들어 /blog/[slug]라는 경로는 다음 세 개의 조각으로 나눌 수 있어요.
/ (루트 조각 - Root Segment)blog (조각 - Segment)[slug] (리프 조각, 즉 나뭇잎처럼 가장 끝에 있는 조각 - Leaf Segment)Next.js에서는 다음과 같은 규칙을 따릅니다:
page나 layout)은 해당 조각에서 화면에 보여줄 UI를 만드는 데 사용돼요.중첩 라우트를 만들려면 폴더 안에 또 폴더를 넣어서(중첩해서) 만들면 됩니다. 예를 들어 /blog라는 경로를 추가하고 싶다면, app 디렉토리 안에 blog라는 폴더를 만드세요. 그런 다음 누구나 /blog 주소로 접속해서 화면을 볼 수 있도록 그 안에 page.tsx 파일을 추가하면 끝입니다!

//filename="app/blog/page.tsx" switcher
// Dummy imports
import { getPosts } from '@/lib/posts'
import { Post } from '@/ui/post'
export default async function Page() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</ul>
)
}
//filename="app/blog/[slug]/page.js" switcher
// Dummy imports
import { getPosts } from '@/lib/posts'
import { Post } from '@/ui/post'
export default async function Page() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</ul>
)
}
계속해서 폴더를 중첩해 나가며 더 깊은 중첩 라우트를 만들 수 있어요. 예를 들어 특정 블로그 게시글 하나만을 위한 경로를 만들고 싶다면, blog 폴더 안에 새롭게 [slug]라는 폴더를 만들고 그 안에 page 파일을 추가하면 됩니다.

function generateStaticParams() {}
export default function Page() {
return <h1>Hello, Blog Post Page!</h1>
}
function generateStaticParams() {}
export default function Page() {
return <h1>Hello, Blog Post Page!</h1>
}
이렇게 폴더 이름을 대괄호로 감싸면(예: [slug]) 동적 라우트 조각(dynamic route segment)이 만들어져요. 이건 블로그 게시글이나 상품 상세 페이지처럼, 외부 데이터에 따라 여러 개의 페이지를 동적으로 찍어내야 할 때 사용하는 아주 중요한 문법입니다.
폴더 계층 구조 안에서 레이아웃들 역시 기본적으로 중첩됩니다. 즉, 상위 레이아웃이 하위(자식) 레이아웃을 자신의 children prop을 통해 감싸게 되는 구조죠. 특정 라우트 조각(폴더) 안에 layout 파일을 추가해서 레이아웃을 중첩시킬 수 있어요.
예를 들어, /blog 경로에서만 사용할 전용 레이아웃을 만들고 싶다면 blog 폴더 안에 새로운 layout 파일을 추가하세요.

export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
export default function BlogLayout({ children }) {
return <section>{children}</section>
}
자, 머릿속으로 그림을 그려볼까요? 위에서 만든 두 개의 레이아웃을 합치면 어떻게 될까요?
가장 바깥쪽의 루트 레이아웃(app/layout.js)이 블로그 전용 레이아웃(app/blog/layout.js)을 감싸고, 그 블로그 레이아웃이 최종적으로 블로그 메인 페이지(app/blog/page.js)와 블로그 개별 게시글 페이지(app/blog/[slug]/page.js)를 겹겹이 감싸게(wrap) 된답니다.
💡 강사의 꿀팁:
마치 러시아 인형 '마트료시카'를 생각하면 쉽습니다! 큰 인형(루트 레이아웃) 안에 중간 인형(블로그 레이아웃)이 있고, 그 안에 작은 인형(페이지)이 들어가는 원리에요. 이 구조 덕분에 공통 UI 관리가 엄청나게 편해집니다.
동적 세그먼트 (Dynamic segments)를 사용하면 외부 데이터로부터 자동으로 생성되는 경로를 만들 수 있어요. 예를 들어, 블로그 게시물이 100개 있다고 해서 일일이 수동으로 라우트(폴더) 100개를 만들 수는 없겠죠? 대신 동적 세그먼트를 하나만 만들어 두면, 블로그 게시글 데이터를 기반으로 100개의 경로가 척척 알아서 생성됩니다.
동적 세그먼트를 만들려면 조각(폴더) 이름을 대괄호로 감싸면 됩니다: [segmentName].
앞서 본 app/blog/[slug]/page.tsx 경로에서는 [slug]가 바로 동적 세그먼트 역할을 하는 거예요.
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
)
}
export default async function BlogPostPage({ params }) {
const { slug } = await params
const post = await getPost(slug)
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
)
}
💡 강사의 부연 설명:
여기서params를 통해 우리가 주소창에 입력한 값을 꺼내 쓸 수 있어요./blog/hello-world로 접속했다면slug값이 "hello-world"가 되어 함수 내부로 들어옵니다. 그 값을 바탕으로 DB에서 알맞은 글을 꺼내오면(getPost(slug)) 되는 거죠! 참고로 최신 버전의 Next.js에서는params가 비동기(Promise) 형태이므로await을 써서 풀어줘야 한다는 점, 잊지 마세요!
더 자세한 내용은 동적 세그먼트(Dynamic Segments)와 params prop 문서를 참고해 보세요.
참고로, 동적 세그먼트 안에 있는 중첩 레이아웃 역시 이 params prop에 접근할 수 있습니다.
서버 컴포넌트(Server Component)로 만든 페이지에서는 searchParams prop을 사용하여 URL의 검색 매개변수(Query String, 예: ?page=2)에 접근할 수 있어요.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const filters = (await searchParams).filters
}
export default async function Page({ searchParams }) {
const filters = (await searchParams).filters
}
페이지 컴포넌트 안에서 searchParams를 사용하면, 해당 페이지는 동적 렌더링(dynamic rendering) 방식을 채택하게 됩니다. 왜냐하면 검색 매개변수 값을 읽어오려면 사용자의 '실시간 요청(incoming request)'이 반드시 필요하기 때문이죠. 미리 만들어둘(정적 렌더링) 수가 없거든요.
반면에 클라이언트 컴포넌트(Client Components)에서는 useSearchParams 훅(hook)을 사용해서 검색 매개변수를 읽어올 수 있습니다.
useSearchParams가 정적으로 렌더링된 라우트와 동적으로 렌더링된 라우트에서 각각 어떻게 다르게 동작하는지 더 알아보세요.
searchParams prop을 사용하세요.useSearchParams를 사용하세요.new URLSearchParams(window.location.search)를 사용해서 불필요한 리렌더링 없이 검색 매개변수를 읽어올 수도 있습니다.경로와 경로 사이를 부드럽게 넘나들려면(네비게이션), <Link> 컴포넌트를 사용하면 됩니다. <Link>는 Next.js에 내장된 아주 똑똑한 컴포넌트로, 기존 HTML의 <a> 태그를 확장해서 프리패칭(prefetching - 화면을 미리 백그라운드에서 불러옴) 기능과 클라이언트 사이드 네비게이션(client-side navigation - 깜빡임 없는 화면 전환) 기능을 제공해 준답니다.
예를 들어, 블로그 게시글 목록을 만들어 연결하려면 next/link에서 <Link>를 불러온(import) 다음, 컴포넌트에 href prop을 전달해 주면 돼요.
import Link from 'next/link'
export default async function Post({ post }) {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
import Link from 'next/link'
export default async function Post({ post }) {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.slug}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
알아두면 좋아요 (Good to know):
<Link>는 Next.js에서 경로 간을 이동할 때 사용하는 가장 기본적이고 권장되는 방법입니다. 만약 버튼을 클릭했을 때 로직을 실행한 후 이동하는 것처럼 더 복잡한 네비게이션이 필요하다면useRouter훅을 사용할 수도 있습니다.
Next.js는 여러분이 작성한 라우트(폴더) 구조를 똑똑하게 분석해서 params나 지정된 슬롯(named slots)의 타입을 자동으로 추론해 주는 유틸리티 타입들을 제공합니다. (TypeScript 환경에서 아주 유용해요!)
page 컴포넌트를 위한 Props 타입으로, params와 searchParams를 포함합니다.layout 컴포넌트를 위한 Props 타입으로, children과 이름이 지정된 슬롯들(예: @analytics와 같은 폴더들)을 포함합니다.이 도우미(Helpers)들은 글로벌하게(어디서나) 사용할 수 있으며, next dev, next build, 또는 next typegen 명령어를 실행할 때 자동으로 생성됩니다.
export default async function Page(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params
return <h1>Blog post: {slug}</h1>
}
export default function Layout(props: LayoutProps<'/dashboard'>) {
return (
<section>
{props.children}
{/* 만약 app/dashboard/@analytics 구조가 있다면, 타입이 지정된 슬롯으로 나타납니다: */}
{/* {props.analytics} */}
</section>
)
}
알아두면 좋아요 (Good to know)
- 정적 라우트(Static routes)의 경우
params는 빈 객체{}로 해석됩니다.PageProps,LayoutProps는 글로벌 도우미이므로 번거롭게 파일 상단에서 import 할 필요가 없습니다.- 타입들은
next dev,next build, 또는next typegen을 실행하는 동안 동적으로 생성됩니다.
💡 강사의 부연 설명:
TypeScript를 쓰다 보면params타입을 일일이 손으로 적어주는 게 꽤 번거로운 일이었어요. 하지만 이제 이 도우미 타입 덕분에 경로만 문자열로 쓱 적어주면 Next.js가 알아서 타입을 매핑해 주니 개발 속도와 안정성이 훨씬 올라갑니다!
이 페이지에서 언급된 기능들에 대해 더 깊이 알고 싶다면 아래 API 참조 문서(원문 링크)를 읽어보세요.
layout.js 파일에 대한 API 레퍼런스입니다.page.js 파일에 대한 API 레퍼런스입니다.next/link 컴포넌트로 빠르고 쾌적한 클라이언트 사이드 네비게이션을 활성화해 보세요.모든 문서의 구조적인 개요를 보려면 /docs/sitemap.md를 참고하세요.
사용 가능한 전체 문서의 색인(Index)을 보려면 /docs/llms.txt를 참고하세요.
여기까지 Next.js의 근간이 되는 레이아웃과 페이지 구조를 살펴보았습니다. 혹시 제 설명 중에 더 깊게 파고들고 싶은 부분이나, 당장 개인 프로젝트에 적용하시면서 헷갈리는 점이 있으신가요? 언제든 편하게 물어보세요! 제가 확실하게 도와드리겠습니다. 화이팅!