1. 핵심 개념
한 줄 정의
App Router는 요청이 들어오면 바깥 Layout부터 안쪽 Page까지 겹겹이 쌓아서 하나의 화면을 완성한다.
실생활 비유 — 러시아 마트료시카 인형
레이아웃 중첩 구조는 마트료시카 인형과 똑같다.
가장 큰 인형(RootLayout)이 앱 전체를 감싸고, 그 안에 중간 인형(Segment Layout)이 특정 구역을 감싸고, 가장 안쪽 인형(Page)이 실제 화면 내용을 담는다.
중요한 점: 바깥 인형은 안쪽 인형이 바뀌어도 그대로 유지된다. 페이지 이동 시 Layout은 유지되고 Page만 교체되는 이유가 바로 이 구조 때문이다.
| 파일 | 역할 | 비유 |
|---|---|---|
app/layout.tsx | 앱 전체 공통 틀 | 가장 바깥 인형 |
app/dashboard/layout.tsx | 대시보드 구역 공통 틀 | 중간 인형 |
app/dashboard/user/page.tsx | /dashboard/user URL의 실제 화면 | 가장 안쪽 인형 |
2. 왜 이렇게 설계하는가
문제 상황: 모든 페이지마다 Header, Sidebar, Provider를 따로 넣으면?
- 코드 중복이 심해짐
- 페이지 이동할 때마다 공통 UI가 다시 렌더링됨 → 불필요한 비용 + 깜빡임 현상
Layout 중첩 구조가 해결하는 것:
- 공통 UI 재사용 — Sidebar, Header 한 번만 선언, 전 페이지에 적용
- 불필요한 리렌더링 방지 — 페이지 이동 시 Layout은 유지, Page만 교체
- 구역별 책임 분리 — 전역 설정은 RootLayout, 구역 규칙은 Segment Layout, 화면 내용은 Page
💡 면접 tip
"왜 Layout을 분리하나요?", "페이지 이동 시 왜 리렌더링이 없나요?" 라는 질문이 나오면 위 3가지 이유가 바로 답이 된다.
3. 사용 원리 (코드 실행 흐름)
RootLayout
// app/layout.tsx
// [역할] 앱 전체의 환경을 세팅하는 최상위 틀
// [주의] 반드시 <html>과 <body> 태그를 포함해야 함 (Next.js 규칙)
export default function RootLayout({
children, // [입력] 하위 Layout 또는 Page가 여기 들어옴
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
{/* [처리] 전역 Provider, 전역 스타일, 공통 리소스 세팅 */}
<QueryClientProvider client={queryClient}>
<ThemeProvider>
{children} {/* [출력] 모든 페이지의 기본 프레임으로 감싸서 반환 */}
</ThemeProvider>
</QueryClientProvider>
</body>
</html>
);
}
Segment Layout (구역 레이아웃)
// app/dashboard/layout.tsx
// [역할] /dashboard 이하 모든 URL에 공통으로 적용되는 틀
export default function DashboardLayout({
children, // [입력] /dashboard/user, /dashboard/settings 등 하위 Page가 들어옴
}: {
children: React.ReactNode;
}) {
return (
<section className="dashboard-layout">
{/* [처리] 대시보드 전용 사이드바, 권한 체크 등 구역 규칙 적용 */}
<Sidebar />
<main>
{children} {/* [출력] 대시보드 틀 안에 각 Page 콘텐츠 삽입 */}
</main>
</section>
);
}
Page (화면 단위)
// app/dashboard/user/page.tsx
// [역할] /dashboard/user URL에서만 보이는 실제 화면 콘텐츠
export default function UserPage() {
// [입력] 이 URL에서만 필요한 데이터 fetch, params 처리
// [처리] 화면 전용 UI 구성
// [출력] 유저 목록 화면 콘텐츠 반환
return <h1>User List</h1>;
}
4. 데이터 흐름 (입력 → 처리 → 출력)
/dashboard/user 요청 시 실행 순서
1. 브라우저 → /dashboard/user 요청
↓
2. app/layout.tsx (RootLayout) 실행
→ <html>, <body>, 전역 Provider 세팅
↓
3. app/dashboard/layout.tsx (DashboardLayout) 실행
→ Sidebar, 권한 체크 등 대시보드 구역 규칙 적용
↓
4. app/dashboard/user/page.tsx (UserPage) 실행
→ 유저 목록 데이터 fetch + UI 렌더링
↓
5. 모든 Layout + Page가 합쳐진 완성된 HTML 반환
페이지 이동 시 (/dashboard/user → /dashboard/settings):
RootLayout→ 유지 (리렌더링 없음)DashboardLayout→ 유지 (리렌더링 없음)page.tsx→user/page.tsx에서settings/page.tsx로 교체
5. 주요 옵션 (실무 기준)
| 위치 | 주 역할 | 실무에서 주로 넣는 것 |
|---|---|---|
| RootLayout | 앱 전체 환경 | QueryClientProvider, ThemeProvider, 전역 CSS, 폰트, Analytics |
| Segment Layout | 구역 규칙 + 공통 UI | Sidebar, 로그인 필수 체크, 구역 전용 Context, 레이아웃 그리드 |
| Page | URL 전용 화면 | 화면 전용 data fetch, params/searchParams 처리, UI 컴포넌트 조립 |
6. 실무 사용 패턴
✅ 권장 패턴 — 구역별 인증 처리
// app/dashboard/layout.tsx
// [실무 패턴] 대시보드 전체에 로그인 체크를 한 번만 적용
import { redirect } from "next/navigation";
import { getSession } from "@/lib/auth";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getSession(); // [입력] 세션 정보 조회
if (!session) {
redirect("/login"); // [처리] 비로그인이면 로그인 페이지로 이동
}
return (
<div>
<Sidebar user={session.user} /> {/* [출력] 로그인된 유저 정보를 Sidebar에 전달 */}
<main>{children}</main>
</div>
);
}
→ dashboard 하위 페이지 어디에서도 인증 코드를 따로 작성할 필요 없음
🔧 실무 point
실무에서는 인증이 필요한 구역에 Segment Layout을 두고 여기서 세션을 체크한다.
각page.tsx마다 인증 코드를 반복하는 건 누락 위험이 있고 유지보수도 어렵다.
✅ 권장 패턴 — 전역 Provider는 RootLayout에
// app/layout.tsx
// [실무 패턴] QueryClient는 앱 전체에서 하나만 존재해야 하므로 RootLayout에 위치
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "@/lib/queryClient";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</body>
</html>
);
}
🔧 실무 point
QueryClientProvider,ThemeProvider처럼 앱 전체에서 하나만 존재해야 하는 Provider는 반드시 RootLayout에 배치한다. Segment Layout에 넣으면 구역 진입마다 인스턴스가 새로 생성된다.
7. 자주 하는 실수 ⚠️
❌ RootLayout에서 페이지별 데이터 fetch
// app/layout.tsx — 잘못된 예
export default async function RootLayout({ children }) {
const userData = await fetchUser(); // 모든 요청마다 실행됨
// ...
}
✅ 올바른 방법: 특정 페이지에서만 필요한 데이터는 해당 page.tsx에서 fetch
🔎 이유: RootLayout은 앱의 모든 요청에서 실행됨. 여기서 불필요한 fetch를 하면 관계없는 페이지 접근 시에도 비용이 발생한다.
❌ 구역 전용 UI를 RootLayout에 배치
// app/layout.tsx — 잘못된 예
export default function RootLayout({ children }) {
return (
<html><body>
<DashboardSidebar /> {/* 대시보드 전용인데 모든 페이지에 노출됨 */}
{children}
</body></html>
);
}
✅ 올바른 방법: app/dashboard/layout.tsx에 배치
🔎 이유: RootLayout은 로그인 페이지, 404 페이지 등 모든 곳에 적용됨. 구역 전용 UI가 의도치 않은 곳에 노출된다.
❌ 인증 로직을 page마다 중복 작성
// app/dashboard/user/page.tsx — 잘못된 예
export default async function UserPage() {
const session = await getSession();
if (!session) redirect("/login"); // 모든 dashboard 페이지마다 이 코드가 반복됨
// ...
}
✅ 올바른 방법: app/dashboard/layout.tsx에서 한 번만 처리
🔎 이유: 인증 로직이 각 page에 흩어지면 누락 가능성이 생기고, 로직 변경 시 모든 파일을 수정해야 한다.
8. 한 줄 요약
RootLayout은 앱의 "환경",
Segment Layout은 구역의 "틀과 규칙",
Page는 URL 하나의 "본문 화면" 이다.