Next.js 교재 · 23편 · 마이그레이션

Pages → App Router 마이그레이션 가이드

회사 코드의 25% 가 아직 Pages Router. 한 번에 다 바꾸지 말고 점진적으로.

옛 구조에서 새 구조로 점진 이전하는 컨셉 일러스트

1편에서 짚었던 통계 — 회사 Next.js 코드의 70% 가 App Router, 25% 가 옛 Pages Router. 신규 입사하면 마이그레이션 작업을 자주 만난다. 한 번에 통째 바꾸는 게 큰 사고 — 점진적 이전이 정답.

다행히 Next.js 는 두 라우터를 같은 프로젝트에서 공존시킬 수 있게 설계됐다. 페이지 단위로 옮기고 검증하고 반복. 이번 편이 그 단계별 가이드.

1. 공존 모드 — pages/ 와 app/ 동시 사용

같은 Next.js 프로젝트에 두 디렉토리가 동시에 있을 수 있다.

my-app/ ├── pages/ ← 옛 라우터 (그대로) │ ├── index.tsx → "/" │ ├── about.tsx → "/about" │ └── blog/[slug].tsx → "/blog/:slug" └── app/ ← 새 라우터 (이전 중) └── dashboard/ └── page.tsx → "/dashboard"

같은 URL 이 두 디렉토리에 있으면 — app/ 가 우선. 한 페이지를 app 으로 옮기면 그 즉시 새 버전이 라이브, pages 쪽은 무시. 이게 점진 이전의 핵심.

이전 순서 권장 — ① 첫 페이지 한 개로 워밍업, ② 자주 안 바뀌는 정적 페이지(약관·소개), ③ 데이터 페칭 단순한 페이지, ④ 데이터 페칭 복잡, ⑤ 폼·인증·복잡한 인터랙션 마지막. 학습 곡선이 그 순서로 가파르다.

2. 페이지 변환 — 5가지 차이

PagesApp
pages/index.tsxapp/page.tsx
pages/about.tsxapp/about/page.tsx
pages/blog/[slug].tsxapp/blog/[slug]/page.tsx
_app.tsxapp/layout.tsx
_document.tsxapp/layout.tsx(html·body)
pages/api/foo.tsapp/api/foo/route.ts

핵심 — 파일 한 개를 폴더 + page.tsx 로 분리. 폴더 이름이 URL 마지막 세그먼트.

3. 데이터 페칭 변환 — 가장 큰 변화

옛 방식:

// pages/blog/[slug].tsx (Pages Router) export const getServerSideProps = async ({ params }) => { const post = await fetchPost(params.slug); return { props: { post } }; }; export default function BlogPost({ post }) { return <article>{post.body}</article>; }

새 방식:

// app/blog/[slug]/page.tsx (App Router) export default async function BlogPost({ params }) { const { slug } = await params; // Next 15: Promise const post = await fetchPost(slug); return <article>{post.body}</article>; }

getServerSideProps·getStaticProps·getStaticPaths 가 다 사라진다. 컴포넌트 자체가 async, 그 안에서 await. 8편 데이터 페칭 챕터 그대로.

PagesApp
getServerSidePropsfetch(url, { cache: 'no-store' })
getStaticPropsfetch(url, { cache: 'force-cache' })
getStaticProps + revalidatefetch(url, { next: { revalidate: N } })
getStaticPathsexport async function generateStaticParams()

4. 클라이언트 컴포넌트 표시

옛 Pages Router 의 모든 컴포넌트는 자동으로 클라이언트. App Router 는 기본이 서버라 — 인터랙티브한 부분은 'use client' 명시가 필수.

// 옛: Pages/components/Counter.tsx import { useState } from 'react'; export default function Counter() { const [n, setN] = useState(0); return <button onClick={() => setN(n + 1)}>{n}</button>; } // 새: App/components/Counter.tsx — 첫 줄 추가 'use client'; // ← 추가 import { useState } from 'react'; export default function Counter() { … }

안 붙이면 빌드 에러 — "useState only works in Client Components". 6편 server vs client 챕터 참조.

5. 그 외 흔한 마이그레이션 항목

useRouter 변경

// 옛 import { useRouter } from 'next/router'; const router = useRouter(); router.push('/dashboard'); router.query.id // 새 'use client'; import { useRouter, useParams, useSearchParams } from 'next/navigation'; const router = useRouter(); const { id } = useParams(); const search = useSearchParams();

Head → Metadata

// 옛 import Head from 'next/head'; <Head> <title>소개</title> </Head> // 새 export const metadata = { title: '소개', description: '...', };

7편 Metadata API 참조.

_app.tsx 의 Provider

_app.tsx 에서 감싸던 SessionProvider·ThemeProvider 같은 클라이언트 컨텍스트는 — 별도 클라이언트 컴포넌트 ('use client') 로 빼서 app/layout.tsx 에서 children 을 감쌈.

흔한 함정 5가지 — ① params 가 Promise 인 거 깜빡(Next 15), ② useRouter import 경로 안 바꿈, ③ 클라이언트 컴포넌트에 metadata export(무시됨), ④ getServerSideProps 잔존(빌드 에러 X 지만 동작 X), ⑤ next/image 의 layout prop deprecate. 이전 후 페이지 한 번씩 다 검증 필수.

요약 — 23편 좌표

여기까지 정리. 공존 모드로 페이지 단위 점진 이전 — app/ 가 우선. 변환 매핑 — _app→layout, _document→layout, pages/api→app/api, getServerSideProps→async + fetch. 인터랙티브 컴포넌트엔 'use client' 첫 줄 필수. useRouter import 경로·Head→Metadata 도 같이. Next 15 의 params Promise 화도 주의. 한 페이지씩 옮기고 QA 하는 게 정석. 다음 편에서 TypeScript 통합.

다음 편 예고 — TypeScript 통합

타입 안전한 Next.js 프로젝트 세팅. 24편.

© 2026 주나이테크(주) @JUNAITECH