레이아웃 컴포넌트 생성 및 중첩
children을 받는 레이아웃을 여러 세그먼트에 중첩해 유지되는 UI 범위와 데이터 공유 경계를 설계합니다.
Next.js App Router에서 레이아웃 컴포넌트는 단순히 UI를 공유하는 것을 넘어, 애플리케이션의 구조와 성능을 최적화하는 데 핵심적인 역할을 합니다.
3장에서 레이아웃의 기본 개념과 최상위 레이아웃, 중첩 레이아웃 사용법을 간략히 살펴보았습니다.
이번 절에서는 레이아웃 컴포넌트를 더 깊이 이해하고, 실제 프로젝트에서 효율적으로 생성하고 중첩하는 고급 기법을 다룹니다.
먼저 layout.tsx가 어떤 UI를 유지하고, 어떤 콘텐츠를 children으로 넘기는지 확인해 봅니다.
레이아웃은 공통 UI를 복사하지 않게 하고, 하위 route가 들어올 children 자리를 고정해 페이지 이동 중 유지될 부분을 결정한다.
| 확인 축 | layout.tsx의 책임 | children의 의미 | 주의할 점 |
|---|---|---|---|
| 공통 UI | 헤더, 사이드바, 푸터를 한곳에 둠 | 하위 page가 들어갈 자리 | page마다 반복 UI를 복사하지 않기 |
| 중첩 범위 | 해당 폴더와 모든 하위 route에 적용 | 다음 layout 또는 page가 들어옴 | 어느 세그먼트부터 공유할지 결정 |
| 상태 유지 | 이동해도 유지될 UI를 감쌈 | 바뀌는 부분만 children으로 교체 | 유지되면 안 되는 상태를 너무 위에 두지 않기 |
| 서버 기본값 | 기본적으로 서버 컴포넌트 | 서버에서 만든 구조 안에 하위 UI 삽입 | 이벤트가 필요하면 client provider 분리 |
| 성능 | 변하지 않는 구조를 재사용 | 페이지별 콘텐츠만 교체 | 데이터와 캐시 범위를 넓히기 전 검토 |
레이아웃 컴포넌트의 본질과 역할
레이아웃 컴포넌트는 app 디렉터리 내의 특정 라우트 세그먼트 폴더 안에 위치한 layout.tsx 파일입니다.
이 파일은 해당 라우트와 그 모든 하위 라우트에 적용되는 공통 UI를 정의합니다.
레이아웃의 핵심 역할- UI 재사용 및 일관성 유지: 헤더, 푸터, 사이드바, 내비게이션 바 등 웹사이트의 여러 부분에서 반복되는 UI 요소를 한 곳에서 관리하여 코드 중복을 방지하고 디자인 일관성을 유지합니다.
- 상태 유지: 페이지 이동 시 레이아웃 컴포넌트는 리렌더링되지 않고 상태를 유지합니다. 이는 SPA(Single Page Application)의 장점을 살려 부드러운 사용자 경험을 제공하는 데 기여합니다.
- 서버 컴포넌트 기본값:
layout.tsx파일은 기본적으로 서버 컴포넌트로 동작합니다. 따라서 레이아웃에서 데이터를 페칭하거나, 서버 전용 로직을 안전하게 실행할 수 있습니다. - 성능 최적화: 레이아웃이 서버에서 렌더링되면서 초기 로딩 시 HTML을 빠르게 전달하고, 변경되지 않는 부분은 캐싱하여 불필요한 클라이언트 사이드 번들 크기를 줄입니다.
주의: 모든 레이아웃 컴포넌트는 반드시 children이라는 prop을 받아야 합니다.
이 children prop을 통해 하위 레이아웃 또는 최종 페이지 컴포넌트의 콘텐츠가 렌더링됩니다.
// 모든 레이아웃 컴포넌트의 기본 형태
export default function MyLayout({ children }: { children: React.ReactNode }) {
return (
<>
{/* 레이아웃에 고유한 공통 UI */}
<nav>공통 내비게이션</nav>
{children} {/* 여기에 하위 콘텐츠가 렌더링됩니다 */}
<footer>공통 푸터</footer>
</>
);
}레이아웃 중첩의 작동 방식
아래 다이어그램은 폴더가 깊어질수록 layout.tsx가 바깥에서 안쪽으로 감싸지는 순서를 보여줍니다.
중첩 레이아웃은 수동으로 import해서 감싸는 구조가 아니라, 폴더의 layout.tsx가 부모 layout의 children 자리에 차례로 들어가는 구조다.
| URL | 렌더 순서 | 각 계층의 역할 | 바뀌는 부분 |
|---|---|---|---|
| /dashboard | RootLayout > DashboardLayout > DashboardPage | 전역 껍데기와 대시보드 공통 UI | DashboardPage |
| /dashboard/profile | RootLayout > DashboardLayout > ProfileLayout > ProfilePage | 프로필 섹션 메뉴 추가 | ProfilePage |
| /dashboard/profile/edit | RootLayout > DashboardLayout > ProfileLayout > EditPage | 프로필 레이아웃 유지 | EditPage |
| /products/42 | RootLayout > ProductPage | 별도 layout이 없으면 바로 page | ProductPage |
| layout.tsx 없음 | 가장 가까운 부모 layout 사용 | 범위가 좁아짐 | 공통 UI가 적용되지 않을 수 있음 |
App Router에서 레이아웃은 폴더 구조에 따라 자동으로 중첩됩니다.
각 라우트 세그먼트(폴더)에 layout.tsx 파일이 있으면, 해당 폴더의 레이아웃은 그 부모 폴더의 레이아웃 안에 중첩되어 렌더링됩니다.
위 구조에서 /dashboard/settings 경로로 접근하면, 다음과 같은 순서로 레이아웃이 중첩되어 최종 페이지가 렌더링됩니다.
RootLayout: 최상위 HTML 구조(<html>, <body>)를 제공하고, 전역 스타일 등을 포함합니다.
이 레이아웃의 children으로 다음 레이아웃이 들어갑니다.
DashboardLayout: 대시보드 섹션에 특화된 UI(예: 사이드바)를 제공합니다.
이 레이아웃의 children으로 다음 레이아웃/페이지가 들어갑니다.
SettingsLayout (선택 사항): 만약 settings 폴더에 레이아웃이 있다면, 이 레이아웃이 추가로 중첩되어 특정 설정 페이지에만 적용되는 UI를 제공합니다.
이 레이아웃의 children으로 최종 페이지가 들어갑니다.
SettingsPage: 마지막으로 settings 폴더의 page.tsx가 렌더링되어 최종 콘텐츠를 표시합니다.
이러한 계층적 중첩은 애플리케이션의 복잡성에 따라 유연하게 UI를 구성할 수 있게 해줍니다.
레이아웃 컴포넌트 생성 실습
이전에 만들었던 대시보드 예제를 바탕으로, 특정 하위 페이지에만 적용되는 중첩 레이아웃을 추가해 보겠습니다.
목표:
/dashboard/profile 경로에 사용자 프로필을 표시하는 페이지를 만들고, 이 페이지에만 적용되는 특정 레이아웃을 추가하여 사이드 메뉴를 더 세분화합니다.
src/app/dashboard/profile 폴더 생성:
src/app/dashboard 폴더 안에 profile이라는 새 폴더를 만듭니다.
src/app/dashboard/profile/layout.tsx 파일 생성:
profile 폴더 안에 layout.tsx 파일을 생성하고 다음 내용을 작성합니다.
import Link from 'next/link';
export default function ProfileLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div style={{ display: 'flex', gap: '20px', padding: '15px', border: '1px dashed orange', borderRadius: '5px', marginTop: '15px' }}>
<aside style={{ width: '150px', backgroundColor: '#fffbe6', padding: '10px', borderRadius: '5px' }}>
<h4>프로필 메뉴</h4>
<ul style={{ listStyle: 'none', padding: 0 }}>
<li style={{ marginBottom: '5px' }}>
<Link href="/dashboard/profile">기본 정보</Link>
</li>
<li style={{ marginBottom: '5px' }}>
<Link href="/dashboard/profile/edit">프로필 편집</Link>
</li>
<li style={{ marginBottom: '5px' }}>
<Link href="/dashboard/profile/security">보안 설정</Link>
</li>
</ul>
</aside>
<section style={{ flexGrow: 1, backgroundColor: '#fff', padding: '10px' }}>
{children} {/* 프로필 관련 페이지 콘텐츠가 여기에 들어옵니다 */}
</section>
</div>
);
}- 이
ProfileLayout은 주황색 점선 테두리로 표시되어 시각적으로 구분됩니다. ProfileLayout은DashboardLayout안에 중첩되어 렌더링될 것입니다. 즉,RootLayout>DashboardLayout>ProfileLayout>page.tsx순으로 중첩됩니다.
src/app/dashboard/profile/page.tsx 파일 생성:
profile 폴더 안에 page.tsx 파일을 생성하고 기본 프로필 정보를 표시하는 내용을 작성합니다.
export default function ProfileHomePage() {
return (
<div>
<h3>내 프로필</h3>
<p>이곳은 사용자 프로필의 기본 정보를 표시하는 페이지입니다.</p>
<ul>
<li>이름: 홍길동</li>
<li>이메일: hong.gildong@example.com</li>
</ul>
</div>
);
}src/app/dashboard/profile/edit/page.tsx 및 src/app/dashboard/profile/security/page.tsx 생성:
src/app/dashboard/profile 안에 각각 edit 폴더와 security 폴더를 만들고, 그 안에 page.tsx 파일을 생성합니다.
-
src/app/dashboard/profile/edit/page.tsxsrc/app/dashboard/profile/edit/page.tsx export default function ProfileEditPage() { return ( <div> <h3>프로필 편집</h3> <p>여기서 사용자 정보를 편집할 수 있습니다.</p> {/* 실제로는 폼 컴포넌트가 들어갈 자리 */} </div> ); } -
src/app/dashboard/profile/security/page.tsxsrc/app/dashboard/profile/security/page.tsx // src/app/dashboard/profile/security/page.tsx export default function ProfileSecurityPage() { return ( <div> <h3>보안 설정</h3> <p>비밀번호 변경, 2단계 인증 등의 보안 설정을 할 수 있습니다.</p> </div> ); }
DashboardLayout에 프로필 링크 추가 (선택 사항):
src/app/dashboard/layout.tsx 파일의 사이드바에 프로필 링크를 추가하여 접근성을 높일 수 있습니다.
// ...
<ul style={{ listStyle: 'none', padding: 0 }}>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard">대시보드 홈</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/overview">개요</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/profile">프로필</Link> {/* 새 링크 추가 */}
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/analytics">분석</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/settings">설정</Link>
</li>
</ul>
// ...실습 확인:
개발 서버(npm run dev)를 실행한 후, 다음 URL들을 방문하며 레이아웃 중첩을 확인해 보세요.
http://localhost:3000/dashboard(루트 + 대시보드 레이아웃)http://localhost:3000/dashboard/profile(루트 + 대시보드 + 프로필 레이아웃 + 프로필 홈 페이지)http://localhost:3000/dashboard/profile/edit(루트 + 대시보드 + 프로필 레이아웃 + 프로필 편집 페이지)
프로필 관련 페이지들에서만 ProfileLayout의 주황색 점선 테두리와 내부 메뉴가 보이는 것을 확인할 수 있습니다.
이는 레이아웃이 폴더 구조에 따라 정확히 중첩되어 적용되고 있음을 의미합니다.
레이아웃에서 데이터 공유
레이아웃은 서버 컴포넌트이므로, 해당 레이아웃이 적용되는 모든 하위 컴포넌트(다른 레이아웃, 페이지 컴포넌트)에서 공통적으로 필요한 데이터를 레이아웃에서 미리 페칭할 수 있습니다.
예를 들어, DashboardLayout에서 사용자 이름을 페칭하여 사이드바와 하위 페이지에서 모두 사용할 수 있습니다.
import Link from 'next/link';
// 데이터 페칭 함수 (서버에서 실행)
async function getUserInfo() {
// 실제로는 DB에서 사용자 정보를 가져오거나 API 호출
// 예시를 위해 딜레이 추가
await new Promise(resolve => setTimeout(resolve, 500));
return { username: '김넥스트', email: 'next@example.com' };
}
export default async function DashboardLayout({ // async 키워드 추가
children,
}: {
children: React.ReactNode;
}) {
const userInfo = await getUserInfo(); // 서버에서 사용자 정보 페칭
return (
<div style={{ display: 'flex', minHeight: 'calc(100vh - 180px)', border: '1px solid #ccc', borderRadius: '8px', overflow: 'hidden' }}>
<aside style={{ width: '200px', padding: '20px', backgroundColor: '#f9f9f9', borderRight: '1px solid #eee' }}>
<h2 style={{ marginBottom: '15px', color: '#333' }}>대시보드 메뉴</h2>
<p>환영합니다, {userInfo.username}님!</p> {/* 가져온 데이터 사용 */}
<ul style={{ listStyle: 'none', padding: 0 }}>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard">대시보드 홈</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/overview">개요</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/profile">프로필</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/analytics">분석</Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/settings">설정</Link>
</li>
</ul>
</aside>
<section style={{ flexGrow: 1, padding: '20px', backgroundColor: '#fff' }}>
{children}
</section>
</div>
);
}DashboardLayout에서 userInfo를 페칭하면, 이 정보가 해당 레이아웃 내의 모든 자식 컴포넌트(프로필 레이아웃, 대시보드 페이지 등)에 직접 전달되지는 않습니다.
대신 Next.js의 데이터 캐싱 메커니즘 덕분에, 하위 컴포넌트에서 동일 데이터를 다시 요청해도 네트워크 요청 없이 빠르게 재사용할 수 있습니다. (이 부분은 데이터 페칭 장에서 더 자세히 다룹니다.)
레이아웃 컴포넌트의 중첩은 복잡한 웹 애플리케이션의 UI를 모듈화하고, 효율적으로 관리하며, 사용자 경험을 향상시키는 데 매우 중요합니다.
이 개념을 잘 이해하고 적용한다면, 유지보수하기 쉽고 성능 좋은 Next.js 애플리케이션을 구축할 수 있을 것입니다.
실제 프로젝트에서는 루트 레이아웃, 세그먼트별 레이아웃, children 슬롯, 메타데이터와 프로바이더의 위치를 함께 보며 어느 계층에 공통 UI와 설정을 둘지 결정합니다.
레이아웃 계층을 설계할 때는 UI가 유지되어야 하는 범위와 바뀌어야 하는 범위를 먼저 나눠야 한다.
| 둘 위치 | 적합한 내용 | 예시 | 잘못 두면 |
|---|---|---|---|
| RootLayout | html/body, 전역 폰트, 최상위 provider | app/layout.tsx | 전역이 아닌 UI까지 모두 감쌈 |
| Section Layout | 대시보드 사이드바, 마케팅 헤더 | app/dashboard/layout.tsx | 섹션 이동 때 유지되어야 할 UI가 사라짐 |
| Nested Layout | 프로필 전용 메뉴, 설정 탭 | app/dashboard/profile/layout.tsx | 너무 위에 두면 다른 페이지까지 오염 |
| Page | 해당 URL의 본문 | profile/page.tsx | 공통 UI를 page마다 반복 |
| Client Provider | 상호작용 상태, 테마 토글 | 작은 client wrapper | layout 전체가 클라이언트 번들로 커짐 |
레이아웃을 여러 겹으로 나눌수록 중요한 것은 “어디에 둘 것인가”입니다.
유지되어야 하는 UI, 범위가 좁은 설정, 캐시되는 데이터, 클라이언트 프로바이더의 위치를 계층별로 분리해 두면 중첩 구조가 복잡해져도 책임이 흐려지지 않습니다.
공유 UI, 데이터, provider, 상태가 어느 URL 범위에서 유지되어야 하는지에 따라 파일 위치가 달라진다.
| 질문 | 예라면 | 둘 위치 | 검증 기준 |
|---|---|---|---|
| 여러 하위 페이지가 같은 UI를 쓰는가 | 대시보드 메뉴 | dashboard/layout.tsx | 하위 이동 때 메뉴가 유지됨 |
| 특정 하위 섹션에서만 쓰는가 | profile 메뉴 | dashboard/profile/layout.tsx | 다른 dashboard 페이지에는 나타나지 않음 |
| 데이터가 모든 하위 페이지에 필요하지만 직접 전달하지 않아도 되는가 | 사용자 기본 정보 | 상위 layout fetch | fetch 캐시 재사용 가능 |
| 상호작용 상태가 필요한가 | 접힘 사이드바 | Client Component wrapper | layout 전체에 use client를 붙이지 않음 |
| 페이지마다 달라지는 본문인가 | 프로필 홈/편집/보안 | 각 page.tsx | layout children 자리만 교체됨 |
레이아웃 계층을 설계할 때는 오래 유지될 UI와 좁은 세그먼트 책임, 서버 데이터 경계, 클라이언트 프로바이더 위치를 분리해서 보면 변경 범위가 작아집니다.
레이아웃을 잘 나누면 한 페이지를 고쳐도 전역 구조가 흔들리지 않고, 섹션별 설정과 데이터 경계가 명확해진다.
| 계층 | 유지되는 것 | 바뀌는 것 | 변경 영향 |
|---|---|---|---|
| RootLayout | html/body, 전역 provider | 모든 children | 전체 앱에 영향 |
| DashboardLayout | 사이드바, 사용자 요약 | dashboard 아래 page | 대시보드 섹션만 영향 |
| ProfileLayout | 프로필 메뉴, 탭 | profile/edit/security page | 프로필 섹션만 영향 |
| Page | 해당 URL 본문 | 없음 | 단일 화면 영향 |
| Client Island | 작은 상호작용 상태 | 서버 layout 구조 밖 이벤트 | 번들 범위를 작게 유지 |
레이아웃 컴포넌트 생성 및 중첩 적용 전에는 서버/클라이언트 경계, 캐싱 조건, 배포 영향을 함께 확인해야 합니다.
겉보기로 UI가 공유되어도 children 누락, 과한 use client, 넓은 데이터 fetch가 있으면 유지보수성과 성능이 떨어진다.
| 점검 항목 | 좋은 신호 | 나쁜 신호 | 수정 방향 |
|---|---|---|---|
| children | 항상 children을 렌더 | children 누락으로 page가 안 보임 | layout 기본 형태 확인 |
| Link 문법 | Link가 직접 텍스트를 감쌈 | Link 안에 a 중첩 | 현재 App Router 문법으로 수정 |
| 서버/클라이언트 | layout은 서버, 작은 wrapper만 client | layout 전체 use client | provider를 하위 client 컴포넌트로 분리 |
| 데이터 범위 | 필요한 계층에서 fetch | RootLayout에서 섹션 데이터 fetch | 사용 범위가 좁은 layout로 내림 |
| 중첩 깊이 | 책임별로 명확히 분리 | 레이아웃이 너무 잘게 쪼개짐 | 유지되는 UI가 있을 때만 추가 |
마지막으로 layout.tsx 중첩이 공유 UI, children, 데이터 공유 책임을 어떻게 나누는지 확인합니다.
레이아웃 중첩을 이해하려면 각 계층이 무엇을 유지하고 무엇을 children으로 넘기는지 함께 봐야 한다.
| 책임 | layout.tsx | page.tsx | 설계 기준 |
|---|---|---|---|
| 공유 UI | 공통 껍데기 렌더 | 고유 본문 렌더 | 여러 경로가 공유하면 layout |
| children | 하위 layout/page 삽입 | children을 받지 않음 | 교체되는 영역을 명확히 둠 |
| 데이터 | 공통 데이터 fetch 가능 | 화면별 데이터 fetch | 데이터 사용 범위에 맞춤 |
| 상태 유지 | 이동 중 유지될 UI 감쌈 | 페이지 전환 때 교체 | 유지되면 좋은가를 먼저 판단 |
| 클라이언트 기능 | 작은 client wrapper 포함 | 상호작용 컴포넌트 배치 | use client 범위를 최소화 |