icon
11장 : API 라우트

HTTP 메서드 처리

이전 절에서 Next.js App Router의 API 라우트를 생성하는 기본적인 방법과 GET, POST 요청을 처리하는 예시를 살펴보았습니다. 웹 애플리케이션에서 클라이언트와 서버 간의 통신은 HTTP(Hypertext Transfer Protocol)를 기반으로 하며, 이 프로토콜은 다양한 HTTP 메서드(Method) 를 통해 서버에 수행하려는 동작의 종류를 알립니다. RESTful API 디자인에서 이러한 HTTP 메서드를 올바르게 사용하여 리소스에 대한 CRUD(Create, Read, Update, Delete) 작업을 명확하게 표현하는 것이 중요합니다.

이 절에서는 Next.js API 라우트에서 NextRequestNextResponse 객체를 사용하여 GET, POST, PUT, DELETE와 같은 주요 HTTP 메서드를 어떻게 효율적으로 처리하는지 자세히 알아보고, 각 메서드의 역할과 실제 구현 시 유의할 점을 다루겠습니다.


HTTP 메서드의 역할과 RESTful API

REST(Representational State Transfer)는 웹 서비스를 설계하는 데 사용되는 아키텍처 스타일입니다. RESTful API는 HTTP 메서드를 사용하여 리소스에 대한 표준화된 작업을 수행합니다.

  • GET: 서버로부터 리소스 조회를 요청합니다. 데이터를 변경하지 않고 읽기 전용 작업을 수행할 때 사용됩니다.
    • 예: /api/users (모든 사용자 조회), /api/users/1 (ID가 1인 사용자 조회)
  • POST: 서버에 새로운 리소스 생성을 요청합니다. 요청 본문(body)에 생성할 데이터가 포함됩니다.
    • 예: /api/users (새로운 사용자 생성)
  • PUT: 서버의 기존 리소스 전체 업데이트를 요청합니다. 요청 본문에는 리소스의 모든 필드가 포함되어야 합니다.
    • 예: /api/users/1 (ID가 1인 사용자 정보 전체 업데이트)
  • PATCH: 서버의 기존 리소스 부분 업데이트를 요청합니다. 요청 본문에는 변경할 필드만 포함됩니다.
    • 예: /api/users/1 (ID가 1인 사용자의 이메일만 업데이트)
  • DELETE: 서버의 리소스를 삭제할 때 사용됩니다.
    • 예: /api/users/1 (ID가 1인 사용자 삭제)
  • HEAD: GET과 동일하지만 응답 본문 없이 헤더만 받습니다. 리소스의 존재 여부나 메타데이터만 확인할 때 사용됩니다.
  • OPTIONS: 특정 리소스에 대해 서버가 어떤 HTTP 메서드를 지원하는지 질의할 때 사용됩니다. CORS(Cross-Origin Resource Sharing) 사전 요청(Preflight Request)에 주로 사용됩니다.

Next.js API 라우트에서는 route.ts 파일 내에 각 HTTP 메서드 이름으로 함수를 export하면 해당 메서드에 대한 요청을 자동으로 처리합니다.

src/app/api/your-resource/route.ts
// src/app/api/your-resource/route.ts

import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  // GET 요청 처리 로직
  return NextResponse.json({ message: 'GET request received' });
}

export async function POST(request: NextRequest) {
  // POST 요청 처리 로직
  const data = await request.json();
  return NextResponse.json({ message: 'POST request received', data });
}

export async function PUT(request: NextRequest) {
  // PUT 요청 처리 로직
  const data = await request.json();
  return NextResponse.json({ message: 'PUT request received', data });
}

export async function DELETE(request: NextRequest) {
  // DELETE 요청 처리 로직
  return NextResponse.json({ message: 'DELETE request received' });
}

// 기타 메서드도 동일하게 export 할 수 있습니다.
// export async function PATCH(request: NextRequest) { ... }
// export async function HEAD(request: NextRequest) { ... }
// export async function OPTIONS(request: NextRequest) { ... }

NextRequestNextResponse 객체 활용

Next.js App Router의 API 라우트 핸들러 함수는 NextRequest 객체를 첫 번째 인자로 받고, NextResponse 객체를 반환합니다.

NextRequest (요청 객체)

NextRequest는 표준 Web Request API를 확장한 객체로, HTTP 요청에 대한 다양한 정보를 제공합니다.

  • request.url: 요청 URL (Full URL)
  • request.method: 요청 HTTP 메서드 (예: 'GET', 'POST')
  • request.headers: 요청 헤더 (Headers 객체)
  • request.cookies: 요청 쿠키 (RequestCookies 객체)
  • request.body: 요청 본문 (ReadableStream). request.json() 또는 request.text()로 파싱합니다.
  • request.nextUrl: Next.js 확장 객체로, 쿼리 파라미터(request.nextUrl.searchParams), 경로 파라미터 등을 쉽게 접근할 수 있습니다.

예시

// NextRequest 활용 예시
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const url = request.url; // 예: http://localhost:3000/api/data?name=test
  const method = request.method; // 'GET'
  const contentType = request.headers.get('Content-Type'); // 요청 헤더 접근
  const myCookie = request.cookies.get('my_cookie')?.value; // 쿠키 접근
  const nameParam = request.nextUrl.searchParams.get('name'); // 쿼리 파라미터 접근

  return NextResponse.json({
    url,
    method,
    contentType,
    myCookie,
    nameParam,
  });
}

export async function POST(request: NextRequest) {
  const body = await request.json(); // JSON 본문 파싱
  // const textBody = await request.text(); // 텍스트 본문 파싱

  return NextResponse.json({
    message: 'Data received',
    receivedBody: body,
  });
}

NextResponse (응답 객체)

NextResponse는 표준 Web Response API를 확장한 객체로, 서버 응답을 구성하는 데 사용됩니다. NextResponse는 응답 본문, 상태 코드, 헤더, 쿠키 등을 설정할 수 있는 유용한 정적 메서드를 제공합니다.

  • NextResponse.json(data, init?): JSON 형식의 응답을 생성합니다. init 객체로 status, headers 등을 설정할 수 있습니다.
  • NextResponse.text(body, init?): 텍스트 형식의 응답을 생성합니다.
  • NextResponse.redirect(url, status?): 특정 URL로 리다이렉트 응답을 생성합니다.
  • NextResponse.rewrite(url): 클라이언트의 URL을 변경하지 않고 내부적으로 다른 경로를 렌더링하도록 합니다 (미들웨어에서 주로 사용).
  • NextResponse.next(): 미들웨어에서 다음 미들웨어 또는 라우트 핸들러로 요청을 전달합니다.

예시

// NextResponse 활용 예시
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  // 200 OK 상태 코드와 JSON 데이터 반환
  return NextResponse.json({ data: '성공적으로 데이터를 가져왔습니다.' }, { status: 200 });
}

export async function POST(request: NextRequest) {
  // 201 Created 상태 코드와 커스텀 헤더 설정
  const newUser = { id: 1, name: '새 사용자' };
  return NextResponse.json(newUser, {
    status: 201,
    headers: {
      'X-Custom-Header': 'Next.js API',
      'Location': `/api/users/${newUser.id}`, // 생성된 리소스의 위치
    },
  });
}

export async function DELETE(request: NextRequest) {
  // 404 Not Found 상태 코드와 오류 메시지
  const id = request.nextUrl.searchParams.get('id');
  if (id === 'invalid') {
    return NextResponse.json({ message: '리소스를 찾을 수 없습니다.' }, { status: 404 });
  }
  // 204 No Content (성공적으로 처리했지만 반환할 내용이 없을 때)
  return new NextResponse(null, { status: 204 });
}

HTTP 메서드별 CRUD 구현 예시

이전 절에서 만든 users API를 확장하여 GET, PUT, DELETE 메서드를 특정 사용자 (/api/users/[id])에 적용하는 예시를 다시 한번 상세히 살펴보겠습니다.

src/app/api/users/[id]/route.ts 파일 (전체 코드)

src/app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';

// 가상의 사용자 데이터 (실제로는 데이터베이스)
// 🚨 중요: 이 예제는 서버가 재시작되면 데이터가 초기화됩니다.
// 실제 애플리케이션에서는 반드시 데이터베이스를 사용해야 합니다.
let users = [
  { id: 1, name: '김철수', email: 'chulsoo@example.com' },
  { id: 2, name: '이영희', email: 'younghee@example.com' },
  { id: 3, name: '박민수', email: 'minsu@example.com' },
];

// 동적 라우트 파라미터 타입을 위한 인터페이스
interface Context {
  params: { id: string };
}

/**
 * GET /api/users/[id] - 특정 사용자 조회
 */
export async function GET(request: NextRequest, context: Context) {
  const id = parseInt(context.params.id); // URL 파라미터 'id'를 정수로 변환

  const user = users.find(u => u.id === id);

  if (!user) {
    // 사용자를 찾지 못한 경우 404 Not Found 응답
    return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
  }

  // 성공적으로 사용자를 찾은 경우 200 OK 응답
  return NextResponse.json(user, { status: 200 });
}

/**
 * PUT /api/users/[id] - 특정 사용자 전체 업데이트
 */
export async function PUT(request: NextRequest, context: Context) {
  const id = parseInt(context.params.id); // URL 파라미터 'id'를 정수로 변환

  const body = await request.json(); // 요청 본문 파싱
  const { name, email } = body;

  // 입력 유효성 검사 (간단한 예시)
  if (!name || !email) {
    return NextResponse.json({ message: '이름과 이메일은 필수입니다.' }, { status: 400 });
  }

  const userIndex = users.findIndex(u => u.id === id);

  if (userIndex === -1) {
    // 사용자를 찾지 못한 경우 404 Not Found 응답
    return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
  }

  // 사용자 정보 업데이트 (불변성을 유지하며 새로운 배열 생성)
  users = users.map(user =>
    user.id === id ? { ...user, name, email } : user
  );

  // 업데이트된 사용자 정보와 함께 200 OK 응답
  return NextResponse.json(users[userIndex], { status: 200 });
}

/**
 * PATCH /api/users/[id] - 특정 사용자 부분 업데이트
 */
export async function PATCH(request: NextRequest, context: Context) {
  const id = parseInt(context.params.id);

  const body = await request.json(); // 요청 본문 파싱 (부분 업데이트 데이터)
  const { name, email } = body; // name 또는 email 중 하나만 있을 수 있음

  const userIndex = users.findIndex(u => u.id === id);

  if (userIndex === -1) {
    return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
  }

  // 기존 사용자 정보를 가져와서 전달된 필드만 업데이트
  const existingUser = users[userIndex];
  const updatedUser = {
    ...existingUser,
    ...(name && { name }), // name이 있으면 업데이트
    ...(email && { email }), // email이 있으면 업데이트
  };

  users[userIndex] = updatedUser; // 배열 직접 수정 또는 새로운 배열 생성 방식 선택

  return NextResponse.json(updatedUser, { status: 200 });
}


/**
 * DELETE /api/users/[id] - 특정 사용자 삭제
 */
export async function DELETE(request: NextRequest, context: Context) {
  const id = parseInt(context.params.id); // URL 파라미터 'id'를 정수로 변환

  const initialLength = users.length;
  // 사용자 삭제 (불변성을 유지하며 새로운 배열 생성)
  users = users.filter(u => u.id !== id);

  if (users.length === initialLength) {
    // 삭제할 사용자를 찾지 못한 경우 404 Not Found 응답
    return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
  }

  // 성공적으로 삭제된 경우 200 OK 또는 204 No Content 응답
  return NextResponse.json({ message: '사용자가 성공적으로 삭제되었습니다.' }, { status: 200 });
  // return new NextResponse(null, { status: 204 }); // 204는 본문이 없음
}

API 라우트 테스트 방법

개발 서버(npm run dev)를 실행한 후, 다음 도구들을 사용하여 API 라우트를 테스트할 수 있습니다.

  • 웹 브라우저: GET 요청만 직접 테스트할 수 있습니다. (예: http://localhost:3000/api/users/1)
  • Postman / Insomnia: 다양한 HTTP 메서드와 요청 본문, 헤더를 설정하여 모든 종류의 API 요청을 테스트하기에 가장 적합한 도구입니다.
  • curl 명령어: 터미널에서 간단한 API 요청을 보낼 때 유용합니다.
    • GET: curl http://localhost:3000/api/users/1
    • POST: curl -X POST -H "Content-Type: application/json" -d '{"name":"새로운사용자","email":"new@example.com"}' http://localhost:3000/api/users
    • PUT: curl -X PUT -H "Content-Type: application/json" -d '{"name":"업데이트된이름","email":"updated@example.com"}' http://localhost:3000/api/users/1
    • PATCH: curl -X PATCH -H "Content-Type: application/json" -d '{"email":"partial@example.com"}' http://localhost:3000/api/users/1
    • DELETE: curl -X DELETE http://localhost:3000/api/users/1
  • 클라이언트 컴포넌트 (fetch API): React 컴포넌트 내에서 fetch API를 사용하여 API 라우트에 요청을 보내는 방식으로도 테스트할 수 있습니다. (다음 절에서 다룰 예정)

API 라우트 보안 및 최적화

  • 인증 및 권한 부여: 중요한 데이터를 다루는 API 라우트에는 반드시 인증(로그인 여부 확인) 및 권한 부여(사용자 역할 확인) 로직을 추가해야 합니다. NextAuth.js의 getServerSession을 사용하여 API 라우트 내부에서 세션 정보를 가져올 수 있습니다.
  • 입력 유효성 검사: 클라이언트로부터 받은 모든 입력 데이터는 서버 측에서 반드시 유효성 검사를 수행해야 합니다. 악의적인 데이터를 막고 애플리케이션의 안정성을 높이는 데 필수적입니다.
  • 에러 처리: 예외 상황에 대한 명확하고 일관된 오류 응답을 제공해야 합니다. (예: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error)
  • 환경 변수 관리: 데이터베이스 연결 문자열, API 키 등 민감한 정보는 .env.local 파일에 저장하고 process.env.VAR_NAME으로 접근해야 합니다.
  • 로깅: API 요청 및 응답, 오류 발생 시 로그를 기록하여 디버깅 및 모니터링을 용이하게 합니다.

Next.js API 라우트에서 HTTP 메서드를 올바르게 처리하는 것은 RESTful 원칙을 따르는 효율적이고 유지보수 가능한 API를 구축하는 핵심입니다. NextRequestNextResponse 객체의 다양한 기능을 활용하여 견고한 API를 만들어 나가세요.