팀 협업 FAQ: 계약, 설정, 픽스처
핵심 요약: 이름 스타일, 시간 형식, null 의미, 열거 모양, 설정 형식, 픽스처 조직, 병합 충돌 — 팀이 진짜로 다투는 열두 가지 절충입니다.

핵심부터 말하면, 팀 관례에서 한 번 정하세요. HTTP 상태 코드와 {"data":…,"meta":…} 봉투, 사이트 전체 한 가지 이름 스타일, 시간대가 있는 RFC 3339 타임스탬프, 문자열 열거. 그런 다음 모든 새 API는 필드 추가만 하고 변경이나 삭제 없이 같은 계약을 따라 진화시킵니다.
이 질문들에 단일 정답은 없지만, 팀에는 단일 답이 있어야 합니다. 아니면 새 API마다 같은 논쟁을 다시 시작합니다.
1. API 계약
API는 어떤 구조를 반환해야 하나?
흔한 세 모양, 각각 비용이 있습니다:
| 모양 | 예 | 장점 | 단점 |
|---|---|---|---|
| 원본 데이터 | [{...}, {...}] |
간단 | 메타데이터를 담을 수 없음 |
| 고정 봉투 | {"code":0,"message":"ok","data":{...}} |
균일한 오류 처리 | 업무 코드가 HTTP 상태를 중복 |
| 리소스 봉투 | {"data":{...},"meta":{...},"links":{...}} |
확장 가능, 의미 명확 | 중첩이 한 단계 더 |
내부 업무 API에는 "리소스 봉투 + 결과를 표현하는 HTTP 상태 코드"를 선호하세요:
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
오류에는 또 다른 code를 중첩하지 말고 HTTP 상태 코드와 구조화된 오류 본문을 쓰세요:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
요점: HTTP 상태 코드가 이미 성공이나 실패를 표현하므로 업무 코드는 중복입니다. 두 체계가 공존하면 "201인데 code는 500" 같은 불일치가 거의 불가피합니다.
맨 배열은 피하세요. 최상위가 배열이면 페이지네이션, 총계, 커서를 붙일 수 없고, 나중에 더하려면 모든 호출자를 바꿔야 합니다.
필드 이름은 snake_case인가 camelCase인가?
하나를 고르고 사이트 전체에 적용하며, 한 JSON 문서 안에서 절대 섞지 마세요.
| 스타일 | 흔한 생태계 | 예 |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go(일부) / 데이터베이스 열 | user_name, created_at |
kebab-case |
드묾, 하이픈 키는 접근이 어색 | user-name |
기준:
- 이 JSON을 누가 소비하나: 프런트엔드가 주도하면 camelCase, 데이터 파이프라인과 Python 서비스가 주도하면 snake_case
- 데이터베이스 열에 직접 대응하나: 표 구조를 그대로 통과시킬 때 snake_case가 변환 계층을 아낍니다
- 기존 시스템의 연속성: 기존 API가 쓰는 방식을 유지하세요. 일관성이 정확성보다 중요합니다.
꼭 변환해야 한다면 업무 코드에 toCamelCase 호출을 흩뿌리지 말고 경계(게이트웨이나 프런트엔드 요청 계층)에서 하세요.
시간 필드는 어떤 형식을 써야 하나?
시간대 오프셋이 있는 RFC 3339 / ISO 8601 문자열을 쓰세요:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
요점:
- 시간대를 포함하세요:
2026-10-30 11:37:57처럼 존이 없으면 시간대 간 시나리오에서 반드시 깨집니다. Z나 오프셋을 일관되게 쓰세요: 둘 다 올바르지만 한 API 안에서 섞지 마세요.- 숫자 타임스탬프를 피하세요: 초인지 밀리초인지, 윤초가 포함되는지 매번 재해석해야 합니다. 간결성이 필요하면 밀리초 정수를 쓰되 문서에 단위를 고정하세요.
createdAt / updatedAt / deletedAt처럼 At 접미사 필드 이름을 선호하세요.
값이 기간이 아니라 시점임이 한눈에 보입니다. 기간에는 timeoutMs, ttlSeconds처럼 이름에 단위를 넣으세요.
null은 null인가 필드 생략인가?
"이 필드가 의미 있는가"에 달려 있습니다:
| 의미 | 형태 | 비고 |
|---|---|---|
| 필드는 존재하지만 아직 값이 없음 | "name": null |
"확인했고 비어 있다"를 명시 |
| 필드가 이 리소스와 무관함 | 생략 | 예: 일반 사용자에게 vipExpireAt이 없어야 함 |
| 부분 업데이트(PATCH) | 생략 = 변경 안 함 | 여기서는 null만 "지우기"를 뜻함 |
세 번째가 가장 흔히 간과됩니다. PATCH 요청에서:
{ "nickname": null }
은 "nickname을 지운다"를 뜻하고, 요청 본문에서 nickname을 빼는 것은 "nickname을 건드리지 않는다"를 뜻합니다.
둘 다 "지우기"로 취급하면 이메일을 바꾸는 사용자가 nickname을 날려 버립니다.
다시 강조: undefined를 쓰지 마세요. 유효한 JSON 값이 아니고 JSON.stringify가 키를 완전히 버려서,
동작은 "생략"과 같으면서 코드는 "명시적으로 빈 값을 넘겼다"처럼 보입니다.
불리언 필드는 어떻게 이름 짓나?
is / has / can / should 접두사를 쓰고 부정을 피하세요.
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
세 가지 교훈:
- 부정 금지:
isDisabled: false는 독자가 한 번 반전해야 하고isEnabled: true가 더 직접적입니다. 이중 부정(isNotDisabled)은 재앙입니다. - 불리언에 숫자를 쓰지 마세요:
"enabled": 1은 모든 소비자가=== 1을 쓰게 합니다. - 문자열을 쓰지 마세요:
"enable": "true"는 JavaScript에서 참 같은 값이고if (data.enable)은"false"에서도 참입니다. 그런 버그는 추적이 극히 어렵습니다.
열거 필드는 숫자인가 문자열인가?
명확한 저장이나 대역폭 제약이 없으면 문자열을 쓰세요.
{ "status": "active" }
{ "status": 1 }과 비교해 문자열이 이기는 이유:
- 로그, 패킷 캡처, 데이터베이스가 매핑 표를 찾지 않고 직접 읽힙니다.
- 열거 값을 추가해도 기존 값의 의미가 바뀌지 않습니다(숫자 열거 가운데에 값을 삽입하면 모두 밀림).
- 프런트엔드가
1 → 'active'매핑을 따로 유지할 필요가 없습니다.
비용은 몇 바이트 더인데, 압축 후에는 거의 무시할 수 있습니다.
동반 실천: 문자열 열거 값은 소문자 + 언더스코어(pending_review)를 쓰고, Schema에서 enum으로 제약해
잘못된 값이 검증 단계에서 거부되게 하세요.
2. 설정과 픽스처
설정은 JSON, YAML, TOML 중 무엇인가?
취향이 아니라 목적으로 고르세요:
| 형식 | 좋은 곳 | 나쁜 곳 |
|---|---|---|
| JSON | 프로그램적 읽기/쓰기, 엄격한 구조, Schema 검증 | 잦은 수동 편집(주석 불가) |
| YAML | 깊이 중첩된 배포 설정(K8s) | 손으로 쓴 들여쓰기는 실수 잦음, yes/no가 불리언이 됨 |
| TOML | 평평한 의존성과 도구 설정(Cargo.toml, pyproject.toml) |
깊은 중첩의 가독성 나쁨 |
| JSONC | 주석이 필요한 에디터 설정(tsconfig.json) |
시스템 경계를 넘을 때 |
팀의 흔한 실수는 업무 설정을 YAML에 두는 것입니다. YAML의 암묵적 타입 변환(1.0이 숫자가 되고,
NO가 불리언이 되며, 2026-10-30이 날짜가 됨)이 파서마다 다른 결과를 냅니다.
설정을 프로그램으로 엄격히 검증해야 하면 JSON + Schema를 고르고, 설명은 _comment 필드나 별도 문서로 남기세요.
테스트 픽스처는 어떻게 조직하나?
"기준 하나 + 최소 diff"로 조직하세요. 테스트 사례마다 거대한 JSON을 복사하지 마세요.
fixtures/
├─ users/
│ ├─ base.json # Baseline user object
│ ├─ no-email.json # Only email is missing
│ └─ invalid-age.json # Only age is out of range
└─ fixtures.ts # Utility functions for loading and shallow merging
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
그러면 새 테스트 사례는 다른 필드만 쓰면 되고, 기준 필드를 바꾸면 모든 사례가 자동으로 갱신됩니다.
세 규칙:
- 픽스처는 Schema 검증을 통과해야 합니다: 기준 픽스처 자체가 유효한 데이터여야 합니다.
- 운영 실제 데이터를 내보내지 마세요: 전화번호, 주민등록번호, 주소는 모두 개인 정보입니다.
- 픽스처는 버전 관리에 두되 작게 유지하세요: 수십 KB를 넘는 단일 파일은 정리하거나 생성할 자격이 있습니다.
JSON 파일의 병합 충돌을 어떻게 줄이나?
JSON은 쉽게 충돌합니다. 포매터가 파일 전체를 한 번에 재배치해 두 사람이 각각 한 줄을 바꿔도 파일 전체에서 충돌합니다.
네 가지 책략:
- 형식을 통일하고
.editorconfig에 더하세요: 일관된 들여쓰기, 줄 끝, 끝 줄 바꿈이 무관한 diff를 피합니다. - 키를 정렬하세요: 알파벳순을 합의해 새 필드가 다른 사람의 삽입 지점과 충돌하지 않게 합니다.
- 파일을 나누세요: 거대한
config.json하나를 도메인별 여러 파일로 나누면 충돌 표면이 자연히 줄어듭니다. - "전체 재형식화" 커밋을 피하세요: 형식과 업무 변경을 두 커밋으로 분리해 리뷰가 읽히게 하세요.
팀이 잠금 파일(pnpm-lock.yaml, package-lock.json)을 쓴다면 손으로 병합하지 말고 재생성하세요:
git checkout --theirs pnpm-lock.yaml
pnpm install
버전 호환성을 어떻게 유지하나?
원칙은 "추가하되, 변경하지 말고, 삭제하지 말 것"입니다.
| 변경 | 호환성 | 접근 |
|---|---|---|
| 선택 필드 추가 | 호환 | 추가, 소비자는 알 수 없는 필드를 무시 |
| 필수 필드 추가 | 비호환 | 버전을 올리거나 기본값 제공 |
| 필드 제거 | 비호환 | 먼저 deprecated 표시, 호출자 관찰 후 제거 |
| 필드 타입 변경 | 비호환 | 새 필드 이름을 더하고 전환 중 듀얼 쓰기 |
| 열거 값 집합 변경 | 경우에 따라 | 값 추가는 호환, 제거는 비호환 |
지원 메커니즘:
- 소비자는 "관대한 파싱"을 씁니다. 알 수 없는 필드를 무시하고 누락 필드에 기본값을 공급합니다.
- 서버는 옛 필드를 한동안 유지하고 응답에
Deprecation헤더를 힌트로 반환합니다. - Schema에서 새 필드를
required로 표시하지 말고, deprecated 필드는deprecated: true로 태그합니다.
CI에서 계약을 어떻게 검증하나?
Schema를 코드의 일부로 취급하고 커밋마다 검증하세요.
# Validate sample data against the Schema (using ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
CI에서 세 가지를 실행하세요:
- Schema 자체의 유효성:
$schema선언이 유효한지,$ref가 해석되는지 - 픽스처가 Schema와 맞는지: 테스트 데이터가 어긋나지 않게
- 계약이 역호환인지: 이전 버전과 비교해 제거된 필드나 변경된 타입 확인
세 번째는 도구 지원(openapi-diff, json-schema-diff 등)이 필요하고,
없으면 적어도 스크립트로 required 배열과 properties 키 집합의 변경을 비교하세요.
3. 빠른 참조
| 질문 | 권장 |
|---|---|
| 응답 구조 | {"data":..., "meta":...}, 오류는 HTTP 상태 코드로 |
| 이름 스타일 | camelCase(프런트엔드 우선) 또는 snake_case(데이터 우선), 사이트 전체 일관 |
| 시간 형식 | 시간대가 있는 RFC 3339 문자열, At으로 끝나는 필드 이름 |
| null | 값 없음은 null, 무관한 필드는 생략, PATCH의 생략은 "변경 안 함" |
| 불리언 이름 | is/has/can 접두사, 부정 없음, 0/1 없음 |
| 열거 | 문자열, 소문자와 언더스코어, Schema에 enum 추가 |
| 설정 형식 | 프로그램 검증은 JSON, 수동 편집은 TOML이나 JSONC |
| 픽스처 | 기준 하나 + 최소 diff 오버라이드, Schema에 유효 |
| 병합 충돌 | 형식 통일, 키 정렬, 파일 분할 |
| 버전 호환성 | 추가만 하고, 변경·삭제 없이, 제거 전 deprecated 표시 |