JSON Toolkit

JSON ↔ YAML과 XML 변환의 경계

핵심 요약: JSON이 YAML의 부분 집합이라는 주장은 한 방향만 보장하며, XML에는 공식 JSON 매핑이 전혀 없어 모든 것이 관례에 달려 있습니다.

중급약 12분 소요2026-10-30 발행2026-10-30 업데이트
JSON ↔ YAML과 XML 변환 경계 표지

핵심부터 말하면, JSON을 중간 형식으로 쓰세요. 소스를 데이터 모델로 파싱한 뒤 대상으로 직렬화합니다. 변환 전에 주석, XML 속성과 네임스페이스, 숫자와 불리언 타입이 살아남아야 하는지 결정하세요.

변환 파이프라인이 쓸 만한지는 출력이 얼마나 예쁜지가 아니라, 무엇을 얼마나 버렸는지 인지하고 있는지에 달려 있습니다.

1. JSON과 YAML: 단방향 포함

YAML 1.2 명세는 분명히 말합니다. JSON은 YAML의 부분 집합입니다. 즉 유효한 JSON은 모두 유효한 YAML입니다:

{"name": "json", "tags": ["a", "b"], "count": 3}

이 조각은 JSON이기도 하고 YAML 플로 스타일이기도 합니다. 반대는 성립하지 않습니다.

기능 JSON YAML 1.2
주석 ✗ ✓
앵커와 별칭 ✗ ✓
여러 문서(---) ✗ ✓
문자열이 아닌 키 ✗ ✓ (1: a, true: b)
복합 키 ✗ ✓ (? [a, b])
들여쓰기 민감 ✗ ✓ (탭 들여쓰기는 거부됨)
타입 추론 명시적 암묵적(0o17, 0x1F, .inf)
단일 줄 가독성 좋음 나쁨(긴 내용은 폴딩 문법에 의존)

1.1 타입 추론이 가장 큰 함정

YAML은 문자열처럼 보이는 일부 값을 다른 타입으로 파싱합니다:

zip: 01234        # string or octal integer? the YAML 1.2 core schema treats it as a string
version: 1.10     # 1.1 (a float); the semantics are lost
country: NO       # a string in YAML 1.2; boolean false in YAML 1.1
date: 2026-10-30  # some implementations parse it as a date object

NO 사례는 많은 사람을 태웠습니다. YAML 1.1 파서(일부 오래된 라이브러리)에서는 country: NO가 false가 됩니다. 대응책: "숫자나 불리언처럼 보이는" 모든 식별자에 따옴표를 붙이세요. 예: version: "1.10".

1.2 앵커, 별칭, 여러 문서

defaults: &defaults
  retries: 3
  timeout: 30

serviceA:
  <<: *defaults
  host: a.example.com

---
serviceB:
  <<: *defaults
  host: b.example.com

앵커를 확장해 JSON으로 변환하면 대개 중복된 설정 객체 두 개가 되거나, 하나의 배열이 됩니다(여러 문서는 [{...}, {...}]가 되어야 함). 어느 쪽이든 역변환에서 앵커는 복원되지 않습니다. 이 손실은 불가피합니다.

1.3 주석을 보존하는 변환

대부분 라이브러리는 변환 중에 주석을 그냥 버립니다. 주석을 유지해야 할 때는 yaml 패키지의 Document API를 쓰세요:

import { parseDocument } from 'yaml';

const document = parseDocument(`
# Service configuration
port: 8080
`);

document.set('port', 3000);
console.log(document.toString());
// # Service configuration
// port: 3000

검사는 간단합니다. 이 파일을 사람이 계속 편집할 것이라면 "파싱 → 객체 → 직렬화" 경로를 타지 마세요.

1.4 왕복 일관성을 검증하는 방법

/**
 * JSON → YAML → JSON의 왕복 일관성을 검증합니다.
 * @param sourceValue 원본 JSON 값
 * @param stringify YAML 직렬화 함수
 * @param parse YAML 파싱 함수
 * @returns 두 결과가 정확히 같은지 여부
 */
function isRoundTripStable(
  sourceValue: unknown,
  stringify: (input: unknown) => string,
  parse: (input: string) => unknown,
): boolean {
  const yamlText = stringify(sourceValue);
  const restored = parse(yamlText);
  return JSON.stringify(restored) === JSON.stringify(sourceValue);
}

실제 데이터에 돌려 보는 것이 문서를 열 번 읽는 것보다 값어치가 큽니다. 샘플 집합이 다음을 덮게 하세요. null, 빈 문자열, 빈 배열, "00", "1e5", 음의 0, 2^53을 넘는 정수, 줄 바꿈이 포함된 긴 문자열.

2. JSON과 XML: 표준 매핑이 없음

XML은 JSON보다 오래되었고 구조 능력(속성, 네임스페이스, 혼합 콘텐츠, DTD)이 훨씬 강합니다. 둘 사이에는 공식 매핑 명세가 없습니다. 모든 도구가 같은 일을 합니다. 관례를 고르고, 많이 잃지 않도록 노력합니다.

2.1 흔한 관례

관례 표현 예
요소 → 객체 키 같은 이름의 요소가 필드가 됨 <a>1</a> → {"a": "1"}
속성 → @ 접두사 @name <a id="1"/> → {"a": {"@id": "1"}}
텍스트 → #text 속성과 텍스트가 모두 있을 때 <a id="1">hi</a> → {"a": {"@id": "1", "#text": "hi"}}
반복 요소 → 배열 두 번 이상 나타나면 배열 <i>1</i><i>2</i> → {"i": ["1","2"]}
네임스페이스 → 접두사 ns:tag <ns:tag/> → {"ns:tag": ...}
<order id="A1001">
  <item sku="X1" qty="2">笔记本</item>
  <item sku="X2" qty="1">钢笔</item>
</order>
{
  "order": {
    "@id": "A1001",
    "item": [
      { "@sku": "X1", "@qty": "2", "#text": "笔记本" },
      { "@sku": "X2", "@qty": "1", "#text": "钢笔" }
    ]
  }
}

2.2 불가피한 다섯 가지 손실

  1. 요소 순서: JSON 객체는 순서가 없고, XML은 요소의 정렬된 시퀀스입니다.
  2. 속성 대 자식 요소: XML에서는 서로 다른 의미를 지니지만 JSON에서는 모두 평범한 필드가 됩니다.
  3. 혼합 콘텐츠: <p>你好 <b>世界</b> 再见</p> 같은 구조는 평범한 객체로 표현할 수 없습니다.
  4. 네임스페이스 선언: xmlns는 스코프 개념이며, 필드로 평탄화되면 스코프가 사라집니다.
  5. CDATA와 엔티티: <![CDATA[...]]>와 &amp;는 JSON에서 텍스트일 뿐입니다.

2.3 "하나의 요소인가 배열인가"의 딜레마

<list><item>1</item></list>

item이 하나면 xml2js는 기본적으로 객체를 반환하고, 두 개가 되어야 배열을 반환합니다. 그래서 프런트엔드 코드는 Array.isArray(x) ? x : [x]를 써야 합니다.

대응책: 파싱할 때 "항상 배열" 옵션을 켜거나, 스키마를 근거로 필드를 배열 타입으로 선언하세요.

import { parseStringPromise } from 'xml2js';

const parsed = await parseStringPromise(xmlText, {
  explicitArray: true,   // return an array even for a single element
  trim: true,
  explicitRoot: false,
});

2.4 보안 알림

신뢰할 수 없는 XML을 파싱하기 전에 외부 엔티티를 반드시 비활성화해야 합니다. 그렇지 않으면 XXE(로컬 파일 읽기, 내부 네트워크 탐색)를 유발할 수 있습니다:

// In the Node ecosystem, DTDs and external entities are usually disabled through parser options
const parserOptions = { doctype: false, externalEntities: false };

XML → JSON은 변환의 첫 계층일 뿐입니다. 변환이 XML이 가져오는 공격 표면을 없애지는 않습니다.

3. 세 형식이 각각 어울리는 곳

시나리오 권장 형식 이유
공개 HTTP API JSON 브라우저와 모든 언어의 네이티브 지원
사람이 관리하는 설정 YAML 주석, 들여쓰기, 가독성
강한 스키마와 네임스페이스가 필요한 문서(SOAP, Office Open XML, SVG) XML 구조 능력과 기존 생태계
운영 도구(K8s, CI)와 상호작용 YAML 생태계 기본값
영속화, 캐시, 로그 JSON / JSONL 보편적이고 의존성 없음

4. 구현 노트

import { stringify as toYaml } from 'yaml';

/**
 * 중국어 텍스트를 이스케이프하지 않은 상태로 JSON 값을 YAML 텍스트로 변환합니다.
 * @param value 변환할 JSON 값
 * @returns YAML 텍스트
 */
function jsonToYaml(value: unknown): string {
  return toYaml(value, {
    indent: 2,
    lineWidth: 0,      // disable automatic wrapping so long strings are not split
    defaultStringType: 'QUOTE_DOUBLE',
  });
}

실용적인 옵션 몇 가지:

  • lineWidth: 0은 자동 줄 바꿈을 끕니다. 그렇지 않으면 긴 URL이 여러 줄로 나뉘어 읽기 어려워집니다.
  • 모든 값에 큰따옴표를 붙이면 NO, 1.10 같은 값이 잘못 읽히지 않습니다.
  • 숫자 정밀도가 민감한 데이터는 직렬화 전에 문자열로 변환하세요.

XML 방향에서는 하나의 매핑 함수를 고정하고 "속성 접두사", "텍스트 키 이름", "배열 전략"을 함수 서명에 넣어 흩어지지 않게 하세요:

type XmlMappingOptions = {
  attributePrefix: string;
  textKey: string;
  alwaysArray: boolean;
};

const defaultMapping: XmlMappingOptions = {
  attributePrefix: '@',
  textKey: '#text',
  alwaysArray: false,
};

5. 체크리스트

  • [ ] YAML에서 "숫자/불리언처럼 보이는" 모든 식별자에 따옴표가 있습니다
  • [ ] JSON → YAML → JSON 검증 샘플 집합이 정의되고 테스트가 덮습니다
  • [ ] 주석을 보존해야 할 때 객체 왕복 대신 Document API를 씁니다
  • [ ] XML 매핑 관례가 문서화되고 팀이 한 세트의 파라미터를 공유합니다
  • [ ] 신뢰할 수 없는 XML을 파싱할 때 DTD와 외부 엔티티를 비활성화합니다
  • [ ] 변환 전후 바이트 수와 필드 수를 비교하고 이상이 있으면 경고를 냅니다

FAQ

JSON, YAML, XML 사이의 변환 방향을 어떻게 고르나?

JSON을 중간 형식으로 선호하세요. YAML과 XML을 먼저 데이터 모델로 파싱한 뒤 텍스트를 다시 쓰는 대신 그 모델에서 직렬화합니다. 주석이 살아남아야 하면 YAML Document API를 쓰고, 속성이나 네임스페이스가 필요하기 전에 하나의 XML 매핑 관례를 고정하세요.

XML-to-JSON 변환이 속성과 네임스페이스를 잃는 이유는?

JSON에는 속성이나 네임스페이스 개념이 없으므로, XML 속성, 접두사, 네임스페이스 URI를 평범한 키로 접어 넣어야 하며, 흔히 @ 접두사나 #text 노드를 씁니다. 매핑 규칙이 갈라지면 같은 XML이 다른 JSON을 만들어 왕복할 수 없습니다.

JSON, YAML, XML을 장기 양방향 동기화해야 하나?

아니요. 주석, 앵커, XML 속성, 네임스페이스는 형식 간에 손실되며, 양방향 동기화는 차이를 계속 누적합니다. 하나의 원본 형식을 고르고 나머지는 일회성 내보내기나 임시 뷰로 취급하세요.

참고 자료

관련 문서

JSON과 CSV 및 표 형식 데이터 변환

CSV는 이차원이고 JSON은 트리 모양입니다. 변환의 어려운 점은 코드가 아니라, 배열을 어떻게 평탄화하고 타입을 어떻게 보존할지 미리 결정하는 것입니다.

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

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

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

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

JSON을 YAML로 바꾸는 커맨드라인: yq 명령 하나에 숨은 네 가지 함정

튜토리얼에서 가장 자주 등장하는 yq -P input.json > output.yaml은 실측에서도 여전히 JSON을 출력합니다. 이 글은 yq v4.44.6, PyYAML 6.0.3, js-yaml을 2.39 MB 샘플에 돌려 출력 형식이 확장자로 결정된다는 사실을 네 세트의 데이터로 보여주고, 일괄 변환·따옴표·들여쓰기·큰 정수 정밀도의 올바른 작성법을 제시합니다.

중급약 9분 소요2026-11-03 업데이트