HTTP 메서드 처리
리소스 CRUD를 HTTP 메서드에 대응시키고 NextRequest·NextResponse로 입력·상태 코드·오류 응답을 처리합니다.
이전 글에서는 App Router Route Handler를 만드는 기본 방법과 GET, POST 처리 예시를 살펴봤습니다. 클라이언트와 서버의 통신은 HTTP로 이루어지며, 서버에 어떤 작업을 요청하는지는 HTTP 메서드(method)로 표현합니다. RESTful API에서는 이 메서드를 정확히 써서 리소스의 CRUD(Create, Read, Update, Delete) 작업을 분명하게 드러내는 것이 중요합니다. 이 글에서는 Next.js Route Handler에서 NextRequest와 NextResponse로 GET, POST, PUT, PATCH, DELETE 같은 주요 HTTP 메서드를 처리하는 방법과, 각 메서드의 역할과 구현할 때 유의할 점을 다룹니다.
HTTP 메서드의 역할과 RESTful API
REST(Representational State Transfer)는 웹 서비스를 설계하는 아키텍처 스타일이며, RESTful API는 HTTP 메서드로 리소스에 대한 표준화된 작업을 수행합니다.
GET은 서버에 리소스 조회를 요청하며, /api/users는 모든 사용자를, /api/users/1은 ID가 1인 사용자를 조회합니다. 클라이언트가 상태 변경을 요청하지 않는 안전한(safe) 메서드이지만, 서버의 접근 로그 같은 부수 효과까지 금지한다는 뜻은 아닙니다. POST는 리소스가 정한 방식으로 요청 내용을 처리하도록 요청하며, 아래 API에서는 /api/users에 보낸 본문(body)의 데이터로 새 사용자를 만듭니다.
PUT은 지정한 URI의 리소스를 요청한 표현으로 만들거나 교체하도록 요청합니다. 아래 구현은 /api/users/1처럼 기존 사용자에게 쓰기 가능한 필드인 이름과 이메일을 모두 요구하며, 서버가 관리하는 ID는 본문에서 받지 않습니다. PATCH는 리소스에 변경 사항을 적용하도록 요청하며, 아래 API는 /api/users/1의 이메일만 바꾸는 것처럼 변경할 필드만 담는 JSON 형식을 골랐습니다. DELETE는 /api/users/1처럼 서버의 리소스를 삭제할 때 씁니다.
HEAD는 GET과 같지만 응답 본문 없이 헤더만 받으므로, 리소스가 있는지나 메타데이터만 확인할 때 씁니다. OPTIONS는 특정 리소스에 대해 서버가 어떤 HTTP 메서드를 지원하는지 물을 때 쓰며, CORS(Cross-Origin Resource Sharing)의 사전 요청(preflight request)에 주로 쓰입니다.
PUT과 DELETE의 멱등성(idempotency)은 같은 요청을 반복해도 의도한 최종 효과가 같다는 뜻입니다. DELETE가 처음에는 200, 다음에는 404를 돌려줘도 이 성질과 모순되지 않습니다. PATCH는 언제나 멱등적인 메서드가 아니므로, 아래처럼 값을 대입하는 구현이 어떤 성질을 가지는지 따로 판단합니다.
Next.js Route Handler에서는 route.ts 파일 안에 각 HTTP 메서드 이름으로 함수를 export하면 해당 요청을 처리합니다. OPTIONS를 생략하면 Next.js가 Allow 헤더를 담은 응답을 만들지만, 다른 출처를 허용하는 CORS 헤더 설정까지 대신하지는 않습니다. 다음 코드는 메서드 분기와 JSON 응답을 보여 주는 독립 예제이며, 저장, 권한 검사, 잘못된 JSON 처리까지 구현한 CRUD API는 아닙니다.
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의 Route Handler는 표준 Request를 받을 수 있고, 쿠키나 nextUrl 같은 확장 기능이 필요하면 NextRequest를 씁니다. 응답도 표준 Response나, 편의 메서드를 제공하는 NextResponse로 돌려줄 수 있습니다.
NextRequest (요청 객체)
NextRequest는 표준 Web Request API를 확장한 객체로, HTTP 요청의 여러 정보를 제공합니다. request.url은 전체 요청 URL, request.method는 'GET'이나 'POST' 같은 요청 메서드, request.headers는 Headers 객체인 요청 헤더, request.cookies는 RequestCookies 객체인 요청 쿠키입니다. request.body는 ReadableStream이나 null인 요청 본문이며, request.json()과 request.text() 가운데 필요한 방식을 골라 한 번만 소비합니다. request.nextUrl은 Next.js가 확장한 URL 객체로 pathname과 쿼리 파라미터(searchParams)에 접근할 수 있고, [id] 같은 동적 경로 파라미터는 두 번째 인자인 Route Context의 await context.params에서 읽습니다.
다음 GET과 POST는 앞의 핸들러를 바꾸어 살펴보는 예제이므로, 같은 파일에 중복해서 선언하지 않습니다.
// 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,
});
}주석의 예처럼 /api/data?name=test로 GET 요청을 보내면 핸들러가 읽은 값을 그대로 JSON으로 돌려주는데, 값이 없을 때의 모양은 항목마다 다릅니다. headers.get()은 헤더가 없으면 null을 돌려주므로 본문 없는 GET 요청에서는 대개 contentType이 null로 나타납니다. 반면 ?.value로 읽은 myCookie는 쿠키가 없으면 undefined가 되고, undefined인 속성은 JSON으로 바꿀 때 키째 빠집니다. POST 핸들러는 request.json()을 try로 감싸지 않았으므로, 본문이 올바른 JSON이 아니면 예외가 그대로 올라가 500 오류 응답이 됩니다.
NextResponse (응답 객체)
NextResponse는 표준 Web Response API를 확장한 객체로, 서버 응답을 구성하는 데 씁니다. 응답 본문, 상태 코드, 헤더, 쿠키 등을 설정하는 정적 메서드를 제공합니다.
NextResponse.json(data, init?)은 JSON 형식의 응답을 만들며, init 객체로 status, headers 등을 설정합니다. new Response(body, init?)은 텍스트나 스트림 같은 본문으로 표준 응답을 만들고, 마찬가지로 init으로 상태와 헤더를 설정합니다. NextResponse.redirect(url, status?)는 특정 URL로 리다이렉트하는 응답을 만듭니다. NextResponse.rewrite(url)은 Proxy에서 URL을 유지한 채 다른 대상으로 요청을 넘기고, NextResponse.next()는 Proxy에서 다음 라우트 처리 단계로 요청을 넘기는 메서드이므로, Route Handler에서는 쓰지 않고 자체 응답을 돌려줍니다.
다음은 상태 코드, 헤더, 본문의 모양을 확인하는 대체 핸들러입니다. POST와 DELETE도 실제 저장소를 바꾸지 않습니다.
// 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 });
}응답은 메서드 이름만으로 고정하지 않고, API가 약속한 형식과 실제 처리 결과에 맞춥니다. 아래 전체 CRUD 코드의 DELETE는 메시지를 담은 200을 쓰며, 204를 고를 때는 응답 본문을 보내지 않습니다.
HTTP 메서드별 CRUD 구현 예시
다음 코드는 PUT과 PATCH의 검증 차이를 읽기 위한 독립된 /api/users/[id] 전체 파일 예제입니다. 이전 글의 users-repository.ts를 가져오는 대신 이 파일 안에 배열을 두므로, 앞 글의 목록과 POST 핸들러의 배열과는 연결되지 않습니다. 같은 저장소로 확장하려면 앞 글처럼 데이터 계층을 일관되게 써야 합니다.
NEXT · 11.2 · HTTP 입력
PUT은 두 필드를, PATCH는 보낸 허용 필드를 검증합니다
유효한 ID로 존재하는 사용자를 요청하고, 제공한 이름·이메일의 값이 유효한 경우를 비교합니다. 이 절의 parseFullUser와 parseUserPatch에서 도출한 결과입니다.
| 요청 본문 | PUT | PATCH |
|---|---|---|
| 이름·이메일 | 두 필드를 정규화해 교체 | 보낸 두 필드를 정규화해 수정 |
| 이메일만 | 400 · 이름 누락 | 이메일 수정 · 기존 이름 유지 |
| 추가 필드 포함 | 유효한 이름·이메일만 사용하고 추가 필드는 무시 | 400 · 허용하지 않은 키 |
| 빈 객체 | 400 · 필수 필드 누락 | 400 · 변경할 필드 없음 |
- 요청 본문: 이름·이메일
- PUT: 두 필드를 정규화해 교체
- PATCH: 보낸 두 필드를 정규화해 수정
- 요청 본문: 이메일만
- PUT: 400 · 이름 누락
- PATCH: 이메일 수정 · 기존 이름 유지
- 요청 본문: 추가 필드 포함
- PUT: 유효한 이름·이메일만 사용하고 추가 필드는 무시
- PATCH: 400 · 허용하지 않은 키
- 요청 본문: 빈 객체
- PUT: 400 · 필수 필드 누락
- PATCH: 400 · 변경할 필드 없음
PUT의 추가 필드 무시는 유효한 이름·이메일이 함께 있을 때의 결과입니다. PATCH는 빈 객체·허용하지 않은 키를 거부합니다. 이 전체 파일의 메모리 배열은 앞 절 목록·POST 저장소와 연결되지 않으며 운영 인증을 구현하지 않았습니다.
위 표는 유효한 ID로 존재하는 사용자에게 요청하고 제공한 이름과 이메일 값이 유효하다는 전제에서, 이 글의 parseFullUser와 parseUserPatch가 요청 본문을 어떻게 처리하는지 비교합니다. 이름과 이메일을 모두 보내면 PUT은 두 필드를 정규화해 교체하고, PATCH도 보낸 두 필드를 정규화해 수정합니다. 이메일만 보내면 PUT은 이름이 빠져 400이지만, PATCH는 이메일만 고치고 기존 이름을 유지합니다.
추가 필드가 들어 있으면 PUT은 유효한 이름과 이메일만 쓰고 추가 필드를 무시하지만, PATCH는 허용하지 않은 키가 있으므로 400입니다. 빈 객체를 보내면 PUT은 필수 필드가 빠져서, PATCH는 바꿀 필드가 없어서 둘 다 400입니다. 그림의 설명처럼 PUT이 추가 필드를 무시하는 것은 유효한 이름과 이메일이 함께 있을 때의 결과이며, 이 파일의 메모리 배열은 앞 글의 저장소와 연결되지 않고 운영 인증도 구현하지 않았습니다.
src/app/api/users/[id]/route.ts 파일의 전체 코드는 다음과 같습니다.
import { NextRequest, NextResponse } from 'next/server';
// 가상의 사용자 데이터 (실제로는 데이터베이스)
// 🚨 중요: 이 예제는 서버가 재시작되면 데이터가 초기화됩니다.
// 실제 애플리케이션에서는 반드시 데이터베이스를 사용해야 합니다.
let users = [
{ id: 1, name: '김철수', email: '[email protected]' },
{ id: 2, name: '이영희', email: '[email protected]' },
{ id: 3, name: '박민수', email: '[email protected]' },
];
type UserFields = { name: string; email: string };
function parseUserId(value: string): number | null {
if (!/^\d+$/.test(value)) return null;
const id = Number(value);
return Number.isSafeInteger(id) && id > 0 ? id : null;
}
function normalizeName(value: unknown): string | null {
if (typeof value !== 'string') return null;
const name = value.trim();
return name && name.length <= 50 ? name : null;
}
function normalizeEmail(value: unknown): string | null {
if (typeof value !== 'string') return null;
const email = value.trim().toLowerCase();
return email.length <= 254 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
? email
: null;
}
function parseFullUser(value: unknown): UserFields | null {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
const record = value as Record<string, unknown>;
const name = normalizeName(record.name);
const email = normalizeEmail(record.email);
return name && email ? { name, email } : null;
}
function parseUserPatch(value: unknown): Partial<UserFields> | null {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
const record = value as Record<string, unknown>;
const keys = Object.keys(record);
if (keys.length === 0 || keys.some((key) => key !== 'name' && key !== 'email')) return null;
const patch: Partial<UserFields> = {};
if ('name' in record) {
const name = normalizeName(record.name);
if (!name) return null;
patch.name = name;
}
if ('email' in record) {
const email = normalizeEmail(record.email);
if (!email) return null;
patch.email = email;
}
return patch;
}
// 동적 라우트 파라미터 타입을 위한 인터페이스
interface Context {
params: Promise<{ id: string }>;
}
/**
* GET /api/users/[id] - 특정 사용자 조회
*/
export async function GET(request: NextRequest, context: Context) {
const { id: idParam } = await context.params;
const id = parseUserId(idParam);
if (id === null) {
return NextResponse.json({ message: '올바른 사용자 ID가 필요합니다.' }, { status: 400 });
}
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: idParam } = await context.params;
const id = parseUserId(idParam);
if (id === null) {
return NextResponse.json({ message: '올바른 사용자 ID가 필요합니다.' }, { status: 400 });
}
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ message: '올바른 JSON 본문이 필요합니다.' }, { status: 400 });
}
const input = parseFullUser(body);
if (!input) {
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, ...input } : user
);
// 업데이트된 사용자 정보와 함께 200 OK 응답
return NextResponse.json(users[userIndex], { status: 200 });
}
/**
* PATCH /api/users/[id] - 특정 사용자 부분 업데이트
*/
export async function PATCH(request: NextRequest, context: Context) {
const { id: idParam } = await context.params;
const id = parseUserId(idParam);
if (id === null) {
return NextResponse.json({ message: '올바른 사용자 ID가 필요합니다.' }, { status: 400 });
}
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ message: '올바른 JSON 본문이 필요합니다.' }, { status: 400 });
}
const patch = parseUserPatch(body);
if (!patch) {
return NextResponse.json({ message: '변경할 이름 또는 이메일 형식을 확인해 주세요.' }, { status: 400 });
}
const userIndex = users.findIndex(u => u.id === id);
if (userIndex === -1) {
return NextResponse.json({ message: '사용자를 찾을 수 없습니다.' }, { status: 404 });
}
// 기존 사용자 정보를 가져와서 전달된 필드만 업데이트
const existingUser = users[userIndex];
const updatedUser = {
...existingUser,
...patch,
};
users[userIndex] = updatedUser; // 배열 직접 수정 또는 새로운 배열 생성 방식 선택
return NextResponse.json(updatedUser, { status: 200 });
}
/**
* DELETE /api/users/[id] - 특정 사용자 삭제
*/
export async function DELETE(request: NextRequest, context: Context) {
const { id: idParam } = await context.params;
const id = parseUserId(idParam);
if (id === null) {
return NextResponse.json({ message: '올바른 사용자 ID가 필요합니다.' }, { status: 400 });
}
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는 본문이 없음
}Route Handler 확인 방법
개발 서버(npm run dev)를 실행한 뒤 여러 도구로 요청과 응답을 확인할 수 있습니다. 웹 브라우저 주소창에 http://localhost:3000/api/users/1 같은 URL을 입력하면 GET 요청을 확인할 수 있고, 다른 메서드는 개발자 도구의 fetch 등으로 보냅니다. Postman이나 Insomnia는 여러 HTTP 메서드와 요청 본문, 헤더를 설정해 모든 종류의 API 요청을 시험하기에 알맞습니다. 클라이언트 컴포넌트 안에서 fetch API로 Route Handler에 요청을 보내는 방식도 있으며, 11장 4절에서 다룹니다.
터미널에서는 curl로 간단한 요청을 보낼 수 있습니다. 다음 POST 명령은 앞 글의 목록 경로 예제용이며, 이 글의 상세 경로에 있는 별도 배열에 사용자를 추가하지 않습니다.
# GET
curl http://localhost:3000/api/users/1
# POST (앞 글의 목록 경로)
curl -X POST -H "Content-Type: application/json" -d '{"name":"새로운사용자","email":"[email protected]"}' http://localhost:3000/api/users
# PUT
curl -X PUT -H "Content-Type: application/json" -d '{"name":"업데이트된이름","email":"[email protected]"}' http://localhost:3000/api/users/1
# PATCH
curl -X PATCH -H "Content-Type: application/json" -d '{"email":"[email protected]"}' http://localhost:3000/api/users/1
# DELETE
curl -X DELETE http://localhost:3000/api/users/1Route Handler 보안 및 최적화
보호된 데이터를 다루는 Route Handler는 로그인과 업무 권한을 서버에서 확인합니다. Auth.js의 auth()는 이 검사를 구현하는 한 가지 방법이며, 위 CRUD 예제에는 인증 검사가 없습니다. 클라이언트에게서 받은 모든 입력은 서버에서 반드시 유효성을 검사해야 하며, 이는 악의적인 데이터를 막고 애플리케이션을 안정적으로 만드는 데 꼭 필요합니다.
예외 상황에는 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error처럼 분명하고 일관된 오류 응답을 줍니다. 데이터베이스 연결 문자열이나 API 키 같은 민감한 정보는 개발 환경의 .env.local이나 배포 플랫폼의 비밀 값 설정에서 관리하고, 서버의 process.env.VAR_NAME으로 읽습니다. API 요청과 응답, 오류를 로그로 남기면 디버깅과 모니터링이 쉬워지지만, 로그에는 진단에 필요한 정보만 남기고 세션 쿠키나 요청의 민감한 원문을 그대로 기록하지 않습니다.
정리
- GET은 조회, POST는 처리 요청, PUT은 전체 교체, PATCH는 부분 변경, DELETE는 삭제에 쓰며, PUT과 DELETE는 멱등적이지만 PATCH는 구현에 따라 다릅니다.
NextRequest는nextUrl,cookies같은 확장 기능을 제공하고, 동적 경로 파라미터는 Route Context의await context.params에서 읽습니다.- 요청 본문은
request.json()이나request.text()로 한 번만 소비하며, 잘못된 JSON은 별도로 처리합니다. NextResponse.json()으로 상태 코드와 헤더를 정하고, 204 응답에는 본문을 보내지 않으며,rewrite()와next()는 Proxy에서만 씁니다.- 오류 응답의 상태 코드는 리소스가 있는지뿐 아니라 검사 순서에도 영향을 받습니다.
연습 문제
1. 허용 필드와 반복 요청
전체 CRUD 파일의 초기 ID 1 사용자에게 유효한 이름과 이메일, 그리고 추가 role 필드를 함께 보냅니다. PUT과 PATCH는 각각 어떤 결과가 될까요? 이어서 PATCH로 같은 이메일 값을 두 번 대입하면 기존 이름과 최종 이메일은 어떻게 될까요? 별도의 초기 상태에서 ID 2를 두 번 DELETE했을 때 두 상태 코드도 적어 보세요.
해설 보기
PUT의 parseFullUser는 이름과 이메일만 골라 정규화하므로 추가 role을 무시하고 200입니다. PATCH의 parseUserPatch는 허용하지 않은 키가 하나라도 있으면 400입니다. 유효한 이메일만 반복해서 대입하는 PATCH는 기존 이름을 유지하고, 두 번 모두 같은 이메일을 만듭니다. 이 구현의 대입 결과가 같다고 해서 모든 PATCH가 멱등적이라는 뜻은 아닙니다.
DELETE는 처음 삭제할 때 200, 이미 없어진 사용자를 다시 찾을 때 404입니다. 두 번째 요청 뒤에도 사용자가 삭제된 최종 상태는 같습니다.
2. 없는 ID에 잘못된 본문을 보낼 때
전체 CRUD 파일에서 존재하지 않는 ID 99에 PUT을 보내는데, 본문에 이름과 이메일이 빠져 있습니다. 응답은 404일까요, 400일까요? 404를 받으려면 요청을 어떻게 바꿔야 할까요?
해설 보기
400입니다. ID 문자열의 형식은 통과했지만, 이 코드는 사용자를 찾기 전에 본문을 읽고 parseFullUser로 검사하기 때문입니다. 같은 ID에 유효한 이름과 이메일 두 필드를 보내야 사용자 검색의 404 분기에 이릅니다. 오류 상태는 리소스가 있는지뿐 아니라 검사 순서에도 영향을 받습니다.
3. 204 응답에 본문 넣기
DELETE가 성공했을 때 NextResponse.json({ message: '삭제됨' }, { status: 204 })처럼 메시지를 담아 204로 돌려주려고 합니다. 이 방식에는 어떤 문제가 있을까요?
해설 보기
204 No Content는 응답 본문이 없다는 뜻의 상태 코드이므로, 메시지를 담으면 HTTP의 의미와 맞지 않습니다. 실제로 NextResponse.json()은 내부에서 표준 Response.json()을 부르는데, 이 함수는 204처럼 본문을 가질 수 없는 상태 코드에 본문을 넣으면 TypeError를 던집니다. 그래서 핸들러는 응답을 만들지 못하고 500 오류로 끝납니다. 메시지를 보내고 싶다면 이 글의 예제처럼 200과 함께 JSON을 돌려주고, 204를 고른다면 new NextResponse(null, { status: 204 })처럼 본문 없이 돌려줍니다.
HTTP 메서드별 Route Handler의 작성 방법은 Next.js 공식 문서의 route.js 레퍼런스와 NextResponse 레퍼런스에서 확인할 수 있습니다.
다음 글 API Proxy에서는 Route Handler로 외부 API 요청을 대신 전달하는 프록시를 만드는 방법을 알아봅니다.