레이아웃 컴포넌트 생성 및 중첩
Next.js App Router에서 레이아웃 컴포넌트는 단순히 UI를 공유하는 것을 넘어, 애플리케이션의 구조와 성능을 최적화하는 데 핵심적인 역할을 합니다. 3장에서 레이아웃의 기본적인 개념과 최상위 레이아웃, 중첩 레이아웃의 사용법을 간략히 살펴보았습니다. 이번 절에서는 레이아웃 컴포넌트를 더 심도 있게 이해하고, 실제 프로젝트에서 효율적으로 생성하고 중첩하는 고급 기법에 대해 알아보겠습니다.
레이아웃 컴포넌트의 본질과 역할
레이아웃 컴포넌트는 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>
</>
);
}
레이아웃 중첩의 작동 방식
App Router에서 레이아웃은 폴더 구조에 따라 자동으로 중첩됩니다. 각 라우트 세그먼트(폴더)에 layout.tsx
파일이 있으면, 해당 폴더의 레이아웃은 그 부모 폴더의 레이아웃 안에 중첩되어 렌더링됩니다.
예시: 중첩 레이아웃 계층
app/
├── layout.tsx # (1) RootLayout
├── dashboard/
│ ├── layout.tsx # (2) DashboardLayout
│ ├── page.tsx # (3) DashboardPage
│ └── settings/
│ ├── layout.tsx # (4) SettingsLayout (선택 사항)
│ └── page.tsx # (5) SettingsPage
└── products/
└── [id]/
└── page.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
이라는 새 폴더를 만듭니다.my-next-app/ └── src/ └── app/ ├── dashboard/ │ ├── layout.tsx # 기존 대시보드 레이아웃 │ ├── page.tsx │ ├── profile/ <- 여기에 새 폴더 생성 │ └── ... └── ...
-
src/app/dashboard/profile/layout.tsx
파일 생성:profile
폴더 안에layout.tsx
파일을 생성하고 다음 내용을 작성합니다.src/app/dashboard/profile/layout.tsx // src/app/dashboard/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"><a>기본 정보</a></Link> </li> <li style={{ marginBottom: '5px' }}> <Link href="/dashboard/profile/edit"><a>프로필 편집</a></Link> </li> <li style={{ marginBottom: '5px' }}> <Link href="/dashboard/profile/security"><a>보안 설정</a></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
파일을 생성하고 기본 프로필 정보를 표시하는 내용을 작성합니다.src/app/dashboard/profile/page.tsx // src/app/dashboard/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.tsx
src/app/dashboard/profile/edit/page.tsx // src/app/dashboard/profile/edit/page.tsx export default function ProfileEditPage() { return ( <div> <h3>프로필 편집</h3> <p>여기서 사용자 정보를 편집할 수 있습니다.</p> {/* 실제로는 폼 컴포넌트가 들어갈 자리 */} </div> ); }
src/app/dashboard/profile/security/page.tsx
:src/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
파일의 사이드바에 프로필 링크를 추가하여 접근성을 높일 수 있습니다.src/app/dashboard/layout.tsx // src/app/dashboard/layout.tsx (일부) // ... <ul style={{ listStyle: 'none', padding: 0 }}> <li style={{ marginBottom: '10px' }}> <Link href="/dashboard"><a>대시보드 홈</a></Link> </li> <li style={{ marginBottom: '10px' }}> <Link href="/dashboard/overview"><a>개요</a></Link> </li> <li style={{ marginBottom: '10px' }}> <Link href="/dashboard/profile"><a>프로필</a></Link> {/* 새 링크 추가 */} </li> <li style={{ marginBottom: '10px' }}> <Link href="/dashboard/analytics"><a>분석</a></Link> </li> <li style={{ marginBottom: '10px' }}> <Link href="/dashboard/settings"><a>설정</a></Link> </li> </ul> // ...
실습 확인:
개발 서버(npm run dev
)를 실행한 후, 다음 URL들을 방문하며 레이아웃 중첩을 확인해 보세요.
http://localhost:3000/dashboard
(루트 + 대시보드 레이아웃)http://localhost:3000/dashboard/profile
(루트 + 대시보드 + 프로필 레이아웃 + 프로필 홈 페이지)http://localhost:3000/dashboard/profile/edit
(루트 + 대시보드 + 프로필 레이아웃 + 프로필 편집 페이지)
프로필 관련 페이지들에서만 ProfileLayout
의 주황색 점선 테두리와 내부 메뉴가 보이는 것을 확인할 수 있습니다. 이는 레이아웃이 폴더 구조에 따라 정확히 중첩되어 적용되고 있음을 의미합니다.
레이아웃에서 데이터 공유
레이아웃은 서버 컴포넌트이므로, 해당 레이아웃이 적용되는 모든 하위 컴포넌트(다른 레이아웃, 페이지 컴포넌트)에서 공통적으로 필요한 데이터를 레이아웃에서 미리 페칭할 수 있습니다. 예를 들어, DashboardLayout
에서 사용자 이름을 페칭하여 사이드바와 하위 페이지에서 모두 사용할 수 있습니다.
// src/app/dashboard/layout.tsx (데이터 페칭 예시)
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"><a>대시보드 홈</a></Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/overview"><a>개요</a></Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/profile"><a>프로필</a></Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/analytics"><a>분석</a></Link>
</li>
<li style={{ marginBottom: '10px' }}>
<Link href="/dashboard/settings"><a>설정</a></Link>
</li>
</ul>
</aside>
<section style={{ flexGrow: 1, padding: '20px', backgroundColor: '#fff' }}>
{children}
</section>
</div>
);
}
DashboardLayout
에서 userInfo
를 페칭하면, 이 정보는 해당 레이아웃 내의 모든 자식 컴포넌트(프로필 레이아웃, 대시보드 페이지 등)에 직접적으로 전달되지는 않습니다. 대신, Next.js의 데이터 캐싱 메커니즘을 통해 동일한 데이터를 하위 컴포넌트에서 다시 요청해도 네트워크 요청 없이 빠르게 데이터를 재사용할 수 있습니다. (이 부분은 데이터 페칭 장에서 더 자세히 다룹니다.)
레이아웃 컴포넌트의 중첩은 복잡한 웹 애플리케이션의 UI를 모듈화하고, 효율적으로 관리하며, 사용자 경험을 향상시키는 데 매우 중요합니다. 이 개념을 잘 이해하고 적용한다면, 유지보수하기 쉽고 성능 좋은 Next.js 애플리케이션을 구축할 수 있을 것입니다.