파일 시스템 규칙: layout.js
파일 시스템 규칙: layout.js | Next.js
섹션 제목: “파일 시스템 규칙: layout.js | Next.js”출처 URL: https://nextjs.org/docs/app/api-reference/file-conventions/layout
layout.js
섹션 제목: “layout.js”최종 업데이트 2026년 2월 20일
layout 파일은 Next.js 애플리케이션에서 레이아웃을 정의하는 데 사용됩니다.
app/dashboard/layout.tsx
JavaScriptTypeScript
export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return <section>{children}</section> }루트 레이아웃 은 루트 app 디렉터리에서 가장 상위에 있는 레이아웃입니다. <html> 및 <body> 태그와 전역으로 공유되는 UI를 정의하는 데 사용합니다.
app/layout.tsx
JavaScriptTypeScript
export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ) }Props
섹션 제목: “Props”children (필수)
섹션 제목: “children (필수)”레이아웃 컴포넌트는 children prop을 받아서 사용해야 합니다. 렌더링 중에 children 은 레이아웃이 감싸고 있는 경로 세그먼트로 채워집니다. 주로 하위 Layout (존재한다면) 또는 Page 컴포넌트가 되지만, 상황에 따라 Loading이나 Error 같은 다른 특수 파일일 수도 있습니다.
params (선택)
섹션 제목: “params (선택)”루트 세그먼트에서 해당 레이아웃까지의 동적 경로 파라미터 객체로 해결되는 프로미스입니다.
app/dashboard/[team]/layout.tsx
JavaScriptTypeScript
export default async function Layout({ children, params, }: { children: React.ReactNode params: Promise<{ team: string }> }) { const { team } = await params }| 예시 경로 | URL | params |
|---|---|---|
app/dashboard/[team]/layout.js | /dashboard/1 | Promise<{ team: '1' }> |
app/shop/[tag]/[item]/layout.js | /shop/1/2 | Promise<{ tag: '1', item: '2' }> |
app/blog/[...slug]/layout.js | /blog/1/2 | Promise<{ slug: ['1', '2'] }> |
paramsprop은 프로미스이므로 값을 읽으려면async/await또는 React의use함수를 사용해야 합니다.- 버전 14 및 이전 버전에서는
params가 동기 prop이었습니다. 하위 호환을 위해 Next.js 15에서도 동기적으로 접근할 수 있지만, 이 동작은 앞으로 제거될 예정입니다.
- 버전 14 및 이전 버전에서는
Layout Props Helper
섹션 제목: “Layout Props Helper”디렉터리 구조에서 유추되는 강한 타입의 params 와 이름 있는 슬롯을 얻기 위해 레이아웃을 LayoutProps 로 타입 지정할 수 있습니다. LayoutProps 는 전역에서 사용할 수 있는 헬퍼입니다.
app/dashboard/layout.tsx
export default function Layout(props: LayoutProps<'/dashboard'>) { return ( <section> {props.children} {/* If you have app/dashboard/@analytics, it appears as a typed slot: */} {/* {props.analytics} */} </section> ) }알아두면 좋아요 :
next dev,next build,next typegen중 하나를 실행할 때 타입이 생성됩니다.- 타입 생성 이후에는
LayoutProps헬퍼가 전역에서 제공되므로 import 할 필요가 없습니다.
Root Layout
섹션 제목: “Root Layout”app 디렉터리에는 루트 app 디렉터리에서 가장 상위에 위치한 루트 레이아웃 이 반드시 포함되어야 합니다. 일반적으로 루트 레이아웃은 app/layout.js 입니다.
app/layout.tsx
JavaScriptTypeScript
export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html> <body>{children}</body> </html> ) }- 루트 레이아웃은
<html>및<body>태그를 반드시 정의해야 합니다.<title>또는<meta>와 같은<head>태그를 루트 레이아웃에 수동으로 추가해서는 안 됩니다. 대신 Metadata API를 사용하면 스트리밍이나<head>요소 중복 제거 같은 고급 요구 사항을 자동으로 처리할 수 있습니다.
- 여러 개의 루트 레이아웃 을 만들 수 있습니다. 위쪽에
layout.js가 없는 레이아웃은 모두 루트 레이아웃입니다. 대표적인 두 가지 방식:app/(shop)/layout.js,app/(marketing)/layout.js처럼 route group을 사용app/layout.js를 생략하여app/dashboard/layout.js,app/blog/layout.js등의 하위 디렉터리 레이아웃이 각 디렉터리의 루트 레이아웃이 되도록 함- 여러 루트 레이아웃 사이 를 탐색하면 클라이언트 측 전환이 아닌 페이지 전체 새로고침 이 발생합니다.
- 루트 레이아웃은 internationalization을 구현할 때처럼
app/[lang]/layout.js같이 동적 세그먼트 아래 위치시킬 수도 있습니다.
주의사항
섹션 제목: “주의사항”Request Object
섹션 제목: “Request Object”레이아웃은 탐색 중 불필요한 서버 요청을 피하기 위해 클라이언트 측에 캐시됩니다.
Layouts는 다시 렌더링되지 않습니다. 페이지 간 탐색 시 레이아웃을 캐시하고 재사용하여 불필요한 연산을 줄일 수 있습니다. 레이아웃에서 원시 요청 객체 접근을 제한하면, Next.js는 잠재적으로 느리거나 비용이 큰 사용자 코드를 레이아웃에서 실행하는 것을 방지해 성능 저하를 막을 수 있습니다.
요청 객체가 필요하다면 Server Components 및 함수에서 headers와 cookies API를 사용할 수 있습니다.
app/shop/layout.tsx
JavaScriptTypeScript
import { cookies } from 'next/headers'
export default async function Layout({ children }) { const cookieStore = await cookies() const theme = cookieStore.get('theme') return '...' }Query params
섹션 제목: “Query params”레이아웃은 탐색 시 다시 렌더링되지 않으므로, 그렇지 않으면 오래될 검색 파라미터(search params)에 접근할 수 없습니다.
최신 쿼리 파라미터를 읽으려면 Page의 searchParams prop을 사용하거나, Client Component 안에서 useSearchParams 훅으로 읽으면 됩니다. Client Component는 탐색 시 다시 렌더링되므로 최신 쿼리 파라미터를 사용할 수 있습니다.
app/ui/search.tsx
JavaScriptTypeScript
'use client'
import { useSearchParams } from 'next/navigation'
export default function Search() { const searchParams = useSearchParams()
const search = searchParams.get('search')
return '...' }app/shop/layout.tsx
JavaScriptTypeScript
import Search from '@/app/ui/search'
export default function Layout({ children }) { return ( <> <Search /> {children} </> ) }Pathname
섹션 제목: “Pathname”레이아웃은 탐색 시 다시 렌더링되지 않으므로, 그렇지 않으면 오래될 pathname에 접근할 수 없습니다.
현재 pathname이 필요하다면 Client Component 안에서 usePathname 훅으로 읽으면 됩니다. Client Component는 탐색 중 다시 렌더링되므로 최신 pathname에 접근할 수 있습니다.
app/ui/breadcrumbs.tsx
JavaScriptTypeScript
'use client'
import { usePathname } from 'next/navigation'
// Simplified breadcrumbs logic export default function Breadcrumbs() { const pathname = usePathname() const segments = pathname.split('/')
return ( <nav> {segments.map((segment, index) => ( <span key={index}> {' > '} {segment} </span> ))} </nav> ) }app/docs/layout.tsx
JavaScriptTypeScript
import { Breadcrumbs } from '@/app/ui/Breadcrumbs'
export default function Layout({ children }) { return ( <> <Breadcrumbs /> <main>{children}</main> </> ) }Fetching Data
섹션 제목: “Fetching Data”레이아웃은 children 에 데이터를 전달할 수 없습니다. 그러나 하나의 경로에서 같은 데이터를 여러 번 가져오고 React cache를 사용해 요청을 중복 제거해도 성능에는 영향을 주지 않습니다.
또는 Next.js에서 fetch를 사용할 때는 요청이 자동으로 중복 제거됩니다.
app/lib/data.ts
JavaScriptTypeScript
export async function getUser(id: string) { const res = await fetch(`https://.../users/${id}`) return res.json() }app/dashboard/layout.tsx
JavaScriptTypeScript
import { getUser } from '@/app/lib/data' import { UserName } from '@/app/ui/user-name'
export default async function Layout({ children }) { const user = await getUser('1')
return ( <> <nav> {/* ... */} <UserName user={user.name} /> </nav> {children} </> ) }app/dashboard/page.tsx
JavaScriptTypeScript
import { getUser } from '@/app/lib/data' import { UserName } from '@/app/ui/user-name'
export default async function Page() { const user = await getUser('1')
return ( <div> <h1>Welcome {user.name}</h1> </div> ) }Accessing child segments
섹션 제목: “Accessing child segments”레이아웃은 자신 아래 경로 세그먼트에 접근할 수 없습니다. 모든 경로 세그먼트가 필요하다면 Client Component 안에서 useSelectedLayoutSegment 또는 useSelectedLayoutSegments를 사용할 수 있습니다.
app/ui/nav-link.tsx
JavaScriptTypeScript
'use client'
import Link from 'next/link' import { useSelectedLayoutSegment } from 'next/navigation'
export default function NavLink({ slug, children, }: { slug: string children: React.ReactNode }) { const segment = useSelectedLayoutSegment() const isActive = slug === segment
return ( <Link href={`/blog/${slug}`} // Change style depending on whether the link is active style={{ fontWeight: isActive ? 'bold' : 'normal' }} > {children} </Link> ) }app/blog/layout.tsx
JavaScriptTypeScript
import { NavLink } from './nav-link' import getPosts from './get-posts'
export default async function Layout({ children, }: { children: React.ReactNode }) { const featuredPosts = await getPosts() return ( <div> {featuredPosts.map((post) => ( <div key={post.id}> <NavLink slug={post.slug}>{post.title}</NavLink> </div> ))} <div>{children}</div> </div> ) }메타데이터
섹션 제목: “메타데이터”metadata 객체나 generateMetadata 함수를 사용하면 title, meta 등의 <head> HTML 요소를 수정할 수 있습니다.
app/layout.tsx
JavaScriptTypeScript
import type { Metadata } from 'next'
export const metadata: Metadata = { title: 'Next.js', }
export default function Layout({ children }: { children: React.ReactNode }) { return '...' }알아두면 좋아요 : 루트 레이아웃에
<title>및<meta>같은<head>태그를 직접 추가하면 안 됩니다. 대신 스트리밍 및<head>요소 중복 제거 같은 고급 요구 사항을 자동으로 처리하는 Metadata API를 사용하세요.
활성 내비게이션 링크
섹션 제목: “활성 내비게이션 링크”usePathname 훅을 사용해 내비게이션 링크가 활성 상태인지 판단할 수 있습니다.
usePathname 는 클라이언트 훅이므로 내비게이션 링크를 Client Component로 분리한 뒤 레이아웃에서 가져와야 합니다:
app/ui/nav-links.tsx
JavaScriptTypeScript
'use client'
import { usePathname } from 'next/navigation' import Link from 'next/link'
export function NavLinks() { const pathname = usePathname()
return ( <nav> <Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/"> Home </Link>
<Link className={`link ${pathname === '/about' ? 'active' : ''}`} href="/about" > About </Link> </nav> ) }app/layout.tsx
JavaScriptTypeScript
import { NavLinks } from '@/app/ui/nav-links'
export default function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <body> <NavLinks /> <main>{children}</main> </body> </html> ) }params 기반 콘텐츠 표시
섹션 제목: “params 기반 콘텐츠 표시”동적 라우트 세그먼트를 사용하면 params prop에 따라 특정 콘텐츠를 표시하거나 가져올 수 있습니다.
app/dashboard/layout.tsx
JavaScriptTypeScript
export default async function DashboardLayout({ children, params, }: { children: React.ReactNode params: Promise<{ team: string }> }) { const { team } = await params
return ( <section> <header> <h1>Welcome to {team}'s Dashboard</h1> </header> <main>{children}</main> </section> ) }클라이언트 컴포넌트에서 params 읽기
섹션 제목: “클라이언트 컴포넌트에서 params 읽기”params 를 Client Component( async 가 될 수 없음)에서 사용하려면 React의 use 함수를 사용해 promise를 읽을 수 있습니다:
app/page.tsx
JavaScriptTypeScript
'use client'
import { use } from 'react'
export default function Page({ params, }: { params: Promise<{ slug: string }> }) { const { slug } = use(params) }버전 기록
섹션 제목: “버전 기록”| Version | Changes |
|---|---|
v15.0.0-RC | params 가 이제 Promise입니다. codemod을 사용할 수 있습니다. |
v13.0.0 | layout 이 도입되었습니다. |
보내기