Ch 05. API를 직접 만들다 — Route Handlers
Next.js 앱 안의 API 서버
Next.js 앱은 프론트엔드이면서 동시에 백엔드가 될 수 있습니다. app/api/ 폴더 안에 route.ts 파일을 만들면 HTTP API 엔드포인트가 생깁니다.
외부 서비스가 데이터를 요청하거나, 모바일 앱이 API를 호출하거나, 웹훅을 받아야 할 때 Route Handlers를 사용합니다.
route.ts 파일 구조
page.tsx와 같은 폴더에는 route.ts가 있을 수 없습니다. 둘 중 하나만 존재할 수 있습니다. 보통 app/api/ 하위에 API를 모아둡니다.
app/├── api/│ └── posts/│ ├── route.ts → GET /api/posts, POST /api/posts│ └── [id]/│ └── route.ts → GET /api/posts/:id, PUT, DELETE└── posts/ └── page.tsx → /posts 페이지
GET, POST, DELETE 핸들러 작성
HTTP 메서드 이름을 가진 함수를 export합니다.
// 파일: app/api/posts/route.tsimport { NextRequest, NextResponse } from 'next/server';interface Post { id: number; title: string; content: string; createdAt: string;}// 임시 데이터 (실제로는 데이터베이스를 사용합니다)const posts: Post[] = [ { id: 1, title: '첫 번째 글', content: '안녕하세요.', createdAt: '2025-01-01', }, { id: 2, title: '두 번째 글', content: '반갑습니다.', createdAt: '2025-01-02', },];// GET /api/postsexport async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const limit = Number(searchParams.get('limit') ?? 10); return NextResponse.json(posts.slice(0, limit));}// POST /api/postsexport async function POST(request: NextRequest) { const body = await request.json(); if (!body.title || !body.content) { return NextResponse.json( { error: '제목과 내용을 입력해주세요.' }, { status: 400 } ); } const newPost: Post = { id: posts.length + 1, title: body.title, content: body.content, createdAt: new Date().toISOString().split('T')[0], }; posts.push(newPost); return NextResponse.json(newPost, { status: 201 });}```text```ts// 파일: app/api/posts/[id]/route.tsimport { NextRequest, NextResponse } from 'next/server';interface RouteParams { params: Promise<{ id: string }>;}// GET /api/posts/:idexport async function GET(request: NextRequest, { params }: RouteParams) { const { id } = await params; const postId = Number(id); // 실제로는 데이터베이스에서 조회 const post = { id: postId, title: `게시글 ${postId}`, content: '내용' }; if (!post) { return NextResponse.json({ error: '게시글을 찾을 수 없습니다.' }, { status: 404 }); } return NextResponse.json(post);}// DELETE /api/posts/:idexport async function DELETE(request: NextRequest, { params }: RouteParams) { const { id } = await params; const postId = Number(id); // 실제로는 데이터베이스에서 삭제 console.log(`게시글 ${postId} 삭제`); return NextResponse.json({ message: '삭제되었습니다.' });}```text### `NextRequest`, `NextResponse` 사용법`NextRequest`는 Web API의 `Request`를 확장합니다. URL, 쿼리 파라미터, 헤더, 쿠키에 쉽게 접근할 수 있습니다.```ts// 파일: app/api/example/route.tsimport { NextRequest, NextResponse } from 'next/server';export async function GET(request: NextRequest) { // URL 파라미터 const { searchParams } = new URL(request.url); const page = searchParams.get('page') ?? '1'; // 헤더 const authHeader = request.headers.get('Authorization'); // 쿠키 const token = request.cookies.get('session')?.value; return NextResponse.json({ page, authenticated: !!authHeader || !!token, });}```text`NextResponse`는 응답을 만드는 객체입니다. JSON 응답, 리다이렉트, 헤더 설정 등을 지원합니다.```ts// 다양한 NextResponse 사용법NextResponse.json({ data: 'hello' }) // 200 JSONNextResponse.json({ error: '잘못된 요청' }, { status: 400 }) // 400 에러NextResponse.redirect(new URL('/', request.url)) // 리다이렉트NextResponse.next() // 미들웨어에서 통과```text### 외부 서비스 웹훅 받기GitHub, Stripe, Slack 등의 서비스는 특정 이벤트가 발생하면 지정된 URL로 HTTP 요청을 보냅니다(웹훅). Route Handler로 이를 받아 처리할 수 있습니다.```ts// 파일: app/api/webhooks/github/route.tsimport { NextRequest, NextResponse } from 'next/server';import crypto from 'crypto';export async function POST(request: NextRequest) { const payload = await request.text(); const signature = request.headers.get('x-hub-signature-256') ?? ''; // 웹훅 서명 검증 const hmac = crypto.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET!); const digest = 'sha256=' + hmac.update(payload).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) { return NextResponse.json({ error: '서명 불일치' }, { status: 401 }); } const event = JSON.parse(payload); console.log('GitHub 이벤트 수신:', event.action); return NextResponse.json({ received: true });}
다음 챕터에서는
다음 챕터에서는 Server Actions와 Route Handlers 중 어느 것을 써야 하는지 비교합니다. 두 가지가 모두 "서버에서 실행되는 것"이라면, 언제 뭘 써야 할까요.