Rust 프로젝트를 열면 Cargo.toml이 있습니다. GitHub 저장소를 클론하면 YAML로 가득 찬 .github/workflows/ 폴더를 볼 수 있습니다. 두 형식 모두 같은 표면적인 작업을 수행합니다 — 인간이 편집하는 구조화된 설정 저장 — 하지만 매우 다른 절충점을 만듭니다. YAML이 조용히 값을 망가뜨린 경험이 있거나, Rust가 패키지 매니페스트에 YAML 대신 TOML을 선택한 이유가 궁금하다면, 이 글이 바로 당신을 위한 것입니다.

핵심 차이점: 명시적 vs 암묵적

TOML과 YAML의 근본적인 분리는 파서가 얼마나 추측하도록 허용되는지에 관한 것입니다. TOML은 명시적입니다: 모든 값은 모호하지 않은 유형을 가집니다. 문자열은 항상 따옴표로 묶입니다. 불리언은 정확히 true 또는 false입니다. 날짜/시간은 자체 구문이 있는 1등급 값입니다. 암묵적 유형 강제 변환이 없습니다 — 파서는 영리하게 행동하려 하지 않습니다.

YAML은 반대 방향으로 기웁니다. 편리하게 하려 합니다: 대부분의 문자열에 따옴표가 필요 없고, 파서는 값의 외관에서 유형을 추론합니다. 그 추론이 사람들을 물어뜯는 것입니다. YAML 1.1 명세(여전히 많은 도구에서 사용됨)는 yes, no, on, off, true, false를 모두 불리언으로 처리합니다. 따옴표 없는 1.0같은 버전 문자열을 부동소수점으로 처리합니다. 그리고 노르웨이 문제가 있습니다.

YAML의 유명한 함정

노르웨이 문제는 DevOps 동아리에서 일종의 밈이 되었습니다. YAML 1.1에서 국가 코드 NO는 불리언 false로 파싱됩니다. 따라서 ISO 국가 코드를 설정에 매핑하면 노르웨이 항목이 조용히 불리언으로 변환됩니다. YAML 1.2 명세가 이것을 고쳤지만, 최근까지 PyYAML의 기본 모드를 포함한 많이 사용되는 파서들이 여전히 YAML 1.1을 대상으로 합니다. 신뢰하기 전에 도구가 실제로 어떤 명세 버전을 구현하는지 확인하세요.

실제의 노르웨이 문제: YAML 1.1에서 따옴표 없는 NO, Yes, on, off는 모두 불리언이 됩니다. 수정은 간단합니다 — 문자열에 따옴표를 붙이면 됩니다 — 하지만 문제는 조용하다는 것입니다. 설정이 오류 없이 로드되고 문자열을 기대하는 곳에 불리언을 얻게 됩니다.
yaml
# YAML 1.1 implicit type coercion — all of these silently become booleans:
countries:
  norway: NO        # → false  ← The Norway Problem
  sweden: SE        # → "SE"   (fine, not in the boolean list)
  enabled: yes      # → true
  disabled: no      # → false
  feature_flag: on  # → true
  another: off      # → false

# Version strings can become numbers:
python_version: 3.10   # → float 3.1 (trailing zero dropped)
api_version: 1.0       # → float 1.0

# Safe: quote anything that could be ambiguous
python_version: "3.10"
country: "NO"
enabled: "yes"
  • 8진수 리터럴. YAML 1.1에서 01010이 아닌 8(8진수)로 파싱됩니다. 0755 같은 파일 권한 값에 중요합니다.
  • 탭 vs 공백. YAML은 들여쓰기에 탭 문자를 금지합니다. 탭을 사용하도록 설정된 에디터에서 코드를 붙여넣으면 불분명한 파싱 오류가 발생합니다 — 또는 더 나쁘게는 조용한 잘못된 정렬.
  • 암묵적 null. 값이 없는 키는 null이 됩니다. 손으로 편집할 때 실수로 만들기 쉽습니다.
  • 들여쓰기 민감성. 공백 하나 더 있으면 값이 조용히 한 부모에서 다른 부모로 이동합니다. 오류 없이 잘못된 데이터만 남습니다.

TOML: 명시적 유형이 실제로 어떻게 보이는가

TOML의 유형 시스템은 TOML v1.0 명세에 나와 있습니다. 문자열은 따옴표로 묶어야 합니다(작은따옴표 또는 큰따옴표). 불리언은 true 또는 false이며 그 외는 없습니다. 정수는 정수입니다. 부동소수점은 부동소수점입니다. 그리고 날짜/시간 — JSON도 YAML도 네이티브 유형으로 가지지 않는 것 — 이 TOML에서 1등급 값입니다.

toml
# TOML types are always unambiguous
name = "my-app"          # string — must be quoted
version = "1.0.0"        # string — quotes make it clear this is not a float
port = 8080              # integer
debug = false            # boolean — only true/false, nothing else
threshold = 0.95         # float

# Datetime is a first-class type — no string parsing needed
created_at = 2024-01-15T09:30:00Z
build_date = 2024-03-20

# Arrays
allowed_hosts = ["localhost", "staging.example.com", "api.example.com"]

# Inline tables
[database]
host = "postgres.internal"
port = 5432
name = "payments_prod"
pool_size = 20

국가 코드 NO는 TOML에서 그냥 문자열입니다. 따옴표가 없지만 TOML은 문자열 같은 값에서 유형을 추론하지 않습니다 — 따옴표 없는 값은 엄격한 구문 규칙을 따릅니다. 문자열이 되려면 따옴표가 필요합니다. TOML에는 단순히 YAML의 함정을 유발하는 암묵적 강제 변환 메커니즘이 없습니다.

나란히 비교: CI 파이프라인 설정

여기 GitHub Actions 워크플로우가 있습니다 — YAML의 홈 그라운드입니다. GitHub Actions가 YAML을 기대하고, 들여쓰기 기반 중첩이 논리적 구조를 반영하며, 주석이 명확하지 않은 단계 설정을 설명하는 데 필수적이기 때문에 형식이 자연스럽게 맞습니다.

yaml
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build --if-present

      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

이제 TOML로 된 동등한 프로젝트 매니페스트입니다 — TOML이 빛나는 곳입니다. 실제 Cargo.toml 구조와 YAML에서 같은 설정이 어떻게 보일지를 비교해보세요:

toml
# Cargo.toml — Rust package manifest
[package]
name = "payments-service"
version = "2.4.1"
edition = "2021"
description = "Payment processing microservice"
license = "MIT"
authors = ["Alice Chen <[email protected]>"]

[dependencies]
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
tracing = "0.1"
anyhow = "1.0"

[dev-dependencies]
tokio-test = "0.4"
wiremock = "0.6"

[profile.release]
opt-level = 3
lto = true
codegen-units = 1

TOML의 약점: 테이블 배열

TOML도 거친 면이 없지 않습니다. 테이블 배열 구문 — [[이중 괄호]] — 은 명세에서 가장 헷갈리는 것 중 하나입니다. JSON이 객체 배열로 작성할 것을 표현하는 방법으로, 처음에는 이상하게 읽힙니다.

toml
# TOML array of tables — [[double brackets]] creates array entries
[[server]]
host = "web-01.example.com"
port = 443
region = "us-east-1"

[[server]]
host = "web-02.example.com"
port = 443
region = "us-west-2"

[[server]]
host = "web-03.example.com"
port = 443
region = "eu-west-1"

# The above is equivalent to this JSON:
# { "server": [
#   { "host": "web-01.example.com", "port": 443, "region": "us-east-1" },
#   { "host": "web-02.example.com", "port": 443, "region": "us-west-2" },
#   { "host": "web-03.example.com", "port": 443, "region": "eu-west-1" }
# ]}

YAML에서 같은 구조는 더 자연스럽게 읽힙니다 — 들여쓰기된 속성이 있는 목록. 반복되는 배열 항목이 있는 깊이 중첩된 데이터의 경우, YAML의 들여쓰기 기반 중첩이 TOML의 [[섹션]] 헤더보다 진정으로 덜 장황합니다. TOML에는 YAML의 앵커-앤-별칭 시스템과 동등한 것도 없어서, 공유 블록을 한 번 정의하고 다른 곳에서 참조할 수 없습니다. 여러 TOML 테이블에서 같은 값 세트를 중복하게 되면 수동으로 복사해야 합니다.

실제로 각 형식이 이기는 곳

YAML이 이 생태계에서 기본으로 이깁니다:

  • GitHub Actions. 전체 워크플로우 구문이 YAML입니다. 선택의 여지가 없으며, 괜찮습니다 — 들여쓰기가 작업, 단계, 조건의 중첩 구조에 잘 매핑됩니다.
  • Kubernetes. 모든 매니페스트 — Deployments, Services, ConfigMaps, Ingress 규칙 — 이 YAML입니다. Kubernetes 객체 모델은 깊이 중첩되어 있으며 YAML이 우아하게 처리합니다.
  • Docker Compose. 서비스 정의, 네트워크, 볼륨 — 모두 YAML. 포트가 노출되는 이유나 특정 헬스체크 간격이 사용되는 이유를 설명하는 주석은 문서의 일부입니다.
  • Ansible. 플레이북, 롤, 변수 파일 — 모두 YAML. 주석 지원은 명확하지 않은 작업 파라미터를 설명하는 데 적극적으로 사용됩니다.

형식을 제어할 때 TOML이 이깁니다:

  • Rust 프로젝트. Cargo.toml이 황금 표준입니다. 의존성 선언, 기능 플래그, 빌드 프로필 — 모두 TOML. 명시적 유형은 "1.0.0" 같은 버전 문자열이 문자열로 유지됨을 의미합니다.
  • Python 프로젝트. pyproject.toml은 Python 프로젝트 메타데이터, 빌드 설정, 도구 설정(Black, isort, mypy, pytest 모두 읽음)의 표준이 되었습니다.
  • 모호함이 버그를 유발하는 도구 설정. 설정에 버전 문자열, 국가 코드, 또는 불리언이나 숫자처럼 보일 수 있는 다른 값이 포함된 경우, TOML의 명시적 유형이 파싱 놀라움의 전체 범주를 제거합니다.
  • 평탄한 설정. 설정이 주로 하나 또는 두 레벨의 중첩에서 키-값 쌍인 경우, TOML이 YAML보다 더 읽기 쉽고 JSON보다 더 깔끔합니다.

실용적인 결정 가이드

대부분의 경우 생태계에 의해 선택이 이루어집니다. GitHub Actions는 YAML. Kubernetes는 YAML. Rust는 TOML. Python 도구는 TOML. 실제로 자유 선택이 있을 때는 이것을 가이드로 사용하세요:

  • YAML을 사용하세요 도구가 그것을 요구할 때 — CI/CD 플랫폼, Kubernetes, Helm 차트, Docker Compose, Ansible. 관례에 맞서는 비용이 절약보다 더 큽니다.
  • YAML을 사용하세요 앵커와 별칭으로 복잡한 설정을 DRY하게 유지해야 할 때 — TOML에는 동등한 것이 없습니다.
  • TOML을 사용하세요 당신이 제어하는 프로젝트 매니페스트와 도구 설정 — 패키지 메타데이터, 린터 설정, 빌드 설정.
  • TOML을 사용하세요 설정에 YAML 1.1이 잘못 해석할 수 있는 값이 포함된 경우 — 버전 문자열, 국가 코드, 불리언이나 숫자를 닮은 모든 것.
  • YAML 문자열에 따옴표를 붙이세요 값이 불리언, 숫자, 또는 null과 혼동될 수 있을 때. 특히: 버전 번호, 국가 코드, 숫자로 시작하는 모든 것, yes, no, on, off 같은 값.

두 형식으로 작업하기

작업 중인 설정 파일을 검증하거나 pretty-print해야 하나요? TOML 포매터가 TOML 파일을 처리하고, YAML 포매터가 YAML을 처리합니다. 한 형식에서 다른 형식으로 설정을 마이그레이션해야 한다면 — 예를 들어, YAML 기반 도구 설정을 그것을 선호하는 프로젝트에서 TOML로 변환 — TOML을 JSON으로YAML을 JSON으로 변환기가 모두 JSON을 출력하며, 그런 다음 대상 형식으로 변환할 수 있습니다. 때로는 JSON을 중간 단계로 사용하는 것이 가장 신뢰할 수 있는 경로입니다.

알아둘 가치가 있는 한 가지: 두 형식 모두 JSON 호환 하위 집합이 있습니다. 유효한 JSON은 유효한 YAML입니다(YAML은 JSON의 수퍼셋). TOML은 JSON과 그런 관계가 없지만, TOML 명세는 의도적으로 간단하게 유지됩니다 — 짧게 읽힙니다. 이것은 전체 YAML 1.2 명세에 대해서는 같은 말을 할 수 없습니다.

마무리

TOML과 YAML은 실제로 경쟁하지 않습니다. 설계 우선순위에 따라 다른 틈새에 자리를 잡았습니다. YAML의 암묵적 유형과 들여쓰기 기반 구조는 팀이 유지 관리하는 크고 중첩된 설정에 자연스럽게 맞습니다 — Kubernetes 매니페스트와 GitHub Actions 워크플로우를 생각해 보세요. TOML의 명시적 유형과 평탄한 섹션 구조는 잘못 읽힌 버전 문자열이나 국가 코드가 실제 버그를 일으킬 수 있는 프로젝트 매니페스트와 도구 설정에 자연스럽게 맞습니다.

가지고 다닐 한 가지: YAML을 작성하고 불리언, 숫자, 또는 null처럼 보일 수 있는 값이 있다면 따옴표를 붙이세요. 그것이 대부분의 YAML 관련 설정 버그를 방지하는 단일 습관입니다. 그리고 새 프로젝트를 시작하면서 설정 형식을 자유롭게 선택할 수 있다면 TOML을 살펴볼 가치가 있습니다. TOML GitHub 저장소에는 좋은 예시가 있으며, Cargo.toml이나 pyproject.toml을 작성해보면 형식의 매력이 상당히 명확해집니다.