git-style 프로젝트에서 Pages Router를 걷어낸 이유
git-style 서비스에서 APNG 이미지를 생성하는 API를 구현하던 중, Pages Router의 API Routes를 사용하고 있었습니다. 프로젝트는 App Router 기반인데 말이죠.
“일단 동작하니까”라는 생각으로 넘어갔지만, 결국 라우터 시스템의 철학적 차이와 유지보수성 문제로 돌아왔습니다. 이 글은 Next.js의 두 라우터 시스템을 이해하고, 왜 혼합 사용을 지양해야 하는지 정리한 글입니다.
문제 상황
git-style은 GitHub 기여 그래프를 다양한 테마로 시각화하는 서비스다. 사용자가 선택한 꽃 종류와 색상에 따라 APNG 애니메이션을 동적으로 생성하는 API가 필요했다.
프로젝트는 Next.js 16(App Router 기반)으로 구성되어 있었는데, API 구현 시점에 익숙함을 이유로 Pages Router의 API Routes 방식을 사용했다.
// pages/api/[username]/animation.ts
import type { NextApiRequest, NextApiResponse } from "next";
export const config = {
api: {
responseLimit: false,
},
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
// ...
}
당장은 동작했지만, 두 가지 문제가 발생했다.
- 디렉토리 구조의 불일치: app/ 디렉토리와 pages/ 디렉토리가 공존하면서 프로젝트 구조가 일관성을 잃었다.
- 라우팅 충돌 가능성: 동일한 경로에 두 라우터 시스템이 존재할 경우, Pages Router가 우선권을 가지며 예기치 않은 동작이 발생할 수 있다.
내가 처음 한 생각
처음에는 “API Routes가 더 익숙하니까, 일단 빠르게 구현하자”는 생각이었다. 실제로 NextApiRequest와 NextApiResponse를 사용한 방식은 이미 경험이 있어서 러닝 커브가 낮았다.
// 익숙한 패턴
res.setHeader("Content-Type", "image/png");
res.status(200).send(Buffer.from(apngData));
하지만 이런 접근은 두 가지 측면에서 문제가 있었다.
- 철학적 불일치: App Router는 React Server Components를 기반으로 하는 새로운 패러다임인데, API만 구식 방식으로 작성하는 것은 아키텍처적 일관성을 해친다.
- 장기적 유지보수: Next.js 팀이 App Router를 권장 방향으로 설정한 만큼, Pages Router에 대한 투자는 점차 줄어들 가능성이 높다.
실제로 해 본 선택
App Router의 Route Handler 방식으로 전환했다.
// app/api/[username]/animation/route.ts
import { type NextRequest, NextResponse } from "next/server";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ username: string }> },
) {
const { username } = await params;
const searchParams = request.nextUrl.searchParams;
const flower = searchParams.get("flower");
const color = searchParams.get("color");
// ...
return new NextResponse(Buffer.from(apngData), {
status: 200,
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, s-maxage=86400, max-age=3600",
},
});
}
주요 변경 사항
| 구분 | Pages Router (API Routes) | App Router (Route Handler) |
|---|---|---|
| 객체 타입 | NextApiRequest, NextApiResponse | NextRequest, NextResponse |
| 함수 정의 | export default function handler | export async function GET |
| 파라미터/쿼리 | req.query | params + request.nextUrl.searchParams |
| 응답 방식 | res.setHeader(), res.send() | NextResponse 내 headers 객체 설정 |
| 설정 (Config) | export const config | 불필요 (Route Handler는 기본적으로 제한 없음) |
지금 다시 해본다면
앞으로 비슷한 상황에 놓인다면
- 새 프로젝트는 무조건 App Router로 시작한다. Next.js 13 이후 App Router가 권장 방향이다.
- “익숙하니까”라는 이유로 레거시 패턴을 혼합하지 않는다. 단기적 편의보다 장기적 일관성이 중요하다.
- 라우터 시스템은 하나만 선택한다. 혼합 사용 시 Pages Router가 우선권을 가지며, 빌드 타임에 충돌 발생할 수 있다.
비고
Next.js Pages Router와 App Router의 탄생 철학
- Pages Router (Next.js 1~12)
Next.js의 초기 라우팅 시스템으로, 파일 시스템 기반 라우팅을 도입했다. pages/ 디렉토리 내 파일이 라우트가 되는 직관적인 구조다. getServerSideProps, getStaticProps 같은 데이터 페칭 함수로 SSR/SSG를 처리했다.
- App Router (Next.js 13~)
https://nextjs.org/blog/next-13는 React Server Components, Suspense, Streaming 등 React의 최신 기능을 활용하기 위해 설계되었다.
App Router가 만들어진 이유
- 레이아웃 시스템 개선: 중첩 레이아웃을 자연스럽게 지원
- React 최신 기능 활용: Server Components로 클라이언트에 전송되는 JavaScript 감소
- 데이터 페칭 개선: 컴포넌트 수준에서의 데이터 페칭, 스트리밍 응답 지원
- 커뮤니티 피드백 반영: 컴포넌트 내 데이터 페칭, 클라이언트 JavaScript 감소 요청
- https://nextjs.org/docs/app
- https://nextjs.org/blog/next-13
- https://nextjs.org/blog/june-2023-update
두 라우터 혼합 시 주의사항
- 경로 충돌: 동일한 URL 경로에 두 라우터가 존재하면 빌드 타임 에러 발생
- 우선순위: 경로가 겹치면 Pages Router가 우선권을 가짐
- API Routes vs Route Handler: 함께 사용할 필요 없음. Route Handler는 API Routes의 App Router 버전이라고 이해하면 됨.
- https://nextjs.org/docs/app/getting-started/route-handlers
- https://vercel.com/blog/common-mistakes-with-the-next-js-app-router-and-how-to-fix-them