JSON Toolkit

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

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

초급약 10분 소요2026-10-30 발행2026-10-30 업데이트
설정, 픽스처, 로컬 데이터를 위한 JSON 관리 표지

핵심부터 말하면, 파일 형식을 걱정하기 전에 설정을 기본값, 환경, 로컬 오버라이드로 만들고 시작 시 검증하세요. 픽스처는 시간, ID, 키 순서, 들여쓰기에서 결정적으로 만들어 diff 가능하게 유지하고, 그다음에 마스킹을 자동화하세요.

이 두 종류의 JSON은 업무 로직에 참여하지 않지만, "새 동료가 얼마나 빨리 개발 환경을 갖추는가"와 "운영 문제를 얼마나 빨리 찾는가"를 결정합니다.

1. 설정: 세 계층 구조

계층 원천 관리자 커밋
기본값 코드 안 상수 / default.json 개발자 예
환경 설정 config/production.json, 환경 변수 운영 예(비밀 제외)
로컬 오버라이드 .env.local, config/local.json 개인 아니요(gitignore)

병합 순서: 기본값 → 환경 → 로컬, 뒤 계층이 앞 계층을 덮습니다. 업무가 필요로 하는 만큼만 깊게 병합하세요. 무턱대고 딥 머지하지 마세요. 딥 머지는 "배열이 예상 밖으로 이어졌다" 같은 문제를 찾기 매우 어렵게 만듭니다.

/**
 * 우선순위에 따라 설정 계층을 병합합니다. 뒤 계층이 앞 계층을 덮고, 배열은 이어 붙이지 않고 통째로 교체합니다.
 * @param layers 우선순위가 낮은 것부터 높은 것까지의 설정 계층
 * @returns 병합된 설정 객체
 */
function mergeConfigLayers(layers: Array<Record<string, unknown>>): Record<string, unknown> {
  return layers.reduce<Record<string, unknown>>((mergedConfig, currentLayer) => {
    for (const [configKey, configValue] of Object.entries(currentLayer)) {
      const isPlainObject =
        typeof configValue === 'object' && configValue !== null && !Array.isArray(configValue);
      const existingValue = mergedConfig[configKey];

      if (isPlainObject && typeof existingValue === 'object' && existingValue !== null) {
        mergedConfig[configKey] = mergeConfigLayers([
          existingValue as Record<string, unknown>,
          configValue as Record<string, unknown>,
        ]);
        continue;
      }

      // Arrays and scalars are always replaced wholesale to avoid accidental element concatenation
      mergedConfig[configKey] = configValue;
    }
    return mergedConfig;
  }, {});
}

2. 시작 시 검증, 런타임까지 기다리지 말 것

설정 오류의 비용은 발견이 늦을수록 지수적으로 커집니다:

발견 시점 비용
시작 시 로그 한 줄, 재시작하고 넘어감
첫 요청 시 일부 기능 오류, 한 시간 디버깅
예약 작업 발동 시 한밤중 경고
정산일 대사 사고
import Ajv2020 from 'ajv/dist/2020';
import configSchema from './config.schema.json' with { type: 'json' };

const ajv = new Ajv2020({ allErrors: true, strict: true });

/**
 * 설정을 검증하고 실패 시 즉시 던져 fail fast를 구현합니다.
 * @param rawConfig 병합된 원시 설정
 * @returns 검증된 설정
 */
export function assertConfig(rawConfig: unknown): Record<string, unknown> {
  const validate = ajv.compile(configSchema);
  if (!validate(rawConfig)) {
    const messages = (validate.errors ?? []).map(
      (errorItem) => `${errorItem.instancePath || '/'} ${errorItem.message ?? ''}`,
    );
    throw new Error(`配置校验失败:\n${messages.join('\n')}`);
  }
  return rawConfig as Record<string, unknown>;
}

지지 원칙:

  • 필수 항목이 빠지면 즉시 종료하고 "그럴듯해 보이는" 기본값을 공급하지 마세요(예: 데이터베이스 주소를 localhost로 기본 설정).
  • 타입 오류에서 즉시 종료하세요. 문자열 "false"는 불리언 false가 아니므로, 환경 변수는 읽은 뒤 명시적으로 변환해야 합니다.
  • 비밀은 저장소에 절대 넣지 말고 환경 변수나 비밀 관리 서비스에서만 읽으세요.

3. 설정 파일에 어떤 형식을 쓸지

파일 형식 이유
package.json 표준 JSON 패키지 관리자가 이것만 받아들임
tsconfig.json JSONC 주석 허용, 공식 지원
.vscode/settings.json JSONC 위와 같음
업무 설정 YAML 또는 JSONC 각 항목의 의미를 설명하는 주석이 필요
환경 변수 목록 .env 평평한 키-값, 주입이 쉬움

실용적인 검사: 이 파일을 사람이 아닌 도구가 읽나? 예 → 표준 JSON, 자체 코드만 읽는다 → 방언도 괜찮습니다.

4. 테스트 픽스처: 디렉터리와 이름

tests/fixtures/
├─ valid/
│  ├─ order-minimal.json
│  ├─ order-full.json
│  └─ order-with-null-fields.json
├─ invalid/
│  ├─ order-missing-id.json
│  ├─ order-price-as-number.json
│  └─ order-nested-broken.json
└─ edge/
   ├─ big-integer.json
   ├─ emoji-and-surrogate.json
   └─ deep-nesting-64.json

좋은 이름 관례는 object-scenario입니다. order-missing-field가 test1.json보다 훨씬 유용합니다. 비 ASCII 파일 이름은 교차 플랫폼 도구 체인에서 간혹 인코딩 문제를 일으키므로, 파일 이름은 영어로 두고 내용은 중국어로 둘 수 있습니다.

4.1 세 종류의 사례, 세 종류의 기대

디렉터리 기대 목적
valid/ 검증 통과 주 흐름, 타입 생성
invalid/ 검증 실패, 오류가 예상 필드를 지목 오류 처리, 메시지 문구
edge/ 동작이 명시됨(통과나 실패 모두 괜찮지만 단언해야 함) 회귀 보호

JSONTestSuite는 같은 세 종류(받아들여야 함 / 거부해야 함 / 구현 정의)에 y_ / n_ / i_ 접두사를 쓰며, 그 이름 관례를 바로 빌려 쓸 수 있습니다.

5. 결정성: 픽스처를 diff 가능하게 만들기

불안정한 픽스처는 코드 리뷰를 무의미하게 만듭니다:

소음 원천 처리 방법
현재 시각 상수로 고정, 예: 2026-01-01T00:00:00Z
무작위 ID / UUID 증가 시퀀스나 고정 값 사용
키 순서 생성 시 알파벳순 정렬
부동소수점 형식 균일하게 소수 둘째 자리 유지 또는 문자열로 변환
시간대 어디서나 UTC 사용
들여쓰기 2 스페이스로 고정, CI에서 Prettier로 검사
/**
 * 키를 재귀적으로 정렬해 안정적이고 diff 가능한 JSON 텍스트를 만듭니다.
 * @param value 직렬화할 값
 * @param indentWidth 들여쓰기 너비
 * @returns 안정적 순서의 JSON 문자열
 */
function toStableJson(value: unknown, indentWidth = 2): string {
  /**
   * 객체 키를 재귀적으로 정렬합니다.
   * @param input 임의의 JSON 값
   * @returns 키가 정렬된 새 값
   */
  const sortKeysDeep = (input: unknown): unknown => {
    if (Array.isArray(input)) {
      return input.map((item) => sortKeysDeep(item));
    }
    if (typeof input === 'object' && input !== null) {
      return Object.fromEntries(
        Object.keys(input)
          .sort()
          .map((entryKey) => [entryKey, sortKeysDeep((input as Record<string, unknown>)[entryKey])]),
      );
    }
    return input;
  };

  return JSON.stringify(sortKeysDeep(value), null, indentWidth);
}

6. 마스킹과 컴플라이언스

실제 데이터를 픽스처로 직접 쓸 수 없습니다:

  • 이름 → "John Doe", "Jane Roe" 같은 자리표시자
  • 전화번호 → 13800138000 같은 명백히 허구인 번호
  • 주민등록번호 → 형식은 맞지만 검사 숫자가 틀린 가짜 번호
  • 주소 → 시/도 구조는 유지하고 상세 호수는 삭제
  • 금액 → 규모는 유지하되 실제 계정 정보는 절대 유지하지 않음

팀에 scripts/mask-fixture.mjs가 있어 내보낸 운영 샘플 위에서 실행해 마스킹 버전을 생성하는 것이 좋습니다. "손으로 세 곳을 고치고 나머지를 잊는" 일을 피합니다.

7. 이전과 버전

픽스처도 낡습니다. 픽스처에 schemaVersion 필드를 더하거나 디렉터리에 VERSION 파일을 두세요:

{
  "schemaVersion": 3,
  "id": "1001",
  "amount": "89.50"
}

API 계약이 업그레이드될 때:

  1. 새 픽스처 디렉터리(v3/)를 더하고 옛 픽스처를 제자리에서 수정하지 마세요.
  2. 옛 API 버전이 은퇴할 때까지 회귀용 옛 픽스처를 유지하세요.
  3. CI에서 두 버전을 실행해 역호환성을 확인하세요.

8. 체크리스트

  • [ ] 세 계층 설정 구조가 명확하고 로컬 오버라이드가 커밋되지 않습니다
  • [ ] 검증이 시작 시 일어나고 실패 시 즉시 종료합니다
  • [ ] 비밀은 환경 변수에서만 오고 설정 파일에는 자리표시자만 있습니다
  • [ ] 픽스처가 valid/invalid/edge 디렉터리로 나뉘고 이름이 시나리오를 표현합니다
  • [ ] 픽스처가 결정적(고정 시간, ID, 순서)이고 diff 가능합니다
  • [ ] 실제 데이터가 마스킹 스크립트로 처리되었습니다
  • [ ] 계약 업그레이드가 제자리 수정이 아니라 새 버전 디렉터리를 더합니다

FAQ

설정 계층은 어떤 순서로 병합해야 하나?

기본값, 환경 설정, 로컬 오버라이드 세 계층을 쓰고 기본값 → 환경 → 로컬 순으로 병합하며, 뒤 계층이 앞 계층을 덮습니다. 업무가 필요로 하는 만큼만 깊게 병합하고 배열과 스칼라는 통째로 교체해 딥 머지가 배열을 예상 밖으로 잇지 않게 하세요. 개인 설정을 커밋하지 말고 로컬 오버라이드 파일을 gitignore하세요.

설정을 시작 시 검증해야 하는 이유는?

설정 오류의 비용은 발견이 늦을수록 지수적으로 커집니다. 시작 시 로그 한 줄과 정산일의 대사 사고를 비교하세요. 시작 시 JSON Schema로 검증하고 필수 항목 누락이나 타입 오류에서 즉시 종료하며, "그럴듯해 보이는" 기본값을 공급하지 마세요.

운영 데이터를 테스트 픽스처로 직접 써도 되나?

아니요. 이름, 전화번호, 주민등록번호, 주소, 금액 모두 자리표시자나 명백히 허구인 값이 필요하고, 팀에 scripts/mask-fixture.mjs가 있어 운영 내보내기 위에서 실행해 마스킹 버전을 생성해야 합니다. 손편집으로 무언가를 놓치는 일을 피합니다.