개요
Next.js의 'File system convention'은 프로젝트 폴더와 파일의 구조만으로
라우팅 및 주요 기능이 자동으로 결정되는 방식이다.
특히 Next.js 15.4.6 App Router 기준으로 20개가 넘는 컨벤션들이 있는데,
공식문서를 보고 하나씩 정리해보려 한다.
먼저, Next.js를 설치하게 되면 기본적으로 세팅이 되있기도 한 `layout.js`이다.
`layout` 파일은 레이아웃을 정의하기 위해 사용된다.
모든 페이지에서 공통적으로 사용되는 레이아웃은 가장 상위인 `app` 디렉터리에 만든다.
Reference
Props
`children` (필수)
레이아웃 컴포넌트는 `children` prop을 반드시 가져야 한다.
렌더링 되는동안, `children`은 레이아웃이 감싸고 있는 경로에 대한 자식들로 채워진다.
이 자식들은 보통 자식 레이아웃이나, 컴포넌트로 채워지지만,
loading.js나 error.js 같은 특별한 파일 일수도 있다.
`params` (선택)
`params`는 동적 라우팅 파라미터 객체를 담은 Promise이다.
만약, 경로가 `app/dashboard/[team]/layout.js`이고,
URL이 `/dashboard/1`이라면,
`params`는 `Promise<{ team: '1' }>`이 된다.
`params`는 Promise이므로 `async/await` 또는 React의 `use` 함수를 사용해 값을 접근해야 한다.
Root Layout
`app` 디렉터리는 반드시 루트 레이아웃이 있어야 한다.
일반적으로, `app/layout.js`가 루트 레이아웃이다.
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>{children}</body>
</html>
)
}
루트 레이아웃은 반드시 `<html>`과 `<body>` 태그를 정의해야 한다.
그리고, 메타데이터는 직접 작성하지 말고, Metatdata API를 사용해야 한다.
import './globals.css';
export const metadata = {
title: '제목',
description: '설명',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>{children}</body>
</html>
);
}
라우트 그룹을 사용하면 여러 개의 루트 레이아웃을 만들 수 있다.
다만, 서로 다른 루트 레이아웃 간 이동은 전체 페이지 새로고침을 발생시킨다.
예를 들어, `/cart`(app/(shop)/layout.js)에서
`blog`(app/(marketing)/layout.js)로 이동하면 새로고침이 발생한다.
또한, 루트 레이아웃은 동적 세그먼트 하위에 있을 수도 있다.
예를 들어, `app/[lang]/layout.js`를 국제화(i8n)에 사용 가능하다.
주의 사항
Request Object
- 네비게이션 시 레이아웃은 캐싱되어 불필요한 서버 요청이 발생하지 않게 한다.
- 레이아웃은 다시 렌더링되지 않으므로 성능이 향상된다.
- 따라서 레이아웃에서는 원본 Request 객체에 접근할 수 없다.
- 대신 Server Component 및 서버 함수에서 `headers`와 `cookies` API를 사용해야 한다.
import { cookies } from 'next/headers'
export default async function Layout({ children }) {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')
return '...'
}
Query Params
- 레이아웃은 네비게이션 시 리렌더링되지 않으므로 쿼리 파라미터가 갱신되지 않는다.
- 만약 최신 쿼리 값을 얻고 싶다면,
- 페이지 컴포넌트에서 `searchParams` prop을 사용하거나,
- Client Component에서 `useSearchParams` 훅을 사용해야 한다.
'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}
</>
)
}
Pathname
- 레이아웃은 네비게이션 시 리렌더링되지 않으므로 최신 pathname에 접근할 수 없다.
- 최신 경로를 사용하려면 Client Component에서 `usePathname`훅을 사용해야 한다.
'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>
)
}
import { Breadcrumbs } from '@/app/ui/Breadcrumbs'
export default function Layout({ children }) {
return (
<>
<Breadcrumbs />
<main>{children}</main>
</>
)
}
Fetching Data
- 레이아웃은 `children`에 데이터를 전달할 수 없다.
- 하지만, 동일한 데이터를 여러 라우트에서 가져와야 할 경우에는
- React의 `cache`를 사용해 중복을 제거하거나,
- Next.js의 `fetch`를 사용해 자동으로 요청을 중복을 제거할 수 있다.
아래와 같은 유저 데이터를 받아오는 함수가 있다고 할 때,
export async function getUser(id: string) {
const res = await fetch(`https://.../users/${id}`)
return res.json()
}
layout은 기본적으로 서버 컴포넌트이기 때문에 직접 fetch 또는 다른 데이터 조회 코드를 직접 호출할 수 있다.
// app/dashboard/layout.tsx
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}
</>
)
}
위의 layout에 children에 들어가는 page 컴포넌트에서 다시 한번 똑같은 데이터를 요청해도,
fetch를 사용할 경우, 같은 요청은 캐싱되어있기 때문에 성능 이슈가 없다.
// app/dashboard/page.tsx
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
- 레이아웃 컴포넌트는 자신을 감싸는 segment는 알 수 있어도, 자식 segment 정보까지는 알 수 없다.
- 하위 segment를 사용하려면 Client Component에서
`useSelectedLayoutSegment` 또는
`useSelectedLayoutSegments` 훅을 사용해야 한다.
아래 코드에서 `useSelectedLayoutSegment()` 훅은
URL 경로에서 상위 레이아웃 바로 아래 레벨의 활성화된 경로를 문자열로 반환한다.
예를 들어 URL이 `/blog/hello-world`라면,
부모인 `blog` 바로 아래 segment인 `hello-world`를 반환한다.
// app/ui/nav-link.tsx
'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>
)
}
따라서 만약에 아래 코드에서 `post.slug`가 'hello-world'라면,
`/blog/hello-world`로 접속했을 때, 위의 NavLink의 isActive가 true가 되면서,
bold로 스타일링 된다.
// app/blog/layout.tsx
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>
)
}
'Web > Next.js' 카테고리의 다른 글
Next.js 15 / Image Component (2) | 2025.08.13 |
---|---|
Next.js 15 / CSS Modules (1) | 2025.08.13 |
Next.js 15 + Supabase로 Kakao 로그인 구현하기 - 2 (1) | 2025.08.11 |
Next.js 15 + Supabase로 Kakao 로그인 구현하기 - 1 (3) | 2025.08.08 |