Skip to content

Page Router

Page Router는 파일 시스템 기반 라우팅을 제공하는 방식으로, 현재도 많이 사용되고 있는 안정적인 라우팅 구조다.

React Router와 비슷하게 페이지 전환을 처리하지만, 라우트 설정을 코드로 직접 작성하지 않아도
pages 폴더의 구조만으로 자동으로 라우팅이 생성된다는 점이 특징이다.


Install

bash
npx create-next-app@14
  • NPX(Node Package Executor): 패키지를 전역 설치 없이 바로 실행할 수 있는 명령어
  • create-next-app: Next.js 프로젝트를 생성하는 공식 패키지
  • 14: Page Router를 기본적으로 사용할 수 있는 버전
    (Next.js 13부터 App Router가 도입됐지만, pages 기반 라우팅도 계속 지원)

Pages 폴더 기반 라우팅 (정적 라우트)

Page Router는 pages 폴더의 파일 구조를 기준으로 URL 경로를 자동으로 매핑한다.

less
📁 pages
├── index.tsx // /
├── 📁 about
│   └── index.tsx // /about
└── item.tsx // /item
  • index.tsx는 해당 경로의 기본 페이지를 의미한다.
  • 폴더 이름은 그대로 URL 경로가 된다
  • 별도의 라우터 설정 파일 없이, 폴더 구조만으로 라우팅이 완성된다.

이 방식은 별도의 설정 없이 파일 구조 그대로 경로가 되는 정적 라우팅 구조를 기본으로 한다.
즉, 폴더 구조 자체가 URL 구조를 결정한다.


less
📁 pages
└── 📁 item
    └── index.tsx   // /item

또한 폴더를 중첩하면 URL 역시 자연스럽게 계층 구조를 갖게 된다.
이 방식은 페이지 구조를 한눈에 파악하기 쉽고,
라우팅 설정을 따로 관리할 필요가 없어 유지보수가 간편하다.


Dynamic Routes

정적 라우팅 구조를 기본으로 하지만, 동적으로 변하는 경로 역시 파일 이름만으로 정의할 수 있다.
동적 경로란, URL에 변할 수 있는 값(파라미터)이 포함된 경로를 의미한다.
파일명을 대괄호[]로 감싸면, 해당 부분이 동적 파라미터로 인식된다.

less
📁 pages
├── index.tsx
└── 📁 item
    ├── index.tsx
    └── [id].tsx
    // 위 구조는 다음과 같은 경로를 모두 처리한다.
    // /item/1, /item/2, /item/100
  • [id].tsx → id 값에 따라 다른 페이지를 렌더링
  • 리스트 → 상세 페이지 구조에서 매우 자주 사용된다.
  • ex. 블로그의 게시글 페이지 → posts/123
  • ex. 쇼핑몰의 상품별 상세페이지 → /products/456

Page Router는 파일 구조만으로 라우팅을 정의할 수 있는 직관적인 라우팅 방식이다.

  • 쿼리 스트링은 useRouter().query로 접근
  • [id] → 단일 동적 경로
  • [...id] → 여러 경로를 처리하는 Catch-All Segment
  • [[...id]] → 경로가 있어도 없어도 되는 Optional Catch-All Segment
  • 404.tsx → 존재하지 않는 모든 경로 처리

쿼리 스트링 읽기 with useRouter

Page Router에서는 useRouter 훅을 사용해 URL에 포함된 쿼리 스트링에 접근할 수 있다.

useRouter

useRouter는 현재 URL 상태를 반영한 라우터 객체를 반환한다.
이를 통해 검색어, 필터 값과 같은 동적 데이터를 컴포넌트 내부에서 처리할 수 있다.

tsx
const router = useRouter();
const { q } = router.query;

쿼리 스트링은 router.query 객체에 key-value 형태로 저장된다.


tsx
import { useRouter } from 'next/router';

export default function Home() {
  const router = useRouter();
  const { q } = router.query;
  // URL: /search?q=react
  // router.query = { q: 'react' }

  return <h1>Search {q}</h1>;
}
  • router.query는 초기 렌더링 시 비어 있을 수 있다.
  • 쿼리 값의 타입은 string | string[] | undefined

초기 렌더링 시 주의할 점

tsx
const router = useRouter();
const { q } = router.query;

// 클라이언트 사이드에서 router.query가 채워질 때까지 대기
if (!router.isReady) return <div>Loading...</div>;
if (!q || Array.isArray(q)) return null;

Next.js는 초기 렌더링 시 query 객체가 비어 있다가 클라이언트에서 채워지기 때문에,
router.isReady로 준비 상태를 확인하는 것이 안전하다.


tsx
useEffect(() => {
  if (!router.isReady) return;

  const q = router.query.q;
  if (typeof q === 'string') {
    fetchData(q);
  }
}, [router.isReady]);

실제 서비스에서는 렌더링을 막기보다는,
useEffect 내부에서 router.isReady를 체크해 API 호출이나 사이드 이펙트만 지연 처리하는 경우가 더 많다고 한다.


[id] – 단일 동적 경로

Page Router에서는 파일명에 대괄호[]를 사용해 동적 라우트를 정의한다.
단일 파라미터 기반 상세 페이지에서 가장 많이 사용되는 패턴이다.

less
📁 pages
└── 📁 book
    └── [id].tsx // /book/1, /book/abc

[id].tsx에서 id는 URL 파라미터를 의미하며, 해당 위치의 값이 동적으로 변할 수 있음을 나타낸다.


tsx
import { useRouter } from 'next/router';

export default function Page() {
  const router = useRouter();
  const { id } = router.query;

  return <h1>Book {id}</h1>;
}
  • /book/123 → id === "123"
  • /book/abc → id === "abc"

[id].tsx만 존재할 경우

이는 /book 경로를 처리할 파일이 존재하지 않기 때문이다.

txt
/book → 404
/book/1 → 정상

/book 경로 자체는 처리되지 않기 때문에, 404 페이지가 표시된다.


[...id] – Catch-All Segment

여러 단계의 하위 경로를 하나의 페이지에서 처리하려면 Catch-All Segment를 사용한다.
URL 깊이가 정해지지 않은 구조에서 유용하게 사용된다.

less
📁 pages
└── 📁 book
    └── [...id].tsx
  • 문서 경로
  • 카테고리 트리
  • 슬러그 기반 URL

tsx
import { useRouter } from 'next/router';

export default function Page() {
  const router = useRouter();
  const { id } = router.query;

  return <h1>Book {id}</h1>;
}
  • [...id].tsx에서 id는 항상 string[]
  • /book/1, /book/1/2/3 같은 경로를 모두 처리한다.
  • 단, /book 자체는 여전히 404

/book/index.tsx

less
📁 pages
└── 📁 book
    ├── index.tsx
    └── [...id].tsx

/bookindex.tsx, 그 외 하위 경로는 [...id].tsx가 처리한다.


[[...id]] – Optional Catch-All Segment

경로가 있어도 되고 없어도 되는 경우에는 Optional Catch-All Segment를 사용한다.
예를 들어 /book, /book/1, /book/1/2 모두 동일한 페이지에서 처리 가능하다.
즉, 기본 경로와 하위 경로를 하나의 페이지에서 모두 처리할 수 있다.

less
📁 pages
├── index.tsx
└── 📁 book
    └── [[...id]].tsx

tsx
import { useRouter } from 'next/router';

export default function Page() {
  const router = useRouter();
  const { id } = router.query;

  return <h1>Book {id}</h1>;
}
  • [...id]string[]로 항상 배열
  • [[...id]]string[] | undefined (경로가 없으면 undefined)
  • 카테고리 / 서브카테고리 구조
  • 깊이가 정해지지 않은 문서 URL
  • 하나의 페이지에서 다양한 경로를 포괄하고 싶을 때

Dynamic Route 요약

파일명처리 가능한 경로id 타입
[id].tsx/book/1string
[...id].tsx/book/1/2string[]
[[...id]].tsx/book, /book/1/2string[] | undefined

_document.tsx

tsx
import { Html, Head, Main, NextScript } from 'next/document';

export default function Document() {
  return (
    <Html lang='en'>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

_document.tsx는 전체 HTML 문서 구조를 커스터마이징하기 위한 파일이다.

  • <html>, <body> 같은 최상위 태그 설정
  • 메타 태그
  • 폰트, 캐릭터셋
  • Google Analytics 같은 서드파티 스크립트

서버에서 한 번만 실행되며, 페이지별 렌더링 로직과는 분리되어 있다.


_app.tsx

tsx
import '@/styles/globals.css';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

현재 라우트에 해당하는 페이지 컴포넌트를 가져와서, 필요한 props를 넘겨 렌더링한다.

모든 페이지가 렌더링되기 전에 공통으로 거쳐 가는 최상위 컴포넌트다.
즉, _app.tsx는 모든 페이지를 감싸는 최상위 컴포넌트다.

🧩 Component와 pageProps

Component

  • 현재 URL에 해당하는 pages 폴더의 페이지 컴포넌트
  • ex. /aboutpages/about/index.tsx

pageProps

  • getStaticProps, getServerSideProps 등에서 반환한 데이터
  • 페이지에 전달될 실제 props

_app.tsx가 필요할까?

_app.tsx를 사용하면 모든 페이지에 공통 로직을 적용할 수 있다.

  • 전역 레이아웃 적용
  • 전역 CSS 적용
  • 공통 Header, Footer 등
  • 전역 상태 관리
  • 인증 처리
  • 페이지 전환 애니메이션
tsx
import '@/styles/globals.css';
import type { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <header>Header</header>
      <Component {...pageProps} />
    </>
  );
}

이렇게 하면 모든 페이지에 Header 컴포넌트를 적용할 수 있다.


_app.tsx_document.tsx의 차이

  • _app.tsx → 모든 페이지를 감싸는 React 컴포넌트 (클라이언트 & 서버)
  • _document.tsx → HTML 문서 구조를 설정하는 파일 (서버에서만 실행)

즉,

  • 레이아웃, 상태, 스타일 → _app.tsx
  • html, body, meta 태그 → _document.tsx

next.config.mjs

js
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};

export default nextConfig;

next.config.mjs는 Next.js 프로젝트 전반의 동작 방식을 설정하는 전역 설정 파일이다.
빌드 방식, 개발 환경 동작 등 프로젝트 전반에 영향을 주는 설정을 이 파일에서 관리한다.

이 파일의 설정은 모든 페이지와 환경에 공통으로 적용되며,
개발 서버 실행 및 빌드 과정에서 Next.js가 자동으로 참조한다.


Page Router 장단점

장점

1. 파일 시스템 기반 라우팅

폴더 / 파일 구조만으로 페이지 경로를 정의할 수 있다.

  • Dynamic Routes
  • Catch All Segment
  • Optional Catch All Segment
  • 구조가 단순하고 예측 가능성이 높다.

2. 다양한 사전 렌더링 방식 지원

  • SSR (Server-Side Rendering)
    요청이 들어올 때마다 페이지를 생성
    항상 최신 데이터, 요청마다 렌더링 비용 발생

  • SSG (Static Site Generation)
    빌드 타임에 페이지를 미리 생성
    응답 속도가 빠름, 최신 데이터 반영 어려움

  • ISR (Incremental Static Regeneration)
    정적 페이지를 주기적으로 재생성
    SSG 속도 + 데이터 최신성 보완, 시간 기반 갱신 가능


단점

1. 페이지별 레이아웃 설정이 번거롭다.

→ getLayout 패턴 필요

2. 데이터 페칭이 페이지 컴포넌트에 집중된다.

→ 컴포넌트 단위 분리 어려움

3. 불필요한 컴포넌트들도 JS Bundle에 포함된다.

→ 페이지 단위 구조 특성상 번들 최적화에 한계가 있다.


Page Router의 특징과 한계

Page Router는 직관적이고 안정적인 구조를 제공하지만, 몇 가지 제약도 존재한다.

  • pages 폴더 외부에서는 라우팅이 불가능하다
  • 중첩 레이아웃을 구현하려면 _app.tsx에서 직접 구성해야 한다
  • Server Component 개념을 사용할 수 없다
  • App Router에 비해 데이터 패칭 방식이 제한적이다

Error Pages

404 페이지

동적 라우팅에서 처리되지 않는 경로가 생길 수 있으므로, 404 페이지를 커스터마이징해두면 사용자 경험이 좋아진다. Next.js에서는 pages/404.tsx 파일을 생성하면 자동으로 커스텀 404 페이지로 인식한다.

less
📁 pages
└── 404.tsx

tsx
import Link from 'next/link';

export default function NotFound() {
  return (
    <div>
      <h1>404 - 존재하지 않는 페이지입니다</h1>
      <Link href='/'>홈으로 돌아가기</Link>
    </div>
  );
}
  • 존재하지 않는 모든 경로에서 자동 렌더링
  • 별도의 라우터 설정은 필요 없음