export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section>{children}</section>
}
export default function DashboardLayout({ children }) {
return <section>{children}</section>
}
루트 레이아웃(Root Layout) 은 app 디렉토리 최상단에 위치하는 가장 꼭대기 레이아웃이에요. 주로 <html>과 <body> 태그를 정의하고, 전역적으로 공유해야 하는 UI(예: 글로벌 내비게이션 바 등)를 설정할 때 쓰입니다.
강사의 팁 💡
최상단app/layout.tsx는 반드시 있어야 합니다. 이 파일이 없으면 Next.js가 페이지를 그릴 때 기본이 되는 HTML 뼈대를 잡을 수가 없거든요!
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
children (필수)레이아웃 컴포넌트는 반드시 children prop을 받아서 사용해야 해요. 렌더링될 때, 이 children 자리에는 레이아웃이 감싸고 있는 하위 라우트 세그먼트들이 쏙 들어가게 됩니다.
보통은 하위에 있는 또 다른 Layout 컴포넌트나 Page 컴포넌트가 들어오겠지만, 상황에 따라 Loading(로딩 화면)이나 Error(에러 화면) 같은 특수 파일들이 들어올 수도 있어요.
params (선택)루트(최상위) 세그먼트부터 해당 레이아웃까지의 동적 라우트 파라미터 (Dynamic route parameters) 객체를 담고 있는 Promise 객체입니다.
강사의 팁 💡
"왜 갑자기 Promise죠?" 라고 생각하실 수 있어요. Next.js 15 버전부터는 성능 최적화와 서버 사이드에서의 비동기 렌더링 파이프라인을 개선하기 위해 파라미터들을 비동기적으로 처리하기 시작했어요. 그래서 반드시await을 써서 열어봐야 합니다!
export default async function Layout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ team: string }>
}) {
const { team } = await params
}
export default async function Layout({ children, params }) {
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'] }> |
params prop이 Promise이기 때문에, 값을 꺼내 쓰려면 반드시 async/await을 사용하거나 React의 use 함수를 사용해야 합니다.params가 동기적인(일반 객체) prop이었어요. 이전 버전과의 호환성을 위해 Next.js 15에서도 여전히 동기적으로 접근할 수는 있지만, 이 기능은 앞으로 폐기(deprecated)될 예정이니 지금부터라도 꼭 비동기 방식으로 코딩하는 습관을 들이세요!타입스크립트를 쓰실 때, LayoutProps를 사용해서 레이아웃에 타입을 지정할 수 있어요. 이걸 쓰면 디렉토리 구조를 바탕으로 추론된 이름 있는 슬롯(named slots)과 params에 대해 강력한 타입 지원을 받을 수 있답니다. LayoutProps는 전역적으로 사용할 수 있는 헬퍼 타입이에요.
export default function Layout(props: LayoutProps<'/dashboard'>) {
return (
<section>
{props.children}
{/* 만약 app/dashboard/@analytics 파일이 있다면, 타입이 지정된 슬롯으로 나타납니다: */}
{/* {props.analytics} */}
</section>
)
}
알아두면 좋은 점 (Good to know):
- 이런 타입들은
next dev,next build, 혹은next typegen명령어가 실행될 때 자동으로 생성됩니다.- 타입이 생성되고 나면,
LayoutProps헬퍼는 전역적으로 사용 가능하므로 파일 상단에서 따로import할 필요가 없습니다. 완전 편하죠?
app 디렉토리에는 반드시 루트 레이아웃이 하나 포함되어야 합니다. 가장 최상단에 위치하는 레이아웃이고, 보통 파일명은 app/layout.js 예요.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>{children}</body>
</html>
)
}
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
<html>과 <body> 태그를 정의해야 합니다. 이건 선택이 아니라 필수 규칙이에요!<title>이나 <meta> 같은 <head> 태그 관련 내용을 수동으로 직접 추가하시면 안 됩니다. 대신, Next.js의 Metadata API를 사용하세요. 이 API를 쓰면 스트리밍(streaming)이나 <head> 요소 중복 제거(de-duplicating) 같은 복잡한 요구사항들을 알아서 우아하게 처리해 준답니다.layout.js가 하나도 없다면 그게 바로 루트 레이아웃이 되거든요. 자주 쓰이는 두 가지 방법이 있어요:app/(shop)/layout.js와 app/(marketing)/layout.js처럼 폴더명을 괄호로 묶어 그룹을 나누고 각각 루트 레이아웃을 두는 방식입니다.app/layout.js 생략하기: 이렇게 하면 app/dashboard/layout.js와 app/blog/layout.js 등 각각의 하위 디렉토리 레이아웃이 해당 경로의 루트 레이아웃 역할을 하게 됩니다.app/[lang]/layout.js 처럼 구성하는 경우죠.이 부분은 실무에서 버그 찾느라 머리 싸매지 않으려면 정말 꼼꼼히 보셔야 해요!
레이아웃은 불필요한 서버 요청을 줄이기 위해 네비게이션(페이지 이동) 시 클라이언트에 캐싱(저장)됩니다.
레이아웃 (Layouts)은 라우팅 시 다시 렌더링(rerender)되지 않습니다. 페이지 사이를 이동할 때 불필요한 연산을 막기 위해 캐싱하고 재사용하는 것이죠.
따라서 레이아웃 안에서는 원본 HTTP Request 객체에 직접 접근하는 것을 막아두었습니다. 만약 접근하게 되면 레이아웃 안에서 느리거나 무거운 코드가 매번 실행될 수 있고, 이는 곧 성능 저하로 이어지기 때문이에요. Next.js의 똑똑한 방어 기제라고 생각하시면 됩니다.
그래도 요청 관련 데이터가 필요하시다고요? Server Components나 서버 함수 안에서 headers와 cookies API를 사용하시면 됩니다.
import { cookies } from 'next/headers'
export default async function Layout({ children }) {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')
return '...'
}
import { cookies } from 'next/headers'
export default async function Layout({ children }) {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')
return '...'
}
레이아웃은 페이지 이동 시 재렌더링되지 않는다고 말씀드렸죠? 그래서 레이아웃은 URL 뒤에 붙는 검색 파라미터(?search=keyword 같은 것들)에 접근할 수 없습니다. 만약 접근할 수 있게 해준다면, 사용자가 검색어를 바꿔도 레이아웃은 다시 안 그려지니까 예전 검색어(stale data)를 계속 들고 있게 되는 문제가 생길 테니까요.
최신 업데이트된 쿼리 파라미터를 읽고 싶다면 두 가지 방법이 있습니다.
1. Page 컴포넌트의 searchParams prop을 사용하기.
2. 클라이언트 컴포넌트(Client Component) 안에서 useSearchParams 훅(hook) 사용하기. 클라이언트 컴포넌트는 네비게이션 시에 최신 상태로 재렌더링 되기 때문에 가장 최신의 쿼리 값을 가져올 수 있습니다.
강사의 팁 💡
"그럼 레이아웃 안에 검색바가 있으면 어떡하죠?" 라고 물어보실 수 있는데, 바로 아래 예제처럼 검색바 부분만 Client Component로 분리해서 레이아웃에 쏙 집어넣으면 깔끔하게 해결됩니다!
'use client'
import { useSearchParams } from 'next/navigation'
export default function Search() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
return '...'
}
'use client'
import { useSearchParams } from 'next/navigation'
export default function Search() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
return '...'
}
import Search from '@/app/ui/search'
export default function Layout({ children }) {
return (
<>
<Search />
{children}
</>
)
}
import Search from '@/app/ui/search'
export default function Layout({ children }) {
return (
<>
<Search />
{children}
</>
)
}
이것도 위와 완전히 똑같은 이유입니다! 레이아웃은 페이지 이동 시 재렌더링되지 않기 때문에 현재 경로(pathname)에 접근할 수 없습니다.
현재 어떤 경로에 있는지(pathname) 알고 싶다면, 클라이언트 컴포넌트를 만들고 그 안에서 usePathname 훅을 사용하세요. 클라이언트 컴포넌트는 라우팅 될 때 재렌더링되면서 최신 pathname을 잘 잡아냅니다. (이걸로 현재 위치를 보여주는 Breadcrumbs 기능 같은 걸 만듭니다.)
'use client'
import { usePathname } from 'next/navigation'
// 단순화된 빵부스러기(Breadcrumbs) 네비게이션 로직입니다
export default function Breadcrumbs() {
const pathname = usePathname()
const segments = pathname.split('/')
return (
<nav>
{segments.map((segment, index) => (
<span key={index}>
{' > '}
{segment}
</span>
))}
</nav>
)
}
'use client'
import { usePathname } from 'next/navigation'
// 단순화된 빵부스러기(Breadcrumbs) 네비게이션 로직입니다
export default function Breadcrumbs() {
const pathname = usePathname()
const segments = pathname.split('/')
return (
<nav>
{segments.map((segment, index) => (
<span key={index}>
{' > '}
{segment}
</span>
))}
</nav>
)
}
import { Breadcrumbs } from '@/app/ui/Breadcrumbs'
export default function Layout({ children }) {
return (
<>
<Breadcrumbs />
<main>{children}</main>
</>
)
}
import { Breadcrumbs } from '@/app/ui/Breadcrumbs'
export default function Layout({ children }) {
return (
<>
<Breadcrumbs />
<main>{children}</main>
</>
)
}
레이아웃 컴포넌트에서 가져온 데이터를 그 하위 자식(children)에게 직접 Props로 넘겨줄 수는 없습니다.
하지만 걱정 마세요! 한 라우트 안에서 똑같은 데이터를 여러 번 불러와도 성능 걱정을 하실 필요가 없거든요. React의 cache 기능을 사용하면 Next.js가 알아서 중복된 요청(dedupe)을 하나로 묶어줍니다.
또한, Next.js 환경에서 기본 fetch 함수를 사용하면 알아서 중복 제거 처리가 됩니다.
강사의 팁 💡
React 개발하시면서 상태 관리 라이브러리로 데이터를 전역에 올려두고 쓰시던 분들에겐 엄청난 혁신이죠! "레이아웃에서도getUser부르고, 자식 페이지에서도 또getUser부르면 네트워크 요청 2번 가는 거 아냐?" 라고 생각하실 텐데, 알아서 1번만 요청 가도록 캐싱해주니 편하게 중복 호출하셔도 됩니다.
export async function getUser(id: string) {
const res = await fetch(`https://.../users/${id}`)
return res.json()
}
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}
</>
)
}
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}
</>
)
}
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>
)
}
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>
)
}
레이아웃은 본인보다 더 아래에 있는 하위 라우트 세그먼트가 뭔지 기본적으로는 알 수 없습니다. 만약 하위 경로 정보에 접근하고 싶다면, 클라이언트 컴포넌트 안에서 useSelectedLayoutSegment 혹은 useSelectedLayoutSegments 훅을 활용하시면 됩니다. (현재 선택된 하위 메뉴를 파악할 때 유용해요!)
'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}`}
// 링크가 활성화된 상태인지에 따라 스타일을 다르게 줍니다
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{children}
</Link>
)
}
'use client'
import Link from 'next/link'
import { useSelectedLayoutSegment } from 'next/navigation'
export default function NavLinks({ slug, children }) {
const segment = useSelectedLayoutSegment()
const isActive = slug === segment
return (
<Link
href={`/blog/${slug}`}
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{children}
</Link>
)
}
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>
)
}
import { NavLink } from './nav-link'
import getPosts from './get-posts'
export default async function Layout({ children }) {
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>
)
}
title이나 meta 같은 <head> 안의 HTML 요소들은 metadata 객체를 내보내거나 generateMetadata 함수를 사용해서 수정할 수 있습니다.
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Next.js',
}
export default function Layout({ children }: { children: React.ReactNode }) {
return '...'
}
export const metadata = {
title: 'Next.js',
}
export default function Layout({ children }) {
return '...'
}
알아두면 좋은 점 (Good to know): 앞서 강조했듯이, 루트 레이아웃에
<title>이나<meta>태그를<head>안에 수동으로 직접 쓰지 마세요. 꼭 Metadata APIs를 쓰셔야 합니다. 그래야 Next.js가 중복도 잡아주고 스트리밍 처리도 깔끔하게 해 주니까요!
현재 페이지에 맞는 네비게이션 메뉴에 하이라이트를 주고 싶을 때, usePathname 훅을 사용하면 현재 경로와 링크 경로를 비교할 수 있어요.
단, usePathname은 클라이언트 전용 훅이기 때문에 네비게이션 링크 부분만 따로 Client Component로 분리해 주고, 그걸 레이아웃 파일에서 가져와(import) 써야 합니다. 실무 GNB(Global Navigation Bar) 만들 때 무조건 쓰는 패턴이니 꼭 기억해두세요!
'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>
)
}
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Links() {
const pathname = usePathname()
return (
<nav>
<Link className={`link ${pathname === '/' ? 'active' : ''}`} href="/">
Home
</Link>
<Link
className={`link ${pathname === '/about' ? 'active' : ''}`}
href="/about"
>
About
</Link>
</nav>
)
}
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>
)
}
import { NavLinks } from '@/app/ui/nav-links'
export default function Layout({ children }) {
return (
<html lang="en">
<body>
<NavLinks />
<main>{children}</main>
</body>
</html>
)
}
params (params를 이용해 동적 콘텐츠 보여주기)동적 라우트 세그먼트 (dynamic route segments) 폴더 구조를 사용하면, 전달받은 params 객체를 활용해서 특정 데이터를 가져오거나 알맞은 콘텐츠를 레이아웃에 띄워줄 수 있습니다.
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>
)
}
export default async function DashboardLayout({ children, params }) {
const { team } = await params
return (
<section>
<header>
<h1>Welcome to {team}'s Dashboard</h1>
</header>
<main>{children}</main>
</section>
)
}
params in Client Components (클라이언트 컴포넌트에서 params 읽어오기)클라이언트 컴포넌트는 async/await을 사용할 수 없어요. 그런데 15버전부터 params가 Promise로 넘어온다고 했죠?
클라이언트 컴포넌트에서 이 Promise params를 열어보려면, React의 새로운 기능인 use 함수를 사용하셔야 합니다!
'use client'
import { use } from 'react'
export default function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = use(params)
}
'use client'
import { use } from 'react'
export default function Page({ params }) {
const { slug } = use(params)
}
| 버전 | 변경 사항 |
|---|---|
v15.0.0-RC | params가 이제 Promise로 변경되었습니다. 마이그레이션을 위한 codemod (코드 자동 변환 도구)도 지원됩니다. |
v13.0.0 | layout 파일 시스템이 처음 도입되었습니다. |
자, 여기까지 Next.js의 layout.js 공식 문서 전체를 함께 살펴봤습니다. 레이아웃의 기본 역할부터 주의사항, 최신 버전의 Promise 파라미터까지 감이 좀 잡히시나요?
혹시 공부하시다가 헷갈리거나 이 중에서 실습으로 직접 코드를 짜보고 싶은 부분이 있다면 편하게 말씀해 주세요! 제가 어떻게 도와드릴까요?