Per-page Layouts
Next.js에서 페이지마다 다른 레이아웃을 적용하는 패턴이다. _app.tsx에서 모든 페이지에 동일한 레이아웃을 적용하는 대신, 각 페이지가 자신만의 레이아웃을 정의할 수 있다.
왜 필요한가?
일반적으로 _app.tsx에서 레이아웃을 적용하면 모든 페이지가 동일한 레이아웃을 사용한다.
// pages/_app.tsx
export default function App({ Component, pageProps }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
);
}
하지만 실제 애플리케이션에서는 페이지마다 다른 레이아웃이 필요한 경우가 많다.
- 홈페이지는 헤더만 있고 사이드바가 없다
- 대시보드는 사이드바가 있다
- 관리자 페이지는 다른 네비게이션을 사용한다
- 로그인 페이지는 레이아웃이 아예 없다
Per-page Layouts 패턴을 사용하면 각 페이지가 필요한 레이아웃을 자유롭게 선택할 수 있다.
기본 구조
페이지 컴포넌트에 getLayout 메서드를 추가하고, _app.tsx에서 이를 호출하는 방식이다.
1. _app.tsx 설정
// pages/_app.tsx
import type { AppProps } from 'next/app';
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
export default function App({ Component, pageProps }: AppPropsWithLayout) {
// getLayout이 있으면 사용, 없으면 페이지 그대로 렌더링
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(<Component {...pageProps} />);
}
위 코드에서 Component.getLayout이 있는지 확인한다. 있으면 해당 함수를 호출하여 페이지를 레이아웃으로 감싸고, 없으면 페이지를 그대로 렌더링한다.
?? 연산자는 왼쪽 값이 null 또는 undefined일 때 오른쪽 값을 사용한다.
2. 레이아웃이 필요한 페이지
// pages/dashboard.tsx
import { ReactElement } from 'react';
import DashboardLayout from '@/components/DashboardLayout';
export default function Dashboard() {
return (
<div>
<h1>대시보드</h1>
<p>통계와 차트가 여기에 표시된다</p>
</div>
);
}
Dashboard.getLayout = function getLayout(page: ReactElement) {
return <DashboardLayout>{page}</DashboardLayout>;
};
페이지 컴포넌트에 getLayout 메서드를 추가한다. 이 메서드는 페이지를 받아서 레이아웃으로 감싼 결과를 반환한다.
3. 레이아웃이 없는 페이지
// pages/login.tsx
export default function Login() {
return (
<div>
<h1>로그인</h1>
<form>{/* 로그인 폼 */}</form>
</div>
);
}
// getLayout을 정의하지 않으면 레이아웃 없이 페이지만 렌더링된다
getLayout을 정의하지 않으면 _app.tsx에서 (page) => page가 사용되어 페이지가 그대로 렌더링된다.
레이아웃 컴포넌트 작성
레이아웃 컴포넌트는 일반적인 React 컴포넌트다.
// components/DashboardLayout.tsx
import Link from 'next/link';
import { ReactNode } from 'react';
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div>
<nav>
<Link href="/dashboard">대시보드</Link>
<Link href="/dashboard/analytics">분석</Link>
<Link href="/dashboard/settings">설정</Link>
</nav>
<main>{children}</main>
</div>
);
}
children prop으로 페이지 컴포넌트를 받아서 원하는 위치에 렌더링한다.
중첩 레이아웃
레이아웃 안에 또 다른 레이아웃을 중첩할 수 있다.
// pages/dashboard/analytics.tsx
import { ReactElement } from 'react';
import DashboardLayout from '@/components/DashboardLayout';
import AnalyticsLayout from '@/components/AnalyticsLayout';
export default function Analytics() {
return <div>분석 페이지 내용</div>;
}
Analytics.getLayout = function getLayout(page: ReactElement) {
return (
<DashboardLayout>
<AnalyticsLayout>{page}</AnalyticsLayout>
</DashboardLayout>
);
};
위 코드는 DashboardLayout 안에 AnalyticsLayout을 중첩한다. 페이지 전환 시 DashboardLayout의 상태는 유지되고 AnalyticsLayout만 교체된다.
레이아웃 상태 유지
Per-page Layouts의 중요한 장점은 페이지 전환 시 레이아웃의 상태가 유지된다는 점이다.
// components/DashboardLayout.tsx
import { useState } from 'react';
import Link from 'next/link';
export default function DashboardLayout({ children }: { children: ReactNode }) {
const [sidebarOpen, setSidebarOpen] = useState(true);
return (
<div>
<button onClick={() => setSidebarOpen(!sidebarOpen)}>
사이드바 토글
</button>
{sidebarOpen && (
<nav>
<Link href="/dashboard">대시보드</Link>
<Link href="/dashboard/analytics">분석</Link>
</nav>
)}
<main>{children}</main>
</div>
);
}
사용자가 사이드바를 닫은 상태에서 /dashboard에서 /dashboard/analytics로 이동해도 사이드바는 닫힌 상태로 유지된다. 같은 레이아웃을 사용하는 페이지 간 이동에서는 레이아웃이 언마운트되지 않기 때문이다.
TypeScript 타입 정의
타입 안정성을 위해 페이지 타입을 확장할 수 있다.
// types/page.ts
import type { NextPage } from 'next';
import type { ReactElement, ReactNode } from 'react';
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
// pages/_app.tsx
import type { AppProps } from 'next/app';
import type { NextPageWithLayout } from '@/types/page';
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(<Component {...pageProps} />);
}
// pages/dashboard.tsx
import type { NextPageWithLayout } from '@/types/page';
import DashboardLayout from '@/components/DashboardLayout';
const Dashboard: NextPageWithLayout = () => {
return <div>대시보드</div>;
};
Dashboard.getLayout = function getLayout(page) {
return <DashboardLayout>{page}</DashboardLayout>;
};
export default Dashboard;
타입을 정의하면 getLayout 메서드의 시그니처가 자동완성되고 타입 체크가 된다.
주의사항
getLayout은 Next.js 기능이 아니다
getLayout은 Next.js가 제공하는 API가 아니라 개발자가 만드는 패턴이다. JavaScript에서 함수도 객체이므로 함수 컴포넌트에 프로퍼티를 추가할 수 있다.
이름도 getLayout 대신 withLayout, customLayout 등 원하는 대로 정할 수 있다. 다만 Next.js 공식 문서에서 getLayout이라는 이름으로 소개했기 때문에 이것이 관례가 되었다.
_app.tsx 레이아웃과 함께 사용
Per-page Layouts와 _app.tsx의 레이아웃을 함께 사용할 수도 있다.
// pages/_app.tsx
export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return (
<GlobalLayout>
{getLayout(<Component {...pageProps} />)}
</GlobalLayout>
);
}
위 코드는 모든 페이지에 GlobalLayout을 적용하고, 추가로 각 페이지의 getLayout도 적용한다.
getStaticProps, getServerSideProps와 함께 사용
Per-page Layouts는 데이터 페칭 메서드와 함께 사용할 수 있다.
// pages/posts/[id].tsx
import type { NextPageWithLayout } from '@/types/page';
import { GetStaticProps, GetStaticPaths } from 'next';
import BlogLayout from '@/components/BlogLayout';
type Props = {
post: {
id: string;
title: string;
content: string;
};
};
const Post: NextPageWithLayout<Props> = ({ post }) => {
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
};
Post.getLayout = function getLayout(page) {
return <BlogLayout>{page}</BlogLayout>;
};
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
// 데이터 페칭 로직
const post = await fetchPost(params?.id as string);
return { props: { post } };
};
export const getStaticPaths: GetStaticPaths = async () => {
// 경로 생성 로직
return { paths: [], fallback: 'blocking' };
};
export default Post;
데이터 페칭과 레이아웃 설정을 모두 같은 페이지 파일에서 처리할 수 있다.