HTTP 메서드 처리
이전 절에서는 App Router API 라우트의 기본 생성 방식과 GET, POST 처리 예시를 살펴봤습니다.
클라이언트-서버 통신은 HTTP 기반으로 이루어지고, 서버에 어떤 작업을 요청하는지는 HTTP 메서드(Method)로 표현합니다.
RESTful API에서는 이 메서드를 정확히 사용해 리소스의 CRUD(Create, Read, Update, Delete) 작업을 명확하게 드러내는 것이 중요합니다.
이 절에서는 Next.js API 라우트에서 NextRequest 및 NextResponse 객체를 사용하여 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하면 해당 메서드에 대한 요청을 자동으로 처리합니다.
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) { ... }NextRequest와 NextResponse 객체 활용
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 });
}메서드별 핸들러를 작성할 때는 “어떤 입력을 읽고, 어떤 상태 코드와 응답 본문을 돌려줄지”를 먼저 정해 두면 구현이 훨씬 안정적입니다. 특히 생성은 201, 삭제 성공은 204처럼 메서드의 의미에 맞는 응답 형태를 고정해 두면 클라이언트 코드도 예측하기 쉬워집니다.
이 매트릭스를 기준으로 각 핸들러의 성공 응답과 오류 응답을 분리한 뒤, 실제 CRUD 코드에서는 검증과 조회 실패 처리만 빠짐없이 채우면 됩니다.
HTTP 메서드별 CRUD 구현 예시
이전 절에서 만든 users API를 확장하여 GET, PUT, DELETE 메서드를 특정 사용자 (/api/users/[id])에 적용하는 예시를 다시 한번 상세히 살펴보겠습니다.
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
- GET:
- 클라이언트 컴포넌트 (
fetchAPI): React 컴포넌트 내에서fetchAPI를 사용하여 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를 구축하는 핵심입니다. NextRequest와 NextResponse 객체의 다양한 기능을 활용하여 견고한 API를 만들어 나가세요.