처음 만난 Next.js 문서


3.2

Pages Router

Pages router는 Next.js 버전12까지 페이지 라우팅을 담당하던 Router입니다. 현재 최신 버전의 Next.js에서도 Pages Router를 사용할 수는 있지만, 바로 다음 절에 나오는 App Router를 사용하도록 권장하고 있습니다.

우리는 곧바로 App Router에 대해서 배우지 않고, Next.js가 발전해온 과정을 이해할 수 있도록 하기 위해서 먼저 Pages Router에 대해서 배우고 넘어가도록 하겠습니다. 지나간 기술을 이해하고 새로운 것을 배우는 것이 해당 기술을 제대로 이해하는 좋은 방법이기 때문입니다. 그리고 아직 이전 버전의 Next.js를 사용해서 서비스를 운영하고 있는 회사들이 있기 때문에, Pages Router에 대해서도 이해하는 것이 꼭 필요합니다.

3.2.1 Pages Router란?

Pages Router는 파일 시스템(File System)을 기반으로 한 라우터입니다. 이 말은 우리가 평소에 흔히 사용하는 폴더와 파일의 구조를 이용해서 라우팅을 처리한다고 이해하면 됩니다. 기존에 리액트만을 사용하는 애플리케이션에서는 React Router를 사용해서 개발자가 정의한 각 경로에 따라 다른 페이지를 보여주었습니다. 하지만 Next.js의 Pages Router에서는 폴더와 파일의 경로를 이용하여 각각의 경로를 지정하게 됩니다.

그래서 아래 그림과 같이 pages 폴더 내에 폴더와 파일을 중첩하여 경로를 구성하게 됩니다. 이렇게 폴더 구조를 사용해서 경로를 구성하는 방식이 처음에는 적응이 안 될 수도 있지만, 익숙해지게 되면 오히려 명확하고 시각적으로 경로들을 볼 수 있기 때문에 편리합니다.

pages/
├── index.js                 # → /
├── about.js                 # → /about
├── contact.js               # → /contact
├── blog/
│   ├── index.js            # → /blog
│   ├── [slug].js           # → /blog/:slug
│   └── categories/
│       ├── index.js        # → /blog/categories
│       └── [category].js   # → /blog/categories/:category
├── products/
│   ├── index.js            # → /products
│   ├── [id].js             # → /products/:id
│   └── [...slug].js        # → /products/* (catch-all route)
├── _app.js                 # App 컴포넌트
├── _document.js            # Document
├── 404.js                  # 커스텀 404 페이지
└── api/
    ├── hello.js            # → /api/hello
    └── users/
        ├── index.js        # → /api/users
        └── [id].js         # → /api/users/:id

3.2.2 Custom App

Next.js의 Pages Router에서는 페이지를 초기화하기 위해서 App 컴포넌트를 사용합니다. App 컴포넌트는 애플리케이션의 가장 최상위 컴포넌트라고 이해하면 됩니다. 그리고 App 컴포넌트를 커스터마이징 하면 페이지 초기화 이외에도 아래와 같은 기능들을 구현할 수 있습니다.

  • 각 페이지에 대한 공통 레이아웃 지정
  • 페이지로 추가적인 데이터 전달
  • 전역적으로 적용되는 global CSS 적용

커스텀 App 컴포넌트를 사용하기 위해서는 pages 디렉토리 내에 _app.tsx 파일을 아래와 같이 작성하면 됩니다.

// pages/_app.tsx

import type { AppProps } from 'next/app';
 
function MyApp(props: AppProps) {
    const { Component, pageProps } = props;

    return <Component {...pageProps} />;
}

export default MyApp;

만약 여기서 공통 레이아웃을 적용하고 싶을 경우에는 아래와 같이 해당 레이아웃 컴포넌트로 감싸주면 됩니다.

// pages/_app.tsx

import type { AppProps } from 'next/app';
import RootLayout from '@/components/RootLayout';

function MyApp(props: AppProps) {
    const { Component, pageProps } = props;

    return (
        <RootLayout>
            <Component {...pageProps} />
        </RootLayout>
    );
}

export default MyApp;

3.2.3 Custom Document

일반적인 SPA(Single Page Application)에는 단 하나의 HTML 파일이 존재하는 것과 다르게, Next.js 애플리케이션에는 index.html 파일이 존재하지 않습니다. 다만, index.html 파일이 완전히 존재하지 않는 것이 아니라 내부적으로 자동으로 생성된다고 보면 됩니다. 그리고 이러한 단일 페이지의 역할을 하는 것이 바로 Document 입니다. 그래서 Document를 커스터마이징 하면 페이지를 렌더링 할 때 사용되는 <html><body> 태그를 수정할 수 있습니다.

기본 Document를 덮어쓰기 위해서는 pages 디렉토리 내에 _document.tsx 파일을 아래와 같이 작성하면 됩니다.

// pages/_document.tsx

import { Html, Head, Main, NextScript } from 'next/document';
 
function Document() {
    return (
        <Html lang="en">
            <Head />
            <body>
                <Main />
                <NextScript />
            </body>
        </Html>
    );
}

export default Document;

커스텀 Document에서는 애플리케이션 전체에 걸쳐서 사용되는 폰트나 외부 스크립트 파일 등을 Script 컴포넌트를 사용해서 가져오도록 할 수 있습니다. 또한 일반적인 리액트 애플리케이션에서 index.html 파일에 작성해야 했던 코드들을 여기에 넣을 수 있다고 이해하면 됩니다.

3.2.4 Routes

지금부터는 Pages Router의 다양한 Routes에 대해 알아보도록 하겠습니다.

Index Routes

우리가 특정 웹사이트의 루트 경로로 방문했을 때 나오는 페이지를 일반적으로 Index 페이지라고 부릅니다. 그리고 Pages Router에서는 아래와 같이 pages 폴더 내에 각 폴더에 index.js 파일을 위치시킴으로써, 사용자가 각 경로에 접속했을 때 보여줄 페이지를 지정할 수 있습니다.

pages/index.js/로 접속했을 때 나옴
pages/blog/index.js/blog로 접속했을 때 나옴

아래 코드는 아주 간단한 형태의 Index 페이지 코드입니다. 여기에 페이지의 내용을 직접 작성해도 되고, 다른 컴포넌트를 import해서 사용해도 됩니다.

function IndexPage() {
    return <div>IndexPage</div>
}

export default IndexPage;

Nested Routes

웹사이트를 개발하다 보면 복잡한 형태의 경로를 구성해야 할 필요가 있습니다. Pages Router에서는 아래와 같이 pages 폴더 내에 폴더와 파일을 중첩시키는 형태로 복잡한 라우팅 경로를 구성할 수 있습니다.

pages/blog/first-post.js/blog/first-post
pages/dashboard/settings/username.js/dashboard/settings/username

즉, 복잡한 경로를 구성하려면 폴더 내에 폴더를 만들고, 또 그 안에 폴더를 만드는 형태로 개발하면 됩니다. 이러한 방식은 VS Code 같은 IDE의 왼쪽 파일 탐색기에서 경로들을 트리 형태로 볼 수 있어서 관리하기 편리하다고 할 수 있습니다.

Pages router directories

Dynamic Routes

우리가 웹사이트를 개발하다보면 동적으로 변하는 경로에 대응해야 할 경우가 있습니다. 이러한 경우를 위해 Pages Router에서는 아래와 같이 대괄호를 사용하는 형태로 동적으로 변하는 경로를 구성할 수 있습니다.

pages/posts/[postId].js/posts/1, /posts/2, /posts/3, ...
pages/users/[nickname].js/users/inje, /users/soaple, ...

여기서 대괄호 안에 있는 이름(postId, nickname)이 페이지로 전달될 쿼리 파라미터의 이름이 되며, 우리는 이 값을 사용해서 아래 예시 코드와 같이 동적으로 해당되는 페이지를 보여줄 수 있습니다.

// [postId].ts 파일 예시 코드

import type { GetServerSidePropsContext } from 'next';

export async function getServerSideProps(context: GetServerSidePropsContext) {
    const { postId } = context.query;

    const post = await fetchPostById(postId);

    return {
        props: {
            post,
        },
    };
}

3.2.5 Layout Pattern

지금부터는 Pages Router의 다양한 Layout Pattern에 대해 살펴보도록 하겠습니다.

HOC (Higher Order Component)

Pages Router에서는 여러 페이지들을 하나의 레이아웃으로 보여주기 위해서, 아래와 같이 Layout 컴포넌트를 만들어서 각 페이지의 상위 컴포넌트로 감싸주는 방법을 사용합니다. 리액트에서 흔히 HOC(Higher Order Component) 라고 부르는 방식입니다.

import { type PropsWithChildren } from 'react';
import Header from './Header';
import Footer from './Footer';
 
function Layout(props: PropsWithChildren) {
    const { children } = props;

    return (
        <>
            <Header />
            <main>{children}</main>
            <Footer />
        </>
    )
}

export default Layout;

예를 들어, 위 레이아웃을 커스텀 App 컴포넌트를 사용해서 모든 페이지에 적용하게 되면, 페이지를 이동하더라도 HeaderFooter 컴포넌트는 고정되어 있고 중간에 나오는 실제 컨텐츠의 내용만 바뀌게 됩니다.

Per-Page Layouts

그렇다면 각 페이지 별로 다른 레이아웃을 적용하고 싶을 때는 어떻게 해야 할까요? 그럴 때는 Per-Page Layouts이라고 부르는 방법을 사용하면 됩니다. Per-Page Layouts은 각 페이지에 getLayout 속성을 추가하여 레이아웃을 제공하는 방법입니다.

아래 코드는 Per-Page Layouts을 사용하는 예시를 나타낸 것입니다. 여기서는 Root Layout에 해당 페이지에만 적용할 레이아웃을 중첩시키는 형태로 getLayout 함수를 작성하였습니다.

// pages/index.js

import Layout from '@/components/Layout';
import NestedLayout from '@/components/NestedLayout';
 
function Page() {
    return (
        /** 실제 페이지 콘텐츠 */
    );
}
 
Page.getLayout = function getLayout(page) {
    return (
        <Layout>
            <NestedLayout>{page}</NestedLayout>
        </Layout>
    );
}

export default Page;

그리고 커스텀 App 컴포넌트에서 아래와 같이 페이지 컴포넌트에 getLayout 함수가 존재하는지 판단하고, 만약 존재할 경우 해당 페이지만을 위한 레이아웃을 적용하도록 구현하면 됩니다.

// pages/_app.js

export default function MyApp({ Component, pageProps }) {
    // getLayout이 있는 경우 해당 페이지만을 위한 레이아웃이 적용됨
    const getLayout = Component.getLayout ?? ((page) => page);
    
    return getLayout(<Component {...pageProps} />);
}

마지막 업데이트: 2025년 10월 24일 02시 03분

이 문서의 저작권은 이인제(소플)에 있습니다. 무단 전재와 무단 복제를 금합니다.

On this page