Next.js App Router, 1년 써보니
왜 App Router로 전환했나
2023년 초, Next.js 13이 App Router를 정식 출시했을 때 솔직히 망설였다. Pages Router로 이미 여러 프로젝트를 운영하고 있었고, "굳이 바꿔야 하나?"라는 생각이 강했다. 하지만 새로 시작하는 프로젝트가 있었고, 이왕이면 최신 기술을 적용해보자는 마음으로 App Router를 선택했다.
1년이 지난 지금, 그 결정을 후회하지 않는다.
실제로 좋았던 점들
1. 서버 컴포넌트의 위력
처음에는 "클라이언트 컴포넌트랑 뭐가 다른 건데?"라고 생각했다. 하지만 실제로 사용해보니, 서버에서 데이터를 직접 fetch하고 렌더링하는 방식이 코드를 얼마나 단순하게 만드는지 체감했다.
// Before (Pages Router + API Route)
// pages/api/posts.ts
export default async function handler(req, res) {
const posts = await prisma.post.findMany()
res.json(posts)
}
// pages/posts.tsx
export default function Posts() {
const { data } = useSWR('/api/posts')
if (!data) return <Loading />
return <PostList posts={data} />
}
// After (App Router)
// app/posts/page.tsx
export default async function PostsPage() {
const posts = await prisma.post.findMany()
return <PostList posts={posts} />
}
API route를 따로 만들 필요도 없고, 로딩 상태를 직접 관리할 필요도 없다. 그냥 데이터를 가져와서 렌더링하면 된다.
2. 레이아웃 시스템
Pages Router에서는 공통 레이아웃을 _app.tsx에서 처리하거나, 각 페이지에서 getLayout을 사용해야 했다. App Router의 layout.tsx는 이 문제를 깔끔하게 해결해준다.
app/
├── layout.tsx # 전역 레이아웃
├── blog/
│ ├── layout.tsx # 블로그 전용 레이아웃
│ ├── page.tsx
│ └── [slug]/
│ └── page.tsx
└── dashboard/
├── layout.tsx # 대시보드 전용 레이아웃
└── page.tsx
레이아웃이 중첩되는 구조라서, 각 섹션별로 다른 UI를 적용하기가 훨씬 자연스럽다.
3. 로딩과 에러 처리
loading.tsx와 error.tsx가 정말 편하다. 예전에는 모든 페이지에서 로딩/에러 상태를 직접 관리해야 했는데, 이제는 파일 하나 만들어두면 자동으로 처리된다.
// app/blog/loading.tsx
export default function Loading() {
return <BlogSkeleton />
}
솔직히 힘들었던 점들
1. 캐싱 동작의 복잡함
Next.js의 캐싱 시스템은 강력하지만, 처음에는 정말 헷갈렸다. fetch의 기본 동작이 캐시되는 것, revalidate 옵션, router.refresh() 등... 왜 내 데이터가 업데이트되지 않는지 한참을 헤맸다.
지금은 이렇게 정리해서 사용한다:
- 정적 데이터: 기본 캐싱 사용
- 자주 변하는 데이터:
{ cache: 'no-store' }또는revalidate: 0 - 일정 주기 갱신:
{ next: { revalidate: 3600 } } - 온디맨드 갱신:
revalidatePath()또는revalidateTag()
2. 클라이언트/서버 컴포넌트 경계
"use client"를 어디에 붙여야 하는지, 왜 이 hook은 서버 컴포넌트에서 안 되는지, 처음에는 에러 메시지를 보며 하나씩 배워야 했다.
내가 정립한 원칙:
- 기본은 서버 컴포넌트로 시작
- 상태, 이벤트 핸들러, 브라우저 API가 필요하면 클라이언트 컴포넌트로 분리
- 클라이언트 컴포넌트는 가능한 작게 유지
예를 들어, 이 블로그의 글 목록은 서버에서 렌더링하지만, 무한 스크롤 기능은 클라이언트 컴포넌트로 분리했다.
3. 메타데이터 다루기
Pages Router의 next/head와 달리, App Router는 generateMetadata를 사용한다. 처음에는 낯설었지만, 동적 메타데이터를 생성할 때는 오히려 더 직관적이다.
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.description,
openGraph: { images: [post.thumbnail] },
}
}
1인 개발자 관점에서
혼자 개발하는 입장에서 App Router의 가장 큰 장점은 코드량 감소다. API route 따로 만들고, 클라이언트에서 fetch하고, 로딩 상태 관리하고... 이런 보일러플레이트가 확 줄어든다.
물론 학습 곡선이 있다. 처음 3개월은 Pages Router보다 오히려 생산성이 떨어졌다. 하지만 익숙해진 이후로는 훨씬 빠르게 기능을 구현할 수 있게 됐다.
외주 프로젝트에서도 App Router를 쓰고 있다. 클라이언트에게 "서버 컴포넌트라서 번들 크기가 작아집니다"라고 설명할 필요는 없지만, 결과적으로 더 빠른 웹사이트를 더 적은 시간에 만들 수 있다는 건 장점이다.
권장하는 학습 순서
App Router를 처음 시작하는 분들께 추천하는 학습 순서:
- 서버 컴포넌트 개념 이해하기
- 레이아웃과 페이지 구조 익히기
- 데이터 페칭 방법들 (서버 컴포넌트에서 직접 fetch)
- 클라이언트 컴포넌트 분리 시점 이해하기
- 캐싱과 revalidation 전략 수립
- Server Actions 활용하기
결론
App Router를 1년 써본 소감은 "초기 투자 대비 장기적 이득이 크다"이다.
처음에는 분명 Pages Router보다 어렵다. 하지만 익숙해지면 더 적은 코드로 더 나은 결과물을 만들 수 있다. 특히 1인 개발자라면, 유지보수할 코드가 줄어드는 것만으로도 큰 의미가 있다.
새 프로젝트를 시작한다면 App Router를 추천한다. 다만, 이미 Pages Router로 잘 돌아가는 프로젝트가 있다면 굳이 마이그레이션할 필요는 없다. Next.js는 두 라우터를 동시에 지원하니까, 새 기능을 추가할 때 점진적으로 전환하는 것도 좋은 방법이다.