stack-analysis
CHAPTER 48 / 90
읽기 약 2분
FUNCTION
API 에러 핸들링 표준 (RFC 7807)
핵심 개념
Problem Details·에러 코드 체계·중앙 핸들러·로깅 — 디버깅 가능한 에러.
본문
RFC 7807 — Problem Details
// Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Failed",
"status": 400,
"detail": "Email format is invalid",
"instance": "/users/signup",
"errors": [
{ "field": "email", "code": "format", "message": "Invalid email" },
{ "field": "password", "code": "min_length", "message": "Min 8 chars" }
]
}
// 필드 의미:
// type: 에러 타입 URI (문서 링크)
// title: 사람이 읽는 제목
// status: HTTP 상태 코드
// detail: 구체적 설명
// instance: 발생 URL커스텀 에러 클래스
// utils/errors.ts
export class ApiError extends Error {
constructor(
public statusCode: number,
public code: string,
public title: string,
message: string,
public details?: any,
) {
super(message);
}
}
export class ValidationError extends ApiError {
constructor(zodError: any) {
super(
400,
'validation_failed',
'Validation Failed',
'Request body is invalid',
zodError.flatten().fieldErrors,
);
}
}
export class NotFoundError extends ApiError {
constructor(resource: string, id?: string) {
super(
404,
'resource_not_found',
'Not Found',
`${resource}${id ? ` ${id}` : ''} not found`,
);
}
}
export class UnauthorizedError extends ApiError {
constructor(reason = 'Authentication required') {
super(401, 'unauthorized', 'Unauthorized', reason);
}
}
export class ForbiddenError extends ApiError {
constructor(reason = 'Permission denied') {
super(403, 'forbidden', 'Forbidden', reason);
}
}
export class ConflictError extends ApiError {
constructor(resource: string, reason?: string) {
super(
409,
'conflict',
'Conflict',
reason ?? `${resource} already exists`,
);
}
}중앙 에러 핸들러
import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { ApiError } from '@/utils/errors';
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction,
) {
// 알려진 에러
if (err instanceof ApiError) {
return res.status(err.statusCode)
.type('application/problem+json')
.json({
type: `https://api.example.com/errors/${err.code}`,
title: err.title,
status: err.statusCode,
detail: err.message,
instance: req.originalUrl,
...(err.details && { errors: err.details }),
});
}
// Zod
if (err.name === 'ZodError') {
return res.status(400)
.type('application/problem+json')
.json({
type: 'https://api.example.com/errors/validation_failed',
title: 'Validation Failed',
status: 400,
detail: 'Request body is invalid',
instance: req.originalUrl,
errors: (err as any).flatten().fieldErrors,
});
}
// 알 수 없는 에러 — Sentry + 500
Sentry.captureException(err, {
tags: { route: req.route?.path },
user: { id: req.user?.id },
});
console.error('Unhandled error:', err);
res.status(500).json({
type: 'https://api.example.com/errors/internal',
title: 'Internal Server Error',
status: 500,
detail: 'An unexpected error occurred',
instance: req.originalUrl,
correlationId: req.headers['x-correlation-id'],
});
}
// 사용
app.use(errorHandler); // 마지막 미들웨어비동기 핸들러 wrapper
// asyncHandler.ts
type AsyncHandler = (req: Request, res: Response, next: NextFunction) => Promise<any>;
export function asyncHandler(fn: AsyncHandler) {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// 사용 — try/catch 불필요
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) throw new NotFoundError('User', req.params.id);
res.json(user);
}));에러 코드 체계
{도메인}_{원인}
예시:
- auth_invalid_credentials
- auth_token_expired
- user_not_found
- user_email_taken
- payment_card_declined
- payment_insufficient_funds
- order_already_shipped
- rate_limit_exceeded
→ 클라이언트가 코드로 분기 가능
→ 메시지 변경에도 안정다음 챕터
CH.49 "페이지네이션: Cursor vs Offset".
AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude
무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6
내 코드의 에러 핸들링 부분을 분석해서 실전 분석 + 개선 우선순위를 알려줘.
ChatGPT
무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro
에러 핸들링 관련 인기 라이브러리/패턴 5개를 비교 분석해서 패턴 추출를 알려줘.
Gemini
무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro
내 프로젝트 전체에서 에러 핸들링 최적화 가능 위치를 보고해줘.
Grok
무료: Grok 4.1 / SuperGrok $30/mo
2026년 한국 백엔드 시장의 에러 핸들링 트렌드를 솔직히 알려줘.
⭐ 이것만 기억하세요
API 에러 핸들링 표준 (RFC 7807)는 이 3가지만 확실히 잡으세요
1.RFC 7807 = API 에러의 표준 — 도구·문서 자동 호환
2.커스텀 에러 클래스 + 중앙 핸들러 = try/catch 분산 제거
3.asyncHandler로 비동기 에러 → next(err) 자동 — 보일러플레이트 제거
공유하기
진행도 48 / 90