레이아웃 컴포넌트 사용하기
Next.js App Router의 가장 강력하고 유용한 기능 중 하나는 바로 레이아웃(Layouts) 입니다. 레이아웃은 여러 페이지에서 공유되는 UI를 정의하는 방법을 제공하며, 이는 코드 중복을 줄이고 일관된 사용자 경험을 유지하는 데 필수적입니다. 이전 절에서 layout.tsx
파일을 잠깐 언급했지만, 이번 절에서는 레이아웃의 개념을 더 깊이 이해하고 실제 프로젝트에 적용하는 방법을 상세히 알아보겠습니다.
레이아웃이란 무엇인가요?
레이아웃은 특정 라우트 세그먼트와 그 하위 라우트에서 공유되는 UI를 래핑하는(감싸는) React 컴포넌트입니다. 이는 웹사이트의 헤더, 푸터, 사이드바, 내비게이션 바 등 여러 페이지에 걸쳐 동일하게 나타나는 부분들을 한 곳에서 관리할 수 있게 해줍니다.
레이아웃의 주요 특징
- 공유 UI: 레이아웃 내부에 정의된 UI는 해당 레이아웃이 적용되는 모든 하위 페이지에 자동으로 포함됩니다.
- 중첩 가능: App Router에서는 레이아웃을 중첩하여 사용할 수 있습니다. 최상위 레이아웃(Root Layout)부터 특정 라우트 세그먼트에만 적용되는 하위 레이아웃까지 계층적으로 구성할 수 있습니다.
- 상태 유지: 레이아웃 컴포넌트는 라우트 이동 시에도 상태(State)를 유지합니다. 즉, 레이아웃 내부의 클라이언트 컴포넌트 상태는 페이지가 변경되어도 그대로 유지됩니다. 이는
template.tsx
와 가장 큰 차이점입니다. - 기본적으로 서버 컴포넌트:
layout.tsx
파일은 기본적으로 서버 컴포넌트로 동작합니다.
최상위 레이아웃 (RootLayout
) 이해하기
Next.js 프로젝트를 생성하면 src/app/layout.tsx
파일이 자동으로 생성됩니다. 이 파일은 애플리케이션의 최상위 레이아웃(Root Layout) 으로, 모든 페이지에 적용되는 가장 기본적인 UI 구조를 정의합니다.
// src/app/layout.tsx
import './globals.css'; // 전역 스타일 임포트
export default function RootLayout({
children, // 필수 prop: 모든 하위 페이지 및 중첩 레이아웃이 여기에 렌더링됩니다.
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body>
<header style={{ backgroundColor: '#f0f0f0', padding: '10px' }}>
<nav>
<a href="/">홈</a> | <a href="/about">소개</a> | <a href="/dashboard">대시보드</a>
</nav>
<h1>나 혼자 Next.js</h1>
</header>
{children} {/* 여기에 페이지 또는 하위 레이아웃 콘텐츠가 들어옵니다 */}
<footer style={{ backgroundColor: '#e0e0e0', padding: '10px', marginTop: '20px' }}>
<p>© 2024 나 혼자 Next.js. All rights reserved.</p>
</footer>
</body>
</html>
);
}
설명
children
prop: 이 레이아웃 내부에 렌더링될 하위 콘텐츠(다른 레이아웃 또는 최종page.tsx
파일)를 나타냅니다. 모든 레이아웃 컴포넌트는 이children
prop을 받아야 합니다.<html>
,<body>
태그: 최상위 레이아웃은 반드시<html>
과<body>
태그를 포함해야 합니다. 이는 웹 페이지의 기본 구조를 형성합니다.- 공통 UI 추가: 예시에서는 간단한
<header>
와<footer>
를 추가했습니다. 이 부분은 어떤 페이지로 이동하든 항상 동일하게 표시됩니다.
실습
위 코드를 src/app/layout.tsx
파일에 붙여넣고 개발 서버(npm run dev
)를 확인해 보세요. 어떤 페이지로 이동하든 상단에 헤더와 하단에 푸터가 항상 표시되는 것을 확인할 수 있습니다.
중첩 레이아웃 (Nested Layouts
) 사용하기
특정 라우트 세그먼트에만 적용되는 별도의 레이아웃을 정의할 수도 있습니다. 이것이 바로 중첩 레이아웃입니다. 예를 들어, 대시보드 페이지와 그 하위 페이지들(dashboard/settings
, dashboard/profile
등)에는 공통된 사이드바가 필요할 수 있습니다.
-
dashboard
폴더에 레이아웃 파일 추가:src/app/dashboard
폴더 안에layout.tsx
파일을 생성합니다.my-next-app/ └── src/ └── app/ ├── about/ ├── dashboard/ │ ├── layout.tsx <- 여기에 새 파일 생성 │ ├── page.tsx │ └── settings/ ├── layout.tsx └── page.tsx
-
src/app/dashboard/layout.tsx
파일 내용 작성src/app/dashboard/layout.tsx // src/app/dashboard/layout.tsx export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { return ( <section style={{ display: 'flex', border: '1px solid #ccc', padding: '10px', marginTop: '10px' }}> <aside style={{ width: '200px', padding: '10px', backgroundColor: '#f9f9f9', borderRight: '1px solid #eee' }}> <h2>대시보드 메뉴</h2> <ul> <li><a href="/dashboard">대시보드 홈</a></li> <li><a href="/dashboard/settings">설정</a></li> {/* 추가 메뉴 아이템 */} </ul> </aside> <main style={{ flexGrow: 1, padding: '10px' }}> {children} {/* 대시보드 하위 페이지 콘텐츠가 여기에 렌더링됩니다 */} </main> </section> ); }
설명
DashboardLayout
은section
태그로 전체를 감싸고, 왼쪽에 사이드바(aside
)와 오른쪽에 메인 콘텐츠 영역(main
)을 가집니다.- 이 레이아웃 역시
children
prop을 받아,/dashboard
경로 또는 그 하위 경로의page.tsx
콘텐츠가main
태그 내부에 렌더링되도록 합니다.
실습
src/app/dashboard/layout.tsx
파일을 생성하고 위 코드를 붙여넣은 후, 개발 서버를 확인해 보세요.
http://localhost:3000/
: 루트 레이아웃만 적용됩니다.http://localhost:3000/about
: 루트 레이아웃만 적용됩니다.http://localhost:3000/dashboard
: 루트 레이아웃 안에DashboardLayout
이 중첩되어 적용되고, 그 안에/dashboard
의page.tsx
내용이 렌더링됩니다. 즉, 헤더-사이드바-대시보드 콘텐츠-푸터 순서로 보일 것입니다.http://localhost:3000/dashboard/settings
: 마찬가지로 루트 레이아웃 안에DashboardLayout
이 중첩되어 적용되고, 그 안에/dashboard/settings
의page.tsx
내용이 렌더링됩니다.
레이아웃의 데이터 페칭
레이아웃은 기본적으로 서버 컴포넌트이므로, 서버에서 데이터를 미리 가져와 UI를 구성하는 것이 가능합니다. 이는 해당 레이아웃이 적용되는 모든 페이지에서 공통적으로 필요한 데이터를 효율적으로 로드하는 데 유용합니다.
// src/app/dashboard/layout.tsx (데이터 페칭 예시)
import { Suspense } from 'react'; // 로딩 상태 관리를 위한 Suspense 임포트
// 서버 컴포넌트에서 데이터 페칭
async function getSharedData() {
// 실제 API 호출 로직
const response = await fetch('https://api.example.com/shared-dashboard-info', {
cache: 'no-store' // 캐시 사용 여부 설정 (필요에 따라)
});
if (!response.ok) {
// 에러 처리
throw new Error('데이터를 가져오지 못했습니다.');
}
return response.json();
}
export default async function DashboardLayout({ // async 키워드 추가
children,
}: {
children: React.ReactNode;
}) {
const sharedData = await getSharedData(); // 데이터 호출 (서버에서 실행)
return (
<section style={{ display: 'flex', border: '1px solid #ccc', padding: '10px', marginTop: '10px' }}>
<aside style={{ width: '200px', padding: '10px', backgroundColor: '#f9f9f9', borderRight: '1px solid #eee' }}>
<h2>대시보드 메뉴</h2>
<p>공유 데이터: {sharedData.message}</p> {/* 가져온 데이터 사용 */}
<ul>
<li><a href="/dashboard">대시보드 홈</a></li>
<li><a href="/dashboard/settings">설정</a></li>
</ul>
</aside>
<main style={{ flexGrow: 1, padding: '10px' }}>
{/* Suspense를 사용하여 children 로딩 중 대체 UI 표시 가능 */}
<Suspense fallback={<div>페이지 로딩 중...</div>}>
{children}
</Suspense>
</main>
</section>
);
}
참고: Next.js 15에서는 fetch
함수가 자동으로 요청을 캐싱하는 기능이 포함되어 있습니다. 동일한 데이터가 여러 레이아웃이나 페이지에서 요청될 때, 한 번만 네트워크 요청을 보내도록 최적화됩니다.
레이아웃 컴포넌트를 사용하면 애플리케이션의 UI 구조를 체계적으로 관리하고, 코드의 재사용성을 높이며, 일관된 사용자 경험을 제공하는 데 큰 도움이 됩니다. 중첩 레이아웃을 통해 복잡한 UI도 효율적으로 구성할 수 있습니다.
이제 레이아웃 컴포넌트의 중요성과 활용법에 대해 충분히 이해하셨으리라 생각합니다.