모든 프로젝트에는 설정이 필요하고, 주요 선택지는 세 가지입니다: JSON, YAML, TOML. 세 가지 모두 광범위하게 사용해봤는데, 각각 다른 방식으로 저를 미치게 합니다. 언제 무엇을 써야 하는지에 대한 솔직한 의견을 말씀드리겠습니다.

JSON: 만능 병사

JSON은 이미 알고 계시죠. 어디에나 있습니다. 모든 언어가 파싱할 수 있습니다. 하지만 설정 파일로서의 약점에 대해 솔직해지겠습니다:

  • 주석 불가. 설정 항목이 무엇을 하는지 말 그대로 설명할 수 없습니다. 설정 파일로서 이건 말이 안 됩니다.
  • 후행 쉼표 불가. 끝에 새 줄을 추가하고, 이전 줄에 쉼표를 잊으면 — 설정이 깨집니다.
  • 모든 곳에 따옴표. 모든 키에 큰따옴표가 필요합니다: port: 8080 대신 {"port": 8080}.

그럼에도 JSON은 npm(package.json), TypeScript(tsconfig.json), VS Code(settings.json) 등에서 설정에 사용됩니다. 이유는? 모호함 제로. JSON은 너무 엄격해서 해석 방법이 딱 하나뿐입니다.

YAML: 아름답지만 위험한

YAML은 멋지게 생겼습니다. 깔끔하고, 미니멀하고, 사람이 읽기 쉽습니다. 하지만 악명 높은 함정이 있습니다:

yaml

이것이 YAML 글에서 언급한 "노르웨이 문제"입니다. YAML 1.1은 NO, YES, ON, OFF를 불리언으로 처리합니다. YAML 1.2에서 수정되었지만, 많은 파서가 여전히 1.1 동작을 기본으로 사용합니다.

그리고 들여쓰기 민감성이 있습니다. 공백 하나만 잘못 놓으면 파일의 의미가 완전히 바뀔 수 있고 — 에러 메시지는 끔찍할 것입니다.

그래도 YAML은 Docker Compose, Kubernetes, GitHub Actions, Ansible의 표준입니다. DevOps/클라우드 분야에서 일한다면 YAML 숙련은 필수입니다.

TOML: 설정 파일 전문가

TOML(Tom's Obvious Minimal Language)은 설정 전용으로 설계되었습니다. 명확함을 목표로 합니다 — 즉, TOML 파일을 잘못 읽을 방법이 (거의) 없다는 뜻입니다.

TOML 설정은 이렇게 생겼습니다:

toml

TOML이 정말 잘하는 것: 네이티브 날짜/시간 타입(created = 2026-03-08T10:30:00Z), 들여쓰기 문제 없음, #으로 주석, 그리고 진짜 틀리기 어려운 문법.

단점은? 깊게 중첩된 구조가 장황해집니다. TOML은 평평한 설정에는 훌륭하지만, 5단계 중첩이 필요하다면 YAML이나 JSON이 더 읽기 쉬울 수 있습니다.

TOML은 Rust(Cargo.toml), Python(pyproject.toml), Hugo, 그리고 점점 더 많은 도구에서 사용됩니다.

추천 사항

시나리오선택이유
기계 생성 설정JSON가장 엄격하고 호환성 높음
DevOps/인프라YAML생태계가 요구함
앱 설정TOML주석 + 최소 모호함
단순 키-값 설정TOML평평한 설정에 가장 깔끔
복잡한 중첩 구조YAML최적의 중첩 문법

결론

세 가지 형식으로 보는 동일한 설정

같은 데이터를 세 가지 형식으로 보면 차이점이 확실히 드러납니다. 간단한 앱 설정을 예로 들어보겠습니다:

JSON:

json

YAML:

yaml

TOML:

toml

JSON은 모든 따옴표와 중괄호가 필요하고, YAML은 가장 간결하지만 들여쓰기에 의존하며, TOML은 명시적 섹션 헤더와 들여쓰기 의존성 없이 중간 지점을 찾는다는 점에 주목하세요.

흔한 설정 파일 실수

YAML: 의도하지 않은 타입 변환. 노르웨이 문제 외에도, YAML은 3.10을 숫자 3.1로 해석하고(뒤의 0을 버림), 1_0001000으로 해석합니다. 설정 값이 3.10 같은 버전 문자열이라면 항상 따옴표로 감싸세요: version: "3.10".

JSON: 순서가 중요하지 않다는 것을 잊기. JSON 객체는 사양상 순서가 없습니다. 설정 처리가 키 순서에 의존한다면 취약한 기반 위에 구축하는 것입니다. 삽입 순서를 유지하는 파서도 있고 그렇지 않은 파서도 있습니다.

TOML: 테이블 배열과의 혼동. TOML은 테이블 배열에 [[이중.대괄호]]를 사용하는데, 초보자가 헷갈려합니다. 여러 서버를 정의하는 방법:

toml

환경별 설정

세 가지 형식 모두가 어려워하는 영역 중 하나는 환경별 오버라이드(개발 vs 스테이징 vs 프로덕션) 처리입니다. 일반적인 해결책은 다음과 같습니다:

  • 다중 파일: config.base.yaml + config.production.yaml을 딥 머지
  • 환경 변수 보간: 일부 YAML 도구는 ${DB_HOST} 구문을 지원하지만 표준은 아님
  • 외부 도구: Doppler, HashiCorp Vault, 또는 클라우드 네이티브 설정 서비스

개인적으로 단순한 접근법을 선호합니다: 합리적인 기본값이 있는 설정 파일 하나와, 환경 간에 달라지는 것들을 위한 환경 변수. 세 가지 형식 모두 애플리케이션 수준에서 환경 변수를 읽을 수 있으므로, 설정 형식 자체가 보간을 지원할 필요는 없습니다.

생태계별 형식 인기도

생태계주요 형식대표 예시
JavaScript/Node.jsJSONpackage.json, tsconfig.json, .eslintrc.json
PythonTOMLpyproject.toml, Cargo.toml (Rust)
DevOps/CloudYAMLdocker-compose.yml, k8s 매니페스트, GitHub Actions
GoTOML/YAML둘 다 널리 사용, 단일 표준 없음
.NETJSONappsettings.json (XML 기반 web.config을 대체)

결론

하나의 정답은 없습니다. 최대 호환성이 필요하면 JSON을 사용하세요. 생태계가 요구하면 YAML을 사용하세요(Kubernetes가 TOML을 받아들이기 시작할 일은 없습니다). 주석과 최소한의 놀라움을 원하면 TOML을 사용하세요.

JSONC와 JSON5: 보조 바퀴 달린 JSON

자, 우리는 JSON이 주석과 후행 쉼표를 지원하지 않는다고 비판해왔습니다. 하지만 아무도 알려주지 않는 사실이 있습니다: 실제로 이런 불편함을 해결하는 JSON 변형이 존재하고, 여러분은 아마 하나를 모르고 사용해왔을 겁니다.

먼저: JSONC (JSON with Comments). tsconfig.json을 열어서 "잠깐, 여기 주석이 있네... JSON은 주석을 허용하지 않는 거 아니었나?"라고 생각한 적이 있다면 — 네, 그게 JSONC입니다. 기본적으로 JSON인데 // 한 줄 주석과 /* */ 블록 주석을 사용할 수 있습니다. VS Code는 JSONC를 사용하여 settings.json, launch.json, keybindings.json 파일을 관리합니다. TypeScript의 tsconfig.json도 기술적으로 JSONC이지 엄격한 JSON이 아닙니다.

JSONC는 이렇게 생겼습니다:

jsonc

그리고 JSON5가 있는데, 이건 더 나아갑니다. JSON5는 JSON의 가장 엄격한 규칙 여러 개를 완화합니다:

  • 작은따옴표 문자열: {'name': 'Sarah'} — 드디어!
  • 후행 쉼표: {"a": 1, "b": 2,} — 더 이상 diff 노이즈 없음
  • 따옴표 없는 키(유효한 식별자인 경우): {name: "Sarah"}
  • 16진수: 0xFF
  • 백슬래시 연속 여러 줄 문자열
  • Infinity, -Infinity, NaN을 유효한 숫자로 사용 가능

JSON5는 사람이 주요 독자인 설정 파일에 훌륭합니다. 일부 도구는 네이티브로 지원합니다 — 예를 들어 Babel의 .babelrc는 JSON5가 될 수 있습니다. 하지만 API 응답이나 데이터 교환에 JSON5를 사용하지 마세요. 엄격한 JSON의 의미는 모두가 형식에 동의한다는 것입니다. JSON5는 사람을 위한 것이지 기계를 위한 것이 아닙니다.

저의 원칙: 도구가 JSONC나 JSON5를 지원하면 사용하세요. 설정 파일에 주석이 있는 것에 단점은 말 그대로 없습니다. 하지만 설정을 읽는 라이브러리를 작성한다면, 표준 JSON을 먼저 지원하고 JSONC/JSON5는 선택적 추가로 두세요.

YAML 앵커와 별칭: DRY 설정

여기서부터 많은 개발자가 발견하지 못하는 YAML의 진짜 킬러 기능이 나옵니다: 앵커와 별칭. 설정 블록을 한 번 정의하고 어디서든 재사용할 수 있습니다. 기본적으로 설정 파일을 위한 DRY(Don't Repeat Yourself)입니다.

앵커는 &이름으로 정의하고 별칭은 *이름으로 참조합니다. 실용적인 Docker Compose 예시를 보겠습니다:

yaml

무슨 일이 일어났는지 보이시나요? 공통 설정을 &common으로 한 번 정의하고 <<: *common으로 세 개 서비스에 가져왔습니다. 앵커가 없으면 재시작 정책과 로깅 설정을 모든 서비스에 복사 붙여넣기해야 합니다. 로그 로테이션을 변경해야 할 때? 열두 곳이 아니라 한 곳만 수정하면 됩니다.

앵커는 더 단순한 값에도 사용할 수 있습니다 — 맵만이 아닙니다:

yaml

이제 주의사항입니다. 앵커를 정말 강력하게 만드는 머지 키 <<는 사실 YAML 1.2 사양의 일부가 아닙니다. YAML 1.1의 타입 확장이었고, 대부분의 인기 파서가 여전히 지원하지만 기술적으로는 비표준입니다. 엄격한 YAML 1.2 파서를 사용한다면 머지 키가 작동하지 않을 수 있습니다.

또한 앵커는 단일 파일 내에서만 작동합니다. 다른 YAML 파일에서 정의된 앵커를 참조할 수 없습니다. 별칭 이름을 틀렸을 때 에러 메시지는? 보통 "undefined alias" 같은 것으로 앵커가 어디에 있어야 했는지에 대한 컨텍스트는 전혀 없습니다. 전형적인 YAML입니다.

TOML 심층 분석: 고급 기능

대부분의 사람들은 TOML 기본을 알고 있습니다 — [대괄호]로 섹션, 키-값 쌍, #으로 주석. 하지만 TOML에는 충분히 주목받지 못하는 정말 멋진 기능들이 있습니다. 하나씩 살펴보겠습니다.

점 키를 사용하면 섹션 헤더 없이 중첩 구조를 정의할 수 있습니다:

toml

중첩된 값이 몇 개뿐이고 전체 섹션을 만들고 싶지 않을 때 유용합니다.

인라인 테이블은 작은 객체를 위한 JSON 스타일의 간결한 문법을 제공합니다:

toml

인라인 테이블은 아껴서 사용하세요 — 여러 줄에 걸칠 수 없고, 너무 많이 넣으면 금방 읽기 어려워집니다.

여러 줄 문자열은 두 가지 종류가 있는데, 여기서 TOML은 놀랍도록 사려깊습니다:

toml

트리플 따옴표 기본 문자열(""")은 이스케이프 시퀀스를 처리하고, 리터럴 버전(''')은 모든 것을 원시 텍스트로 취급합니다. 백슬래시를 이스케이프로 해석하지 않으려는 정규식 패턴이나 Windows 파일 경로에 완벽합니다.

네이티브 날짜/시간 타입은 JSON이나 YAML 어느 쪽도 이렇게 깔끔하게 다루지 못합니다:

toml

이것들은 TOML의 일급 타입이며, 날짜인 척하는 문자열이 아닙니다. TOML 파서는 실제 날짜/시간 객체를 반환하며, 직접 파싱해야 하는 문자열이 아닙니다.

Rust의 Cargo는 왜 TOML을 선택했을까요? Cargo 설정이 정확히 TOML의 최적점이기 때문입니다: 적당히 중첩되고, 사람이 편집하며, 주석이 필요하고, 엄격한 타이핑의 혜택을 받습니다. 의존성 버전이 조용히 float로 해석되는 건 원하지 않잖아요(보고 있다, YAML).

보안 고려사항

자, 이 섹션은 좀 무서운 이야기가 나옵니다. 신뢰할 수 없는 소스에서 설정 파일을 로드하고 있다면 — 또는 그렇지 않다고 생각하더라도 — 역직렬화 공격에 대해 알아야 합니다.

대표적인 예시는 PyYAML의 yaml.load()입니다. 이전 버전에서는 이 무해하게 보이는 함수가 YAML 파일에 내장된 임의의 Python 코드를 실행할 수 있었습니다. 농담이 아닙니다. 이것을 보세요:

yaml

누군가 이것을 YAML 설정에 몰래 넣고 Python 앱이 yaml.safe_load() 대신 yaml.load()로 로드하면, 말 그대로 그 시스템 명령을 실행합니다. 모든 것을 삭제. 백도어 설치. 공격자가 원하는 것은 무엇이든.

수정은 아주 간단합니다 — Python에서 항상 yaml.safe_load()를 사용하세요:

python

PyYAML 문서는 이제 이에 대해 경고하고 있으며, 최신 버전에서는 Loader를 지정하지 않고 yaml.load()를 사용하면 사용 중단 경고가 표시됩니다. 하지만 여전히 수많은 튜토리얼과 Stack Overflow 답변이 안전하지 않은 버전을 보여주고 있습니다. 눈 앞에 숨겨진 지뢰입니다.

그리고 "billion laughs" 공격(XML 폭탄이라고도 불리지만 YAML에서도 작동합니다)이 있습니다. 아이디어는 재귀적 확장입니다 — 다른 엔티티를 참조하는 엔티티를 정의하여 기하급수적 성장을 만듭니다:

yaml

각 레벨에서 데이터가 5배 증가하므로, 레벨 8이나 9에 도달하면 몇 줄의 YAML에서 기가바이트의 데이터가 됩니다. 대부분의 최신 YAML 파서에는 이를 방지하기 위한 깊이 및 확장 제한이 있지만 알아둘 가치가 있습니다.

JSON은 본질적으로 더 안전합니다. 실행 시맨틱이 전혀 없기 때문입니다. 코드를 내장할 방법이 없고, 타입 생성자도 없고, 엔티티 확장도 없습니다. JSON 파서는 그저 데이터를 읽습니다 — 문자열, 숫자, 불리언, 배열, 객체. 그게 전부입니다. 이것은 JSON 단순함의 과소평가된 장점 중 하나입니다. 보안이 중요할 때, JSON의 기능 부재는 사실상 기능입니다.

TOML도 꽤 안전합니다 — 코드 실행 기능이나 재귀 확장이 없습니다. 하지만 JSON 파서만큼 실전에서 검증되지는 않았으므로, TOML 라이브러리를 최신 상태로 유지하세요.

요약: 사용자나 외부 소스에서 설정 파일을 받아들인다면, JSON이 가장 안전한 선택입니다. YAML을 써야 한다면, 항상 안전한 로딩 함수를 사용하고 의심스러운 구조를 감지하기 위해 YAML 린터 실행을 고려하세요.

실제 설정 파일 예시

이론은 좋지만, 실제로 마주치게 될 설정 파일을 살펴보겠습니다. 각 파일에 형식별 패턴을 강조하는 주석을 달았습니다.

GitHub Actions 워크플로우 (YAML):

yaml

YAML이 정말 빛나는 부분입니다. GitHub Actions 워크플로우 문법은 JSON으로는 고통스러울 것입니다 — 모든 중첩된 리스트와 들여쓰기 기반 구조가 YAML에 자연스럽게 매핑됩니다. 주석이 매트릭스 전략을 설명하는 데 도움이 되는 것도 보입니다.

Rust의 Cargo.toml (TOML):

toml

Cargo 매니페스트가 기능이 있는 의존성에 인라인 테이블을 사용하는 것에 주목하세요. [[bin]] 이중 대괄호 문법은 테이블 배열을 정의합니다 — 각 [[bin]] 항목이 또 다른 바이너리 타겟을 추가합니다. TOML은 들여쓰기 트릭 없이 평평하고 읽기 쉽게 유지합니다.

package.json (JSON):

json

클래식입니다. 주석 없고, 따옴표 투성이지만, 지구상의 모든 도구가 읽을 수 있습니다. JSON의 한계에도 불구하고 package.json이 작동하는 이유는 스키마가 너무 유명해서 — scripts.build가 무엇을 하는지 설명하는 주석이 필요 없습니다. 모든 JavaScript 개발자가 이미 알고 있으니까요.

.prettierrc (JSON):

json

단순한 평평한 키-값 설정입니다. 솔직히 TOML(주석!)이나 JSONC로 하면 약간 더 좋을 텐데, Prettier는 다른 형식도 지원합니다. 하지만 JSON이 기본이고, 이렇게 간단한 것에는 큰 차이가 없습니다.

형식 간 마이그레이션

프로젝트의 설정 형식이 실수였다고 결정하고 바꾸고 싶다고 합시다. 또는 다른 형식을 사용하는 도구에서 설정을 가져오고 있을 수도 있습니다. 어떤 경우든 TOML, YAML, JSON 간 변환이 필요합니다. 말해두자면, 기대만큼 순조롭지 않을 때가 있습니다.

변환 도구:

  • yq — YAML의 스위스 아미 나이프. YAML, JSON, TOML, XML 간 변환 가능. yq -o=json config.yaml로 JSON 출력. jq의 YAML 버전입니다.
  • toml-clitaplo — 커맨드라인 TOML 프로세서. Taplo는 특히 TOML 포매팅과 검증에 뛰어납니다.
  • Python 한 줄 명령 — 급할 때 python -c "import yaml, json, sys; print(json.dumps(yaml.safe_load(sys.stdin), indent=2))" < config.yaml로 YAML을 JSON으로 변환. 예쁘진 않지만 작동합니다.
  • 온라인 변환기 — 빠른 일회성 변환에는 저희 포매터가 도움될 수 있습니다. 적절한 포매터에 설정을 붙여넣고, 정리한 다음, 대상 형식으로 수동으로 다시 작성하세요.

마이그레이션 시기:

  • 팀이 CI 설정에서 YAML 들여쓰기 오류를 계속 만든다면? TOML로 전환하면 그 골칫거리가 줄어들 수 있습니다.
  • JSON 설정 파일에 주석이 필요하다면? JSONC(도구가 지원하는 경우)나 TOML로 마이그레이션을 고려하세요.
  • 새 Rust/Python 프로젝트를 구축 중이고 생태계가 TOML을 기대한다면? 저항하지 말고 TOML로 가세요.

마이그레이션 시 흔한 함정:

YAML의 암시적 날짜 파싱에 당할 수 있습니다. release: 2024-03-08 같은 YAML 값이 있으면, YAML은 이를 문자열이 아닌 날짜 객체로 해석합니다. JSON으로 변환하면 변환기에 따라 "release": "2024-03-08T00:00:00Z""release": "2024-03-08"을 얻을 수 있습니다. 항상 출력을 테스트하세요.

TOML의 엄격한 타이핑은 혼합 타입 배열을 가질 수 없다는 의미입니다. JSON과 YAML에서 [1, "two", true]는 완벽히 유효합니다. TOML에서는 배열의 모든 요소가 같은 타입이어야 합니다. 소스 데이터에 혼합 배열이 있으면 재구성이 필요합니다.

그리고 큰 포인트: 주석이 사라집니다. JSON은 주석을 지원하지 않으므로, TOML이나 YAML에서 JSON으로 변환하면 정성들여 작성한 주석이 모두 사라집니다. 반대 방향(JSON에서 YAML/TOML)에서는 명확하지 않은 설정을 설명하기 위해 수동으로 주석을 추가하고 싶을 것입니다. JSON 버전에는 분명히 주석이 없었으니까요.

한 가지 더 — YAML의 앵커와 별칭에는 JSON이나 TOML에서의 동등한 기능이 없습니다. YAML 설정이 DRY 설정을 위해 앵커에 의존한다면, 다른 형식으로 변환할 때 공유 블록을 수동으로 복제해야 합니다. 복잡한 설정의 경우 파일 크기가 상당히 커질 수 있습니다.

직접 해보세요

어떤 형식을 선택하든, 설정을 잘 포매팅해두면 리뷰와 디버깅이 쉬워집니다. TOML Formatter로 지저분한 TOML 파일을 즉시 깔끔하게 정리할 수 있습니다. YAML Formatter는 문제가 발생하기 전에 들여쓰기 문제를 수정합니다. 그리고 JSON Formatter는 깊게 중첩된 JSON 설정도 한눈에 읽을 수 있게 만들어줍니다.