API 응답 포맷: 일관된 API를 위한 모범 사례
일관성 없는 API 응답은 프론트엔드 개발자들이 가장 많이 불만을 제기하는 사항입니다. 모든 엔드포인트가 서로 다른 형태로 데이터를 반환하면 클라이언트 코드에 특수 처리가 난무하게 됩니다. 일관된 응답 포맷은 개발자 경험을 개선하고, 버그를 줄이며, API를 자체 문서화합니다.
응답 엔벨로프
모든 응답을 일관된 구조로 감싸세요:
성공 (단일 리소스)
{
"data": {
"id": "user_123",
"name": "Alice",
"email": "alice@example.com",
"createdAt": "2024-01-15T10:30:00Z"
},
"meta": {
"requestId": "req_abc123"
}
}
성공 (컬렉션)
{
"data": [
{ "id": "user_123", "name": "Alice" },
{ "id": "user_456", "name": "Bob" }
],
"meta": {
"total": 142,
"page": 1,
"perPage": 20,
"requestId": "req_def456"
}
}
오류
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
},
"meta": {
"requestId": "req_ghi789"
}
}
핵심 원칙: 클라이언트는 항상 최상위 레벨에서 data 또는 error를 확인합니다. 같은 응답에 성공 데이터와 오류 정보를 절대 혼합하지 마세요.
응답 포맷을 JSON 유효성 검사기로 확인하세요.
HTTP 상태 코드
상태 코드를 올바르게 사용하세요 — 클라이언트가 가장 먼저 확인하는 항목입니다:
성공 코드
| 코드 | 사용 시점 |
|---|---|
| 200 OK | GET 성공, PUT/PATCH 성공 |
| 201 Created | 리소스를 생성한 POST |
| 204 No Content | DELETE 성공 (본문 없음) |
클라이언트 오류 코드
| 코드 | 사용 시점 |
|---|---|
| 400 Bad Request | 잘못된 요청 구문 |
| 401 Unauthorized | 인증 누락 또는 유효하지 않음 |
| 403 Forbidden | 인증되었으나 권한 없음 |
| 404 Not Found | 리소스가 존재하지 않음 |
| 409 Conflict | 리소스 상태 충돌 (중복) |
| 422 Unprocessable | 올바른 구문이나 의미론적 오류 |
| 429 Too Many Requests | 요청 제한 초과 |
서버 오류 코드
| 코드 | 사용 시점 |
|---|---|
| 500 Internal Server Error | 예상치 못한 서버 장애 |
| 502 Bad Gateway | 업스트림 서비스 장애 |
| 503 Service Unavailable | 일시적 과부하 또는 유지보수 |
오류 응답 설계
좋은 오류 응답은 개발자가 빠르게 디버깅할 수 있도록 도와줍니다:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User with ID 'user_999' not found",
"details": [],
"documentationUrl": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND"
},
"meta": {
"requestId": "req_xyz789",
"timestamp": "2024-01-15T10:30:00Z"
}
}
규칙:
code는 기계가 읽을 수 있는 값입니다 (상수 문자열, HTTP 상태가 아님)message는 사람이 읽을 수 있는 값입니다 (클라이언트를 손상시키지 않고 변경 가능)details는 유효성 검사 실패에 대한 필드 수준 오류를 제공합니다requestId는 지원팀이 로그에서 요청을 추적할 수 있게 합니다
페이지네이션 패턴
오프셋 기반
간단하며 임의의 페이지로 이동을 지원합니다:
{
"data": ["..."],
"meta": {
"page": 2,
"perPage": 20,
"total": 142,
"totalPages": 8
},
"links": {
"first": "/api/users?page=1&per_page=20",
"prev": "/api/users?page=1&per_page=20",
"next": "/api/users?page=3&per_page=20",
"last": "/api/users?page=8&per_page=20"
}
}
커서 기반
실시간 데이터와 대규모 데이터셋에 더 적합합니다:
{
"data": ["..."],
"meta": {
"hasNext": true,
"hasPrev": true
},
"links": {
"next": "/api/users?cursor=eyJpZCI6MTQzfQ&limit=20",
"prev": "/api/users?cursor=eyJpZCI6MTIzfQ&limit=20&direction=prev"
}
}
날짜와 시간
항상 시간대 정보와 함께 ISO 8601을 사용하세요:
{
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:22:33+00:00",
"expiresAt": "2024-12-31T23:59:59Z"
}
절대 사용하지 마세요:
- Unix 타임스탬프 (모호함: 초인지 밀리초인지?)
- 시간대가 없는 로컬 날짜
- MM/DD/YYYY와 같은 사용자 정의 포맷
타임스탬프 처리에 대한 자세한 내용은 Unix 타임스탬프 가이드를 참조하세요.
Null 대 부재 필드
두 가지 일반적인 접근 방식:
null을 포함 (명시적):
{ "name": "Alice", "avatar": null, "bio": null }
부재 필드 생략 (희소):
{ "name": "Alice" }
권장 사항: API 내에서 일관성을 유지하세요. 명시적 null은 타입이 있는 언어에 적합합니다 (클라이언트가 필드의 존재를 알 수 있음). 희소 방식은 대역폭이 제한된 환경에 적합합니다.
응답 버전 관리
응답 포맷이 변경될 때, API의 버전을 관리하세요:
GET /api/v2/users/123
버전 관리가 필요한 호환성을 깨는 변경사항:
- 필드 제거
- 필드 타입 변경
- 필드 이름 변경
- 응답 엔벨로프 구조 변경
호환성을 유지하는 변경사항 (버전 관리 없이 안전):
- 새 필드 추가
- 새 엔드포인트 추가
- 새 enum 값 추가
자주 묻는 질문
성공 응답을 data 키로 감싸야 하나요, 아니면 리소스를 직접 반환해야 하나요?
data 래퍼를 사용하면 모든 응답에 일관된 구조를 제공하고, 데이터와 함께 메타데이터, 페이지네이션, 링크를 위한 공간을 확보합니다. 리소스를 직접 반환하는 것은 단일 리소스 엔드포인트에 더 간단합니다. 대부분의 현대 API는 일관성을 위해 래퍼 방식을 사용합니다.
배치 작업에서 부분 실패를 어떻게 처리해야 하나요?
data 객체에 성공과 실패를 모두 포함하는 응답과 함께 200을 반환하세요. 207 Multi-Status (WebDAV) 사용도 또 다른 옵션이지만 REST API에서는 덜 일반적으로 사용됩니다.
관련 리소스
- JSON 포맷터 — API 응답 포맷팅
- JSON API 디자인 패턴 — 종합 API 디자인 가이드
- JSON 스키마 유효성 검사 — 응답 구조 유효성 검사