iBetter Books
수정

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 중 어느 것을 써야 하는지 비교합니다. 두 가지가 모두 "서버에서 실행되는 것"이라면, 언제 뭘 써야 할까요.