JSON Toolkit

JSON API 데이터 계약 설계 요점

핵심 요약: 출시 후 바꾸기 매우 어려운 여섯 가지가 있습니다. 응답 봉투, 오류 형식, 페이지네이션 모양, ID와 시간의 표현, null 대 누락의 의미, 버전 진화입니다.

중급약 13분 소요2026-10-30 발행2026-10-30 업데이트
JSON API 데이터 계약 설계 요점 표지

핵심부터 말하면, 페이지네이션과 이름 디테일을 논의하기 전에 응답 봉투, 오류 형식, ID와 시간 표현, null 대 누락 의미를 계약에 고정하세요. 최상위를 항상 data 봉투로 감싸면 나중에 기존 호출자를 깨뜨리지 않고 페이지네이션과 추적 ID를 더할 수 있습니다.

API 설계에는 표준 답이 없지만, "한 번 고르면 되돌리기 어려운" 결정이 있으며 첫 코드 줄을 쓰기 전에 생각해 두는 것이 좋습니다.

1. 응답을 봉투로 감쌀지

세 가지 스타일:

// Style A: bare response
{ "id": 1, "name": "张三" }

// Style B: data envelope
{ "data": { "id": 1, "name": "张三" } }

// Style C: data + metadata
{ "data": { "id": 1, "name": "张三" }, "meta": { "requestId": "a1b2" } }
스타일 장점 단점
맨 응답 가장 단순, 프런트엔드 계층 하나 적음 메타데이터를 붙일 수 없고, 최상위 배열은 JSON 하이재킹과 진화 위험을 짊어짐
데이터 봉투 meta / errors / links 추가가 쉬움 중첩이 하나 늘고 프런트엔드가 res.data.data를 써야 함
데이터 + 메타데이터 관측성이 가장 좋음 어떤 필드가 meta에 속하는지 엄격히 합의해야 함

권장: 목록과 상세 모두 data 봉투를 균일하게 쓰고, 최상위를 절대 배열로 만들지 마세요. 그러면 나중에 페이지네이션이나 추적 ID를 더해도 계약이 깨지지 않습니다.

2. 오류 형식: RFC 9457을 복사하기

자체 오류 구조를 발명하는 것은 협업 비용의 거대한 원천입니다. RFC 9457(RFC 7807을 대체)은 application/problem+json을 정의합니다:

{
  "type": "https://bbdrug.com/problems/insufficient-balance",
  "title": "余额不足",
  "status": 422,
  "detail": "账户余额 12.00 元,本次扣款需要 30.00 元",
  "instance": "/accounts/A1001/payments/p-889",
  "balance": "12.00",
  "required": "30.00"
}
필드 의미 필수
type 안정적인 문제 유형 URI 권장
title 사람이 읽을 수 있는 짧은 요약 권장
status HTTP 상태 코드 권장
detail 이번에 무슨 일이 있었는지 구체적 설명 선택
instance 문제가 발생한 리소스 인스턴스 선택
확장 필드 임의의 업무 필드 선택

확장 필드(위의 balance / required 등)는 명세에서 허용되며, 프런트엔드는 detail 텍스트를 파싱하는 대신 이를 사용해 정확한 메시지를 만들 수 있습니다.

3. 페이지네이션: 오프셋보다 커서

// Offset pagination
{ "data": [], "page": { "offset": 40, "limit": 20, "total": 137 } }

// Cursor pagination
{ "data": [], "page": { "nextCursor": "eyJpZCI6MTAwfQ", "hasMore": true } }
차원 오프셋 페이지네이션 커서 페이지네이션
구현 난이도 낮음(LIMIT/OFFSET) 중간(안정적인 정렬 키 필요)
깊은 페이지 성능 나쁨(오프셋이 클수록 느림) 좋음
데이터가 변할 때 중복 또는 누락 안정
특정 페이지로 점프 가능 불가능
총 개수 필요 추가 count 필요 불필요

권장: 관리자 테이블에는 오프셋 페이지네이션(페이지 점프와 총계 필요), 사용자 대상 무한 피드에는 커서 페이지네이션을 쓰세요.

4. ID와 시간

4.1 ID에는 항상 문자열 사용

근거는 타입 글에서 다뤘습니다. JavaScript 쪽에서 2^53을 넘는 값은 조용히 정밀도를 잃습니다. 현재 ID가 작은 자동 증가 정수라도 문자열을 쓰면 나중에 스노우플레이크 ID로 전환할 여지가 남습니다.

{ "id": "1000000000000000001" }

4.2 시간에는 RFC 3339 문자열 사용

{
  "createdAt": "2026-10-30T10:41:45+08:00",
  "updatedAt": "2026-10-30T02:41:45Z"
}
  • 밀리초 타임스탬프 숫자를 쓰지 마세요. 읽기 어렵고 단위(초/밀리초)를 자주 혼동합니다.
  • 시간대를 명시하세요. Z나 오프셋으로 표시하고, "시간대 없는 로컬 시간"은 절대 쓰지 마세요.
  • 필드 이름을 xxxAt으로 균일하게 지어 한눈에 시점임을 알 수 있게 하세요.

5. null과 누락

하나를 고르고 문서에 써 넣어야 합니다:

접근 "값 없음" 표현 "수정하지 않음" 표현
접근 A 필드 값이 null 요청에서 필드를 생략
접근 B 필드가 존재하지 않음 요청에서 필드를 생략

접근 A의 문제는 PATCH에서 "생략"과 "null로 보냄"이 다른 뜻이라 오해하기 쉽다는 것입니다. 접근 B의 문제는 프런트엔드의 JSON.stringify가 값이 undefined인 필드를 버리며, 이는 우연히 의미와 맞아떨어진다는 것입니다.

실용 권장:

  • 응답 본문에는 접근 A를 쓰세요(null이 분명히 값 없음을 뜻하고, 프런트엔드가 undefined를 구분할 필요가 없습니다).
  • 요청 본문(특히 PATCH)에는 접근 B를 쓰고, Merge Patch나 JSON Patch와 결합해 의미를 명시하세요.

6. 필드 이름과 구조

결정 흔한 선택 주의
이름 스타일 camelCase(JS 생태계) / snake_case(Python, 데이터베이스) 프로젝트 전체에서 균일하게, 섞지 말 것
열거 값 소문자와 언더스코어, "pending_review" 숫자를 쓰지 말 것, 읽기 어려움
불리언 필드 isActive / enabled status: 0/1을 쓰지 말 것
배열 필드 복수 명사, items 빈 배열에는 []를 반환하고 절대 null을 반환하지 말 것
중첩 깊이 3단계 이하 더 깊으면 분할하거나 expand 파라미터를 제공

빈 배열에 null 대신 []를 반환하는 것은 매우 중요합니다. 프런트엔드의 data.items.map(...)이 null 때문에 깨지지 않습니다.

7. 진화 전략

변경 호환성 파괴 처리 방법
선택 필드 추가 아니요 바로 출시
필수 필드 추가 예 기본값을 더하거나 버전으로 출시
필드 제거 예 먼저 deprecated로 표시하고 호출자를 관찰한 뒤 제거
필드 타입 변경 예 새 필드 이름을 더하고 전환 중 듀얼 쓰기
열거 값 집합 변경 경우에 따라 클라이언트는 알 수 없는 값에 폴백이 있어야 함
필드 의미 변경 예 새 필드로 취급

세 가지 규율:

  1. 클라이언트는 알 수 없는 필드에 관대해야 합니다(additionalProperties: false로 스스로를 옭아매지 마세요).
  2. 클라이언트는 알 수 없는 열거 값에 폴백 분기를 가져야 합니다.
  3. deprecated 필드는 최소 하나의 메이저 버전 동안 유지하고, 응답 헤더나 문서에서 Deprecation으로 표시하세요.

8. 완전한 예

{
  "data": [
    {
      "id": "1001",
      "name": "JSON 权威指南",
      "price": "89.50",
      "tags": ["json", "spec"],
      "publishedAt": "2026-09-01T00:00:00+08:00",
      "author": null
    }
  ],
  "page": { "nextCursor": null, "hasMore": false },
  "meta": { "requestId": "req-7f3a", "tookMs": 12 }
}

이 응답은 위의 모든 관례를 반영합니다. data 봉투, 문자열 ID, 문자열 금액, 시간대가 붙은 타임스탬프, null이 누락을 뜻함, 빈 배열, 커서 페이지네이션, 관측성 정보를 위한 meta.

9. 체크리스트

  • [ ] 최상위가 배열이 아니고 data 봉투를 균일하게 씁니다
  • [ ] 오류 응답이 RFC 9457을 따르고 안정적인 type을 포함합니다
  • [ ] 페이지네이션 모양이 업무 시나리오와 맞습니다
  • [ ] ID가 문자열이고 시간이 RFC 3339를 씁니다
  • [ ] null과 누락의 의미가 문서에 적혀 있습니다
  • [ ] 이름 스타일이 프로젝트 전체에서 균일하고 열거가 소문자 문자열입니다
  • [ ] 클라이언트가 알 수 없는 필드와 알 수 없는 열거 값에 폴백을 가집니다
  • [ ] 호환성 파괴 변경이 버전 관리나 듀얼 쓰기 전환을 거칩니다

FAQ

진화 가능한 JSON API를 어떻게 설계하나?

목록과 상세를 data 봉투로 균일하게 감싸고 최상위를 절대 배열로 만들지 않아, 계약을 깨뜨리지 않고 페이지네이션과 추적 ID를 더할 수 있게 하세요. 문자열 ID와 RFC 3339 타임스탬프를 쓰고 null 대 누락 의미를 문서에 쓰세요. 새 필드는 선택적이고 의미상 독립적으로 두고, 클라이언트에 알 수 없는 필드와 열거 값의 폴백을 주세요.

자체 오류 형식을 발명하면 왜 안 되나?

자체 오류 구조를 발명하는 것은 협업 비용의 거대한 원천입니다. RFC 9457의 application/problem+json을 복사하고, 안정적인 type으로 문제를 식별하며, balance나 required 같은 확장 필드를 허용해 프런트엔드가 detail 텍스트를 파싱하지 않고 정확한 메시지를 만들게 하세요.

모든 엔드포인트가 커서 페이지네이션을 써야 하나?

아니요. 관리자 테이블은 페이지 점프와 총계가 필요해 오프셋 페이지네이션이 더 맞습니다. 사용자 대상 무한 피드는 커서 페이지네이션을 써서 깊은 페이지에서도 안정적이고 데이터가 변할 때 중복이나 누락을 피하세요. 시나리오가 페이지 점프와 총계를 필요로 하는지로 고르세요.

참고 자료

관련 도구

관련 문서

여섯 가지 타입, 숫자 정밀도, 인코딩 함정

JSON의 타입은 여섯 가지뿐이지만, 그 각각이 조용히 데이터의 모습을 바꿔 놓습니다. 이 글은 숫자, 문자열, null의 함정을 한 번에 정리합니다.

초급약 12분 소요2026-10-30 업데이트

JSON Pointer, Patch, Merge Patch

세 개의 짧은 명세가 변경 프로토콜을 이룹니다. Pointer는 찾고, Patch는 정확히 편집하며, Merge Patch는 부분적으로 병합합니다. 잘못 고르면 배열과 동시성에서 다칩니다.

중급약 12분 소요2026-10-30 업데이트

설정, 픽스처, 로컬 데이터를 위한 JSON 관리

설정은 계층화하고 검증해야 하며, 픽스처는 결정적이고 읽기 쉽고 diff 가능해야 합니다. 이 두 종류의 파일을 잘못 관리하면 문제 해결 비용이 오래 높게 유지됩니다.

초급약 10분 소요2026-10-30 업데이트