Next.js App Router에서 Layout은 여러 페이지가 공유하는 UI를 한 곳에 정의하는 컴포넌트다. 페이지 이동 시 Layout은 언마운트되지 않고 상태를 유지하기 때문에, 내비게이션이나 사이드바 같은 공통 요소를 배치하기에 적합하다. layout.tsx 파일을 폴더에 두는 것만으로 해당 경로 하위 전체에 자동으로 적용된다.
app/layout.tsx는 애플리케이션 전체를 감싸는 Root Layout으로, 반드시 존재해야 한다. <html>과 <body> 태그를 이 파일에서 정의한다.
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ko">
<body>
<header>공통 헤더</header>
{children}
<footer>공통 푸터</footer>
</body>
</html>
)
}
Root Layout: app/ 디렉터리 최상단의 layout.tsx. 삭제하거나 <html>·<body> 태그를 생략하면 빌드 오류가 발생한다.
폴더마다 layout.tsx를 추가해 레이아웃을 중첩할 수 있다. 하위 Layout은 상위 Layout 안에 자동으로 중첩된다.
app/
├── layout.tsx ← Root Layout (헤더, 푸터)
├── dashboard/
│ ├── layout.tsx ← Dashboard Layout (사이드바)
│ ├── page.tsx
│ └── settings/
│ └── page.tsx ← 두 Layout 모두 적용
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<nav>사이드바</nav>
<main>{children}</main>
</div>
)
}
/dashboard/settings에 접근하면 Root Layout → Dashboard Layout → 페이지 순으로 렌더링된다.
**template.tsx**는 layout.tsx와 동일한 위치에 둘 수 있지만, 동작 방식이 다르다.
| 항목 | layout.tsx | template.tsx |
|---|---|---|
| 페이지 이동 시 | 언마운트 안 됨 (상태 유지) | 매번 새 인스턴스 생성 |
| 사용 사례 | 내비게이션, 전역 상태 | 페이지 진입 애니메이션, 매번 초기화 필요한 UI |
useEffect 실행 | 첫 마운트 한 번 | 이동마다 재실행 |
template.tsx: 경로 이동마다 새 컴포넌트 인스턴스를 생성한다. 진입 애니메이션처럼 매 방문마다 트리거가 필요한 경우 사용한다.
Layout은 Server Component이므로 async/await로 직접 데이터를 가져올 수 있다.
// app/dashboard/layout.tsx
async function getUser() {
const res = await fetch('https://api.example.com/me', {
cache: 'no-store',
})
return res.json()
}
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const user = await getUser()
return (
<div className="flex">
<nav>
<span>{user.name}</span>
</nav>
<main>{children}</main>
</div>
)
}
Layout에서 패칭한 데이터는 하위 페이지로 props를 통해 전달할 수 없다. 공유가 필요하면 React Context나 서버 캐시(cache)를 활용한다.
내비게이션처럼 현재 경로에 반응해야 하는 컴포넌트는 Client Component로 분리한 뒤 Layout에서 사용한다.
// components/Nav.tsx
'use client'
import { usePathname } from 'next/navigation'
export function Nav() {
const pathname = usePathname()
return (
<nav>
<a href="/" style={{ fontWeight: pathname === '/' ? 'bold' : 'normal' }}>홈</a>
<a href="/dashboard" style={{ fontWeight: pathname.startsWith('/dashboard') ? 'bold' : 'normal' }}>대시보드</a>
</nav>
)
}
// app/layout.tsx
import { Nav } from '@/components/Nav'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<Nav />
{children}
</body>
</html>
)
}
usePathname은 Client Component에서만 동작하므로, Layout 자체를 Client Component로 만들지 않고 내비게이션 부분만 분리하는 것이 올바른 패턴이다.