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 라우트에서 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
하면 해당 메서드에 대한 요청을 자동으로 처리합니다.
// 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) { ... }
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 });
}
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:
- 클라이언트 컴포넌트 (
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를 구축하는 핵심입니다. NextRequest
와 NextResponse
객체의 다양한 기능을 활용하여 견고한 API를 만들어 나가세요.