Per-page Layouts

작성일: 2025. 10. 08최종 수정: 2025. 10. 09. 15시 01분

Next.js에서 페이지마다 다른 레이아웃을 적용하는 패턴이다. _app.tsx에서 모든 페이지에 동일한 레이아웃을 적용하는 대신, 각 페이지가 자신만의 레이아웃을 정의할 수 있다.

왜 필요한가?

일반적으로 _app.tsx에서 레이아웃을 적용하면 모든 페이지가 동일한 레이아웃을 사용한다.

typescript
// pages/_app.tsx
export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  );
}

하지만 실제 애플리케이션에서는 페이지마다 다른 레이아웃이 필요한 경우가 많다.

  • 홈페이지는 헤더만 있고 사이드바가 없다
  • 대시보드는 사이드바가 있다
  • 관리자 페이지는 다른 네비게이션을 사용한다
  • 로그인 페이지는 레이아웃이 아예 없다

Per-page Layouts 패턴을 사용하면 각 페이지가 필요한 레이아웃을 자유롭게 선택할 수 있다.

기본 구조

페이지 컴포넌트에 getLayout 메서드를 추가하고, _app.tsx에서 이를 호출하는 방식이다.

1. _app.tsx 설정

typescript
// 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. 레이아웃이 필요한 페이지

typescript
// 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. 레이아웃이 없는 페이지

typescript
// pages/login.tsx
export default function Login() {
  return (
    <div>
      <h1>로그인</h1>
      <form>{/* 로그인 폼 */}</form>
    </div>
  );
}

// getLayout을 정의하지 않으면 레이아웃 없이 페이지만 렌더링된다

getLayout을 정의하지 않으면 _app.tsx에서 (page) => page가 사용되어 페이지가 그대로 렌더링된다.

레이아웃 컴포넌트 작성

레이아웃 컴포넌트는 일반적인 React 컴포넌트다.

typescript
// 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으로 페이지 컴포넌트를 받아서 원하는 위치에 렌더링한다.

중첩 레이아웃

레이아웃 안에 또 다른 레이아웃을 중첩할 수 있다.

typescript
// 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의 중요한 장점은 페이지 전환 시 레이아웃의 상태가 유지된다는 점이다.

typescript
// 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 타입 정의

타입 안정성을 위해 페이지 타입을 확장할 수 있다.

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;
};
typescript
// 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} />);
}
typescript
// 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의 레이아웃을 함께 사용할 수도 있다.

typescript
// 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는 데이터 페칭 메서드와 함께 사용할 수 있다.

typescript
// 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;

데이터 페칭과 레이아웃 설정을 모두 같은 페이지 파일에서 처리할 수 있다.

참고 자료