OPEN HYPER STEP
← 목록으로 (stack-analysis)
STACK-ANALYSIS · 48 / 90
stack-analysis
CHAPTER 48 / 90
읽기 약 2
FUNCTION

API 에러 핸들링 표준 (RFC 7807)


핵심 개념

Problem Details·에러 코드 체계·중앙 핸들러·로깅 — 디버깅 가능한 에러.

본문

RFC 7807 — Problem Details

TYPESCRIPT📋 코드 (20줄)
// 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

커스텀 에러 클래스

TYPESCRIPT📋 코드 (58줄)
// 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`,
    );
  }
}

중앙 에러 핸들러

TYPESCRIPT📋 코드 (59줄)
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

TYPESCRIPT📋 코드 (16줄)
// 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);
}));

에러 코드 체계

📋 코드 (15줄)
{도메인}_{원인}

예시:
- 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