devlog.
개발#Next.js

Next.js App Router 마이그레이션 가이드

·10분 읽기

App Router란?#

Next.js 13에서 도입된 App Router는 React Server Components를 기반으로 한 새로운 라우팅 시스템입니다. 기존 pages/ 디렉토리 방식(Pages Router)을 대체하며, app/ 디렉토리를 사용합니다.

주요 변경사항#

구분Pages RouterApp Router
라우팅pages/ 디렉토리app/ 디렉토리
데이터 페칭getServerSideProps, getStaticPropsasync 서버 컴포넌트
레이아웃_app.tsxlayout.tsx (중첩 가능)
기본 컴포넌트클라이언트 컴포넌트서버 컴포넌트

서버 컴포넌트 vs 클라이언트 컴포넌트#

App Router의 가장 큰 변화는 기본적으로 모든 컴포넌트가 서버 컴포넌트라는 점입니다.

서버 컴포넌트 (기본값)#

// app/posts/page.tsx
// 'use client' 없이 사용 — 서버에서만 실행됨
async function PostsPage() {
  // 서버에서 직접 DB 조회 가능
  const posts = await db.posts.findMany()

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

export default PostsPage

클라이언트 컴포넌트#

'use client'

// useState, useEffect 등 훅 사용 시 필요
import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      클릭 수: {count}
    </button>
  )
}

export default Counter

데이터 페칭 마이그레이션#

Before: Pages Router#

// pages/blog/[slug].tsx
import type { GetStaticProps, GetStaticPaths } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await getAllPosts()
  return {
    paths: posts.map(post => ({ params: { slug: post.slug } })),
    fallback: false,
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const post = await getPostBySlug(params?.slug as string)
  return { props: { post } }
}

export default function BlogPost({ post }: { post: Post }) {
  return <article>{post.content}</article>
}

After: App Router#

// app/blog/[slug]/page.tsx
import { getAllPosts, getPostBySlug } from '@/lib/mdx'
import { notFound } from 'next/navigation'

export async function generateStaticParams() {
  const posts = getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export default async function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  const post = getPostBySlug(params.slug)

  if (!post) notFound()

  return <article>{post.content}</article>
}

마이그레이션 순서 권장사항#

  1. 점진적 마이그레이션: pages/app/은 공존 가능
  2. 레이아웃부터 시작: app/layout.tsx 먼저 구성
  3. 정적 페이지 먼저: 데이터 페칭이 없는 페이지부터
  4. 클라이언트 컴포넌트 최소화: 필요한 경우에만 'use client' 사용
  5. 테스트 보강: 마이그레이션 후 동작 검증

알려진 함정들#

  • 서버 컴포넌트에서 훅 사용 불가: useState, useEffect 등은 클라이언트 컴포넌트에서만 사용
  • Context는 클라이언트 전용: createContextuseContext는 클라이언트 컴포넌트에서만 동작
  • next/headers는 서버 전용: cookies(), headers() 등은 서버 컴포넌트에서만 사용 가능

관련 포스트