← 전체 글로 돌아가기

API

API 상태 코드 체크리스트

API 응답의 상태 코드가 의미하는 바를 정확히 이해하고 올바른 코드를 반환하는 방법.

API를 만들 때 상태 코드는 무시하기 쉽다. "일단 데이터만 반환하면 되지"라고 생각하면 클라이언트에서 응답을 제대로 처리할 수 없다. 상태 코드는 화면만 봐서는 모르는 정보를 전달한다.

문제의 경계 파악하기

같은 에러도 상황에 따라 다른 코드를 반환해야 한다.

// ❌ 잘못된 예: 모든 에러를 500으로 반환
app.post('/api/users', async (req, res) => {
  try {
    const user = await createUser(req.body);
    res.json({ success: true, user });
  } catch (e) {
    res.status(500).json({ error: 'Server error' });  // 너무 많은 상황을 500으로
  }
});

// ✅ 올바른 예: 상황에 맞는 상태 코드
app.post('/api/users', async (req, res) => {
  // 요청 검증
  if (!req.body.email) {
    return res.status(400).json({ error: 'Email required' });
  }

  // 인증
  if (!req.user) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  // 권한
  if (req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }

  try {
    // 이미 존재하는 데이터
    const existing = await User.findOne({ email: req.body.email });
    if (existing) {
      return res.status(409).json({ error: 'Email already exists' });
    }

    const user = await User.create(req.body);
    res.status(201).json({ success: true, user });  // 생성 성공
  } catch (e) {
    res.status(500).json({ error: 'Server error' });
  }
});

로컬과 운영 비교

API의 응답은 환경에 따라 다를 수 있다.

# 로컬 환경 테스트
curl -i http://localhost:3000/api/users -X POST \
  -H 'Content-Type: application/json' \
  -d '{"email": "[email protected]"}'

# 배포 환경 테스트
curl -i https://example.com/api/users -X POST \
  -H 'Content-Type: application/json' \
  -d '{"email": "[email protected]"}'

같은 요청인데 다른 상태 코드가 나오면, 환경 설정이 다른 것이다.

이벤트 흐름으로 상태 코드 선택

사용자 흐름을 따라가며 각 단계에서 나올 수 있는 상태 코드를 정의한다.

성공 케이스

200 OK           - 기존 자원 조회/수정/삭제
201 Created      - 새 자원 생성
204 No Content   - 응답 본문 없음

클라이언트 에러 (4xx)

400 Bad Request       - 요청 형식 잘못됨
401 Unauthorized      - 인증 필요
403 Forbidden         - 권한 없음
404 Not Found         - 자원 없음
409 Conflict          - 중복/충돌
429 Too Many Requests - 요청 너무 많음

서버 에러 (5xx)

500 Internal Server Error - 예상 못 한 에러
502 Bad Gateway           - 업스트림 서버 응답 없음
503 Service Unavailable   - 서버 유지보수 중

응답 설계

상태 코드만으로는 부족하다. 응답 본문도 명확해야 한다.

// 200 OK - 성공
{
  "success": true,
  "data": { "id": 1, "name": "John" }
}

// 400 Bad Request - 입력 검증 실패
{
  "success": false,
  "error": "Validation failed",
  "details": {
    "email": "Invalid email format",
    "age": "Must be at least 18"
  }
}

// 401 Unauthorized - 인증 필요
{
  "success": false,
  "error": "Authentication required"
}

// 403 Forbidden - 권한 없음
{
  "success": false,
  "error": "Insufficient permissions",
  "required": "admin"
}

클라이언트에서 상태 코드 처리

프론트엔드에서는 상태 코드로 사용자 경험을 결정한다.

fetch('/api/users', { method: 'POST', body })
  .then(res => {
    if (res.status === 201) {
      // 생성 성공
      return res.json();
    } else if (res.status === 400) {
      // 입력 검증 실패 - 폼 에러 표시
      throw new ValidationError(res.json());
    } else if (res.status === 401) {
      // 로그인 필요
      window.location.href = '/login';
    } else if (res.status === 403) {
      // 권한 없음
      throw new PermissionError('관리자만 가능합니다');
    } else if (res.status === 409) {
      // 중복 - 특정 메시지
      throw new ConflictError('이미 존재하는 이메일입니다');
    } else if (res.status >= 500) {
      // 서버 에러 - 재시도 유도
      throw new ServerError('잠시 후 다시 시도해주세요');
    }
  })
  .catch(error => {
    if (error instanceof ValidationError) {
      // 필드별 에러 표시
      displayFieldErrors(error.details);
    } else if (error instanceof ServerError) {
      // 사용자에게 알림
      showToast(error.message);
    }
  });

측정과 기록

API 응답의 상태 코드 분포를 모니터링한다.

# 로그에서 상태 코드별 집계
grep 'POST /api/users' app.log | awk '{print $NF}' | sort | uniq -c

만약 4xx 에러가 많다면, API 문서나 입력 검증이 부족한 것일 수도 있다.

체크리스트

  1. 각 엔드포인트의 성공/실패 시나리오 정의
  2. 각 시나리오에 맞는 상태 코드 선택
  3. 응답 본문에 충분한 정보 포함 (에러 메시지, 필드별 에러 등)
  4. 클라이언트에서 상태 코드별 처리 로직 구현
  5. 로컬과 배포 환경에서 테스트
  6. 상태 코드 분포 모니터링

상태 코드는 API의 언어다. 정확한 상태 코드는 클라이언트 개발자의 시간을 절약하고, 사용자 경험도 개선한다.