JSON·YAML·TOML·XML·INI 중 무엇을 쓸까: 설정 파일 형식 비교와 변환·검증

이 글의 핵심

설정 파일 형식은 한 번 정하면 도구와 팀 습관이 함께 굳어서 바꾸기 어렵습니다. 각 파서가 문서를 해석하는 방식의 차이에서 오는 버그, Docker Compose나 Cargo처럼 형식별로 자리 잡은 사용 사례, 사람이 편집할 파일과 기계가 주고받을 데이터에 맞는 선택 기준을 정리해 프로젝트에 맞는 형식을 고를 수 있게 합니다.

들어가며: 설정 파일 형식의 중요성

모든 프로그래밍 프로젝트는 설정 파일을 사용합니다. package.json, docker-compose.yml, nginx.conf, README.md 등 각 형식마다 특징과 용도가 다릅니다.

형식을 고를 때 가장 중요한 질문은 “누가 이 파일을 쓰고 누가 읽는가”입니다. 프로그램끼리 주고받는 데이터라면 해석이 모호하지 않은 JSON이 안전하고, 사람이 자주 손으로 고치는 설정이라면 주석을 달 수 있는 YAML이나 TOML이 편합니다. 그리고 실무에서 부딪히는 버그의 대부분은 문법 자체보다 파서가 값을 어떤 타입으로 해석하는가에서 나옵니다. 같은 파일이라도 파서와 버전에 따라 yes가 불린이 되기도 하고 문자열이 되기도 하며, 큰 정수가 조용히 반올림되기도 합니다. 이 글은 각 형식의 문법과 함께 이런 해석 차이를 중심으로 정리합니다.

이 글에서 다룰 형식:

  • JSON (JavaScript Object Notation)
  • YAML (YAML Ain’t Markup Language)
  • XML (eXtensible Markup Language)
  • TOML (Tom’s Obvious, Minimal Language)
  • INI (Initialization File)
  • Markdown
  • 기타 (Properties, HCL, Jsonnet)

JSON (JavaScript Object Notation)

JSON 파싱 내부 메커니즘

JSON 파서가 동작하는 원리:

JSON.parse(text) 내부 동작:

1. 토큰화 (Tokenization):
   
   입력 텍스트를 토큰으로 분리:
   
   {"name":"John","age":30}
   
   → 토큰 스트림:
   [ {, "name", :, "John", ,, "age", :, 30, } ]
   
   토큰 타입:
   - LEFT_BRACE: {
   - RIGHT_BRACE: }
   - LEFT_BRACKET: [
   - RIGHT_BRACKET: ]
   - STRING: "..."
   - NUMBER: 123, 3.14
   - TRUE/FALSE/NULL
   - COLON: :
   - COMMA: ,

2. 재귀 하강 파싱 (Recursive Descent Parsing):
   
   문법 규칙:
   value → object | array | string | number | true | false | null
   object → { members }
   members → pair | pair , members
   pair → string : value
   array → [ elements ]
   elements → value | value , elements
   
   파싱 과정:
   
   parseValue():
     if token == '{':
       return parseObject()
     else if token == '[':
       return parseArray()
     else if token == STRING:
       return parseString()
     else if token == NUMBER:
       return parseNumber()
   
   parseObject():
     result = {}
     expect('{')
     while token != '}':
       key = parseString()
       expect(':')
       value = parseValue()  ← 재귀!
       result[key] = value
       if token == ',': advance()
     expect('}')
     return result

3. 값 변환:
   
   "John" → JavaScript String
   30 → JavaScript Number
   true → JavaScript Boolean
   null → JavaScript null
   
   특수 처리:
   - "123" → String (따옴표 있음)
   - 123 → Number (따옴표 없음)
   - "\n" → 개행 문자 (이스케이프)
   - "\u0041" → "A" (유니코드)

4. 에러 검증:
   
   구문 오류 감지:
   - 따옴표 미매칭: {"name": "John}
   - 후행 쉼표: {"name": "John",}
   - 주석: {"name": "John"} // comment
   - 키 미인용: {name: "John"}
   
   → SyntaxError 즉시 발생

JSON 파싱 성능이 빠른 이유:

JSON 문법의 단순성:

1. LL(1) 파서로 구현 가능:
   - 한 토큰만 미리 보면 (lookahead) 결정 가능
   - 백트래킹 불필요
   - 선형 시간 O(n)

2. 타입이 명확:
   - "..."는 무조건 문자열
   - 123은 무조건 숫자
   - true/false/null은 키워드
   
   YAML은 타입 추론 필요:
   - yes → Boolean true
   - "yes" → String "yes"
   - 123 → Number
   - "123" → String
   → 복잡한 파싱 로직

3. 메모리 할당 최소화:
   - JavaScript 엔진의 네이티브 객체로 직접 변환
   - V8 엔진에서 JSON.parse는 최적화된 C++ 코드

   일반적으로 같은 데이터라면 JSON 파싱이 YAML·XML 파싱보다 빠르지만,
   차이는 파서 구현과 데이터 모양에 따라 크게 달라지므로 필요하면 직접 측정할 것

JSON의 엄격함은 불편하지만 장점이기도 합니다. 문법이 단순해 언어마다 파서가 거의 같은 결과를 내므로, 서로 다른 시스템이 주고받는 데이터 형식으로 가장 안전합니다. 그래도 JSON에는 알아 둘 함정이 두 가지 있습니다. 첫째, 숫자 정밀도입니다. JSON 명세 자체는 숫자 크기를 제한하지 않지만, JavaScript의 JSON.parse는 모든 숫자를 64비트 부동소수점으로 읽으므로 2^53(약 9천조)을 넘는 정수는 조용히 반올림됩니다. 트위터 API가 ID를 id와 문자열 id_str로 함께 내려준 것도 이 때문입니다. 64비트 정수 ID, 주문 금액처럼 정확해야 하는 값은 문자열로 주고받는 것이 안전합니다. 둘째, 오류 메시지가 위치만 알려 준다는 점입니다. 후행 쉼표가 있는 파일을 Node.js로 읽으면 SyntaxError: Unexpected token } in JSON at position 42(최신 V8에서는 Expected double-quoted property name in JSON at position 42)처럼 몇 번째 문자인지만 나오므로, 긴 설정 파일이라면 jq . config.json이나 에디터의 JSON 검사기로 줄 번호를 확인하는 편이 빠릅니다.

JSON5와 JSONC (확장):

// JSON5 (주석 허용, 후행 쉼표 허용)
{
  // 주석 가능
  name: 'John',  // 키 인용 생략 가능
  age: 30,       // 후행 쉼표 OK
}

// JSONC (JSON with Comments - VS Code 설정)
{
  "editor.fontSize": 14,  // 폰트 크기
  "editor.tabSize": 2     // 탭 크기
}

// 표준 JSON은 불가능:
{
  "name": "John",  // ❌ 주석 불가
  "age": 30,       // ❌ 후행 쉼표 불가
}

JSON이란?

JSON은 JavaScript 객체 표기법을 기반으로 한 경량 데이터 교환 형식입니다. 사람이 읽기 쉽으며, 기계가 파싱하기 쉽습니다.

JSON 기본 문법

{
  "name": "John Doe",
  "age": 30,
  "email": "[email protected]",
  "isActive": true,
  "balance": 1234.56,
  "tags": ["developer", "designer"],
  "address": {
    "street": "123 Main St",
    "city": "Seoul",
    "zipCode": "12345"
  },
  "projects": [
    {
      "id": 1,
      "name": "Project A",
      "status": "active"
    },
    {
      "id": 2,
      "name": "Project B",
      "status": "completed"
    }
  ],
  "metadata": null
}

JSON 데이터 타입

{
  "string": "Hello, World!",
  "number": 42,
  "float": 3.14159,
  "boolean": true,
  "null": null,
  "array": [1, 2, 3, "mixed", true],
  "object": {
    "nested": "value"
  }
}

JSON 장단점

flowchart TB
    JSON[JSON]
    
    subgraph Pros[장점]
        P1[✅ 파싱 속도 빠름]
        P2[✅ JavaScript 네이티브]
        P3[✅ 간결한 문법]
        P4[✅ 널리 지원됨]
    end
    
    subgraph Cons[단점]
        C1[❌ 주석 불가]
        C2[❌ 후행 쉼표 불가]
        C3[❌ 멀티라인 문자열 불편]
        C4[❌ 날짜 타입 없음]
    end
    
    JSON --> Pros
    JSON --> Cons

JSON 사용 예시

// JavaScript
const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3
};
// JSON으로 변환
const json = JSON.stringify(config, null, 2);
console.log(json);
// JSON 파싱
const parsed = JSON.parse(json);
console.log(parsed.apiUrl);
# Python
import json
config = {
    "apiUrl": "https://api.example.com",
    "timeout": 5000,
    "retries": 3
}
# JSON으로 변환
json_str = json.dumps(config, indent=2)
print(json_str)
# JSON 파싱
parsed = json.loads(json_str)
print(parsed['apiUrl'])
# 파일 I/O
with open('config.json', 'w') as f:
    json.dump(config, f, indent=2)
with open('config.json', 'r') as f:
    loaded = json.load(f)

JSON Schema (검증)

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "age": {
      "type": "integer",
      "minimum": 0,
      "maximum": 150
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1
    }
  },
  "required": ["name", "email"]
}

YAML (YAML Ain’t Markup Language)

YAML 파싱 메커니즘과 복잡성

YAML 파서가 복잡한 이유:

YAML은 문맥 의존적 (Context-Sensitive) 문법:

1. 들여쓰기 레벨 추적:
   
   name: value
     nested: value2
       deep: value3
   
   → 파서가 현재 들여쓰기 레벨을 스택으로 관리
   → 들여쓰기 감소 시 스택 pop
   → 들여쓰기 증가 시 스택 push
   
   들여쓰기 오류:
   items:
     - name: item1
      - name: item2  ← 1칸 부족 → 에러!

2. 타입 추론 (Implicit Typing):
   
   YAML은 따옴표 없이 값의 타입 자동 추론:
   
   age: 30           → Number
   age: "30"         → String
   enabled: true     → Boolean true
   enabled: yes      → Boolean true (YAML 키워드)
   enabled: "yes"    → String "yes"
   version: 1.0      → Number 1.0
   version: "1.0"    → String "1.0"
   date: 2026-04-01  → Date 객체
   null_value: null  → null
   null_value: ~     → null (YAML 축약)
   
   → 파서가 모든 값에 대해 타입 판단 필요
   → JSON은 명시적이라 불필요

3. 복잡한 구조 표현:
   
   블록 스타일 (들여쓰기):
   items:
     - name: item1
       value: 100
     - name: item2
       value: 200
   
   플로우 스타일 (인라인):
   items: [{name: item1, value: 100}, {name: item2, value: 200}]
   
   앵커와 별칭 (재사용):
   defaults: &defaults
     timeout: 30
     retries: 3
   
   prod:
     <<: *defaults      ← defaults 내용 병합
     url: https://prod
   
   → 파서가 앵커를 추적하고 참조 시 삽입

4. 멀티라인 문자열:
   
   리터럴 (개행 유지):
   script: |
     line 1
     line 2
     line 3
   → "line 1\nline 2\nline 3"
   
   폴딩 (개행 → 공백):
   description: >
     This is a long
     description text.
   → "This is a long description text."
   
   → 특수 처리 로직 필요

5. 파싱 단계:
   
   text → Tokens → Events → Representation Graph → Native Objects
   
   Events:
   - STREAM_START
   - DOCUMENT_START
   - MAPPING_START (객체 시작)
   - SCALAR (값)
   - MAPPING_END
   - DOCUMENT_END
   - STREAM_END
   
   → JSON은 Tokens → Objects (단순)

YAML 파싱이 느린 이유:

JSON vs YAML 파싱 복잡도:

JSON:
- LL(1) 파서: 1개 토큰만 미리 봄
- 타입 명시적: "string", 123, true
- 단순 재귀 하강
- 시간 복잡도: O(n)

YAML:
- 문맥 의존: 들여쓰기 레벨 추적
- 타입 추론: yes → Boolean, "yes" → String
- 앵커/별칭 해석
- 멀티라인 처리
- 시간 복잡도: O(n) 이상 (복잡한 상수)

벤치마크 (동일한 데이터):
- JSON.parse (V8): 1x (기준)
- yaml.load (js-yaml): 10-20x
- yaml.safe_load (PyYAML): 50-100x

YAML 보안 이슈:

# ❌ 위험: yaml.load() (Python, 임의 코드 실행)
!!python/object/apply:os.system
args: ['rm -rf /']

# ✅ 안전: yaml.safe_load() (기본 타입만)
name: value
age: 30

# Node.js js-yaml:
# v3: yaml.load는 JS 함수 태그 등을 허용했으므로 safeLoad를 써야 했음
# v4: load가 기본적으로 안전한 스키마를 쓰고, safeLoad는 제거됨

Python에서 yaml.load(f)를 Loader 인자 없이 호출하면 PyYAML 5.1부터 경고가 나고, 6.0부터는 TypeError: load() missing 1 required positional argument: 'Loader'로 실패합니다. 신뢰할 수 없는 입력은 물론이고 자기 설정 파일이라도 yaml.safe_load()를 기본으로 쓰는 것이 원칙입니다.

YAML이란?

YAML은 사람이 읽기 쉬운 데이터 직렬화 형식입니다. 들여쓰기로 계층 구조를 표현하며, 주석을 지원합니다.

YAML 기본 문법

# 주석은 #으로 시작
# 키-값 쌍
name: John Doe
age: 30
email: [email protected]
# 불린
isActive: true
isDeleted: false
# Null
metadata: null
# 또는
metadata: ~
# 문자열 (따옴표 선택적)
city: Seoul
country: "South Korea"
description: 'Single quotes'
# 멀티라인 문자열
bio: |
  This is a multi-line string.
  It preserves line breaks.
  Very useful for long text.
summary: >
  This is a folded string.
  Line breaks are converted to spaces.
  Useful for long paragraphs.
# 배열 (리스트)
tags:
  - developer
  - designer
  - writer
# 또는 인라인
tags: ["developer", "designer", "writer"]
# 객체 (딕셔너리)
address:
  street: 123 Main St
  city: Seoul
  zipCode: "12345"
# 또는 인라인
address: {street: 123 Main St, city: Seoul, zipCode: "12345"}
# 배열 + 객체
projects:
  - id: 1
    name: Project A
    status: active
  - id: 2
    name: Project B
    status: completed
# 앵커와 별칭 (재사용)
defaults: &defaults
  timeout: 30
  retries: 3
production:
  <<: *defaults
  apiUrl: https://api.example.com
development:
  <<: *defaults
  apiUrl: http://localhost:3000

YAML 주의사항

# ❌ 탭 사용 불가 (공백만 가능)
parent:
	nested: error  # 탭으로 들여쓰기하면 에러!
# ✅ 공백 사용
parent:
  nested: correct
# ❌ 들여쓰기 불일치
items:
  - name: item1
    value: 100
   - name: item2  # 들여쓰기 오류!
# ✅ 일관된 들여쓰기
items:
  - name: item1
    value: 100
  - name: item2
    value: 200
# 특수 문자 주의
# 콜론(:)이 포함된 문자열은 따옴표 필요
url: "https://example.com"  # 따옴표 권장
time: "12:30"  # 따옴표 필수
# 숫자로 시작하는 문자열
version: "1.0"  # 따옴표 필요 (아니면 숫자로 파싱)

위 규칙들이 왜 필요한지는 YAML 1.1과 1.2의 차이를 알면 이해가 쉽습니다. 2005년의 YAML 1.1은 yes/no/on/off를 불린으로, 12:30 같은 콜론 숫자를 60진수 정수(750)로, 0755를 8진수로 해석했습니다. 2009년의 YAML 1.2는 이런 규칙을 정리해 true/false만 불린으로 보지만, Python의 PyYAML처럼 널리 쓰이는 파서가 여전히 1.1 규칙을 따릅니다. 그래서 국가 코드 목록에 NO(노르웨이)를 따옴표 없이 쓰면 false가 되는 “노르웨이 문제”나, Docker Compose 포트 매핑 - 22:22가 옛 파서에서 숫자로 바뀌는 문제가 생깁니다. Compose 문서가 포트를 "22:22"처럼 따옴표로 감싸라고 권하는 이유입니다. 버전 문자열 1.10이 1.1로 바뀌는 것도 같은 부류의 버그입니다. 실무 원칙은 단순합니다. 문자열이어야 하는 값은 따옴표로 감싼다. 그리고 yamllint의 truthy 규칙을 켜 두면 따옴표 없는 yes/no를 커밋 전에 잡을 수 있습니다.

앵커(&)와 병합 키(<<)도 주의가 필요합니다. << 병합 키는 YAML 1.1의 부가 타입으로 정의되었고 1.2 명세에는 없어서, 파서에 따라 지원하지 않거나 경고를 냅니다. 병합은 얕은 병합이라 중첩된 딕셔너리를 덮어쓰면 기본값의 하위 키가 합쳐지지 않고 통째로 대체된다는 점도 자주 놓칩니다. 또 앵커를 재귀적으로 참조하도록 만든 작은 파일이 파싱 시 기하급수적으로 부풀어 메모리를 고갈시키는 “billion laughs” 공격도 있으므로, 외부에서 받은 YAML은 크기와 별칭 수를 제한하는 파서 옵션을 확인해야 합니다.

YAML 사용 예시

# Python
import yaml
config = {
    'name': 'MyApp',
    'version': '1.0.0',
    'database': {
        'host': 'localhost',
        'port': 5432,
        'name': 'mydb'
    },
    'features': ['auth', 'api', 'admin']
}
# YAML로 변환
yaml_str = yaml.dump(config, default_flow_style=False)
print(yaml_str)
# YAML 파싱
parsed = yaml.safe_load(yaml_str)
print(parsed['database']['host'])
# 파일 I/O
with open('config.yaml', 'w') as f:
    yaml.dump(config, f, default_flow_style=False)
with open('config.yaml', 'r') as f:
    loaded = yaml.safe_load(f)

Docker Compose 예시

version: '3.8'
services:
  web:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./html:/usr/share/nginx/html:ro
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    environment:
      - NGINX_HOST=example.com
      - NGINX_PORT=80
    depends_on:
      - app
    networks:
      - webnet
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/health"]
      interval: 30s
      timeout: 10s
      retries: 3
  
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - NODE_ENV=production
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    volumes:
      - ./app:/app
      - /app/node_modules
    networks:
      - webnet
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
  
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - webnet
networks:
  webnet:
    driver: bridge
volumes:
  db-data:

XML (eXtensible Markup Language)

XML이란?

XML은 확장 가능한 마크업 언어로, 태그 기반으로 데이터를 구조화합니다. 엄격한 문법과 스키마 검증을 지원합니다.

XML 기본 문법

<?xml version="1.0" encoding="UTF-8"?>
<!-- 주석 -->
<configuration>
  <!-- 단순 요소 -->
  <name>MyApp</name>
  <version>1.0.0</version>
  
  <!-- 속성 -->
  <database type="postgresql" ssl="true">
    <host>localhost</host>
    <port>5432</port>
    <name>mydb</name>
    <credentials>
      <username>user</username>
      <password>pass</password>
    </credentials>
  </database>
  
  <!-- 배열 (반복 요소) -->
  <features>
    <feature name="auth" enabled="true"/>
    <feature name="api" enabled="true"/>
    <feature name="admin" enabled="false"/>
  </features>
  
  <!-- CDATA (특수 문자 포함) -->
  <description><![CDATA[
    This is a <description> with special characters: & < > " '
  ]]></description>
  
  <!-- 네임스페이스 -->
  <config xmlns="http://example.com/config"
          xmlns:db="http://example.com/database">
    <db:connection>localhost</db:connection>
  </config>
</configuration>

XML 파싱

# Python (ElementTree)
import xml.etree.ElementTree as ET
# XML 파싱
tree = ET.parse('config.xml')
root = tree.getroot()
# 요소 접근
name = root.find('name').text
print(f"Name: {name}")
# 속성 접근
db = root.find('database')
db_type = db.get('type')
print(f"Database type: {db_type}")
# 반복 요소
for feature in root.findall('.//feature'):
    name = feature.get('name')
    enabled = feature.get('enabled')
    print(f"Feature {name}: {enabled}")
# XPath 사용
host = root.find('.//database/host').text
print(f"Database host: {host}")
// JavaScript (Browser)
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlString, "text/xml");
// 요소 접근
const name = xmlDoc.getElementsByTagName("name")[0].textContent;
console.log(name);
// 속성 접근
const db = xmlDoc.getElementsByTagName("database")[0];
const dbType = db.getAttribute("type");
console.log(dbType);

XML Schema (XSD)

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  
  <xs:element name="configuration">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="name" type="xs:string"/>
        <xs:element name="version" type="xs:string"/>
        <xs:element name="database" type="DatabaseType"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  
  <xs:complexType name="DatabaseType">
    <xs:sequence>
      <xs:element name="host" type="xs:string"/>
      <xs:element name="port" type="xs:integer"/>
    </xs:sequence>
    <xs:attribute name="type" type="xs:string" use="required"/>
  </xs:complexType>
  
</xs:schema>

XML 사용 사례

<!-- Maven (pom.xml) -->
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>myapp</artifactId>
  <version>1.0.0</version>
  
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <version>3.2.0</version>
    </dependency>
  </dependencies>
</project>
<!-- Spring (applicationContext.xml) -->
<beans xmlns="http://www.springframework.org/schema/beans">
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="org.postgresql.Driver"/>
    <property name="url" value="jdbc:postgresql://localhost:5432/mydb"/>
  </bean>
</beans>
<!-- Android (AndroidManifest.xml) -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
  <uses-permission android:name="android.permission.INTERNET"/>
  <application android:label="MyApp">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
      </intent-filter>
    </activity>
  </application>
</manifest>

TOML (Tom’s Obvious, Minimal Language)

TOML이란?

TOML은 읽기 쉽고 명확한 설정 파일 형식입니다. INI의 개선 버전으로, 타입과 중첩을 지원합니다.

TOML 기본 문법

# 주석
# 키-값 쌍
name = "MyApp"
version = "1.0.0"
# 숫자
port = 8080
timeout = 30.5
# 불린
debug = true
production = false
# 날짜/시간
created_at = 2026-04-01T10:00:00Z
# 배열
tags = ["rust", "web", "api"]
# 인라인 테이블
author = { name = "John Doe", email = "[email protected]" }
# 테이블 (섹션)
[database]
host = "localhost"
port = 5432
name = "mydb"
[database.credentials]
username = "user"
password = "pass"
# 배열 테이블
[[servers]]
name = "server1"
ip = "192.168.1.1"
role = "primary"
[[servers]]
name = "server2"
ip = "192.168.1.2"
role = "backup"
# 중첩 구조
[app.cache]
enabled = true
ttl = 3600
[app.cache.redis]
host = "localhost"
port = 6379

TOML 파싱

# Python
import tomli  # Python 3.11+는 tomllib 내장
with open('config.toml', 'rb') as f:
    config = tomli.load(f)
print(config['name'])
print(config['database']['host'])
print(config['servers'][0]['name'])
# TOML 생성
import tomli_w
data = {
    'name': 'MyApp',
    'version': '1.0.0',
    'database': {
        'host': 'localhost',
        'port': 5432
    }
}
with open('output.toml', 'wb') as f:
    tomli_w.dump(data, f)
// Rust
use serde::Deserialize;
use std::fs;
#[derive(Deserialize)]
struct Config {
    name: String,
    version: String,
    database: Database,
}
#[derive(Deserialize)]
struct Database {
    host: String,
    port: u16,
}
fn main() {
    let contents = fs::read_to_string("config.toml").unwrap();
    let config: Config = toml::from_str(&contents).unwrap();
    
    println!("Name: {}", config.name);
    println!("DB Host: {}", config.database.host);
}

TOML 사용 사례

# Cargo.toml (Rust)
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dev-dependencies]
criterion = "0.5"
[profile.release]
opt-level = 3
lto = true
# pyproject.toml (Python)
[project]
name = "myapp"
version = "1.0.0"
description = "My Python application"
authors = [{name = "John Doe", email = "[email protected]"}]
dependencies = [
    "fastapi>=0.104.0",
    "uvicorn>=0.24.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.4.0", "black>=23.0.0"]
[tool.black]
line-length = 88
target-version = ['py311']

TOML이 설정 파일에서 인기를 얻은 이유는 YAML의 타입 추론 문제를 피하면서도 주석을 쓸 수 있기 때문입니다. 문자열은 반드시 따옴표로 감싸야 하고, 날짜·시간은 RFC 3339 형식일 때만 날짜 타입이 되므로 “이 값이 무슨 타입인가”가 문법에서 드러납니다. 대신 TOML에는 초보자가 자주 부딪히는 함정이 있습니다. [database] 같은 테이블 헤더 아래에 쓴 키는 다음 헤더가 나올 때까지 모두 그 테이블에 속합니다. 파일 맨 아래에 최상위 설정 debug = true를 추가하면, 마지막 테이블([app.cache.redis]) 안의 키가 되어 버립니다. 최상위 키는 반드시 첫 테이블 헤더보다 위에 둬야 합니다. 또 같은 테이블을 두 번 정의하거나 같은 키를 두 번 쓰면 파서가 Cannot declare ('database',) twice 같은 오류로 거부하는데, YAML이 중복 키를 조용히 마지막 값으로 덮어쓰는 것과 대조적입니다. 깊게 중첩된 구조(쿠버네티스 매니페스트처럼 여러 단계의 리스트 안의 딕셔너리)는 [[a.b.c]] 헤더가 길어져 읽기 어려워지므로, TOML은 비교적 평평한 설정에 가장 잘 맞습니다.


INI (Initialization File)

INI란?

INI는 간단한 설정 파일 형식으로, Windows에서 널리 사용됩니다. 섹션과 키-값 쌍으로 구성됩니다.

INI 기본 문법

; 주석은 세미콜론으로 시작
# 또는 해시(#)로도 가능
; 전역 설정 (섹션 없음)
app_name = MyApp
version = 1.0.0
; 섹션
[database]
host = localhost
port = 5432
name = mydb
username = user
password = pass
[cache]
enabled = true
ttl = 3600
type = redis
[cache.redis]
host = localhost
port = 6379
; 배열 (비표준, 구현마다 다름)
[features]
feature1 = auth
feature2 = api
feature3 = admin
; 또는
features = auth,api,admin

INI 파싱

# Python
import configparser
config = configparser.ConfigParser()
config.read('config.ini')
# 값 읽기
app_name = config['DEFAULT']['app_name']
db_host = config['database']['host']
db_port = config.getint('database', 'port')
cache_enabled = config.getboolean('cache', 'enabled')
print(f"App: {app_name}")
print(f"DB: {db_host}:{db_port}")
print(f"Cache: {cache_enabled}")
# 값 쓰기
config['database']['host'] = 'db.example.com'
with open('config.ini', 'w') as f:
    config.write(f)

INI 사용 사례

; php.ini
[PHP]
engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
serialize_precision = -1
disable_functions = exec,passthru,shell_exec,system
[Date]
date.timezone = Asia/Seoul
[Session]
session.save_handler = files
session.save_path = "/var/lib/php/sessions"
session.gc_maxlifetime = 1440
; .gitconfig
[user]
    name = John Doe
    email = [email protected]
[core]
    editor = vim
    autocrlf = input
[alias]
    st = status
    co = checkout
    br = branch
    ci = commit

Markdown

Markdown이란?

Markdown은 일반 텍스트로 서식 있는 문서를 작성하는 경량 마크업 언어입니다.

Markdown 기본 문법

# 제목 1 (H1)
## 제목 2 (H2)
### 제목 3 (H3)
**굵게** 또는 __굵게__
*기울임* 또는 _기울임_
~~취소선~~
`인라인 코드`
> 인용문
> 여러 줄 가능
- 순서 없는 리스트
- 항목 2
  - 중첩 항목
  - 중첩 항목 2
1. 순서 있는 리스트
2. 항목 2
3. 항목 3
[링크 텍스트](https://example.com)
![이미지 대체 텍스트](image.png)
---
수평선
| 컬럼 1 | 컬럼 2 | 컬럼 3 |
|--------|--------|--------|
| 값 1   | 값 2   | 값 3   |
| 값 4   | 값 5   | 값 6   |
```
코드 블록
여러 줄 코드
```
```python
# 언어 지정 (신택스 하이라이팅)
def hello():
    print("Hello, World!")
```
- [ ] 체크박스 (미완료)
- [x] 체크박스 (완료)

GitHub Flavored Markdown (GFM)

# GitHub 확장 기능
## 작업 목록

- [x] 완료된 작업
- [ ] 미완료 작업
- [ ] 진행 중
## 테이블 정렬

| Left | Center | Right |
|:-----|:------:|------:|
| 왼쪽 | 중앙   | 오른쪽 |
## 이모지

:smile: :rocket: :tada:
## 멘션

@username
## 이슈 참조
#123
## 코드 블록 파일명

```javascript:app.js
console.log("Hello");
```
## 접기/펼치기

<details>
<summary>클릭하여 펼치기</summary>
숨겨진 내용
</details>
## 수식 (LaTeX)

$E = mc^2$
$$
\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$

Markdown 파싱

# Python (markdown 라이브러리)
import markdown
md_text = """
# Hello
This is **bold** and *italic*.
- Item 1
- Item 2
"""
html = markdown.markdown(md_text)
print(html)
# <h1>Hello</h1>
# <p>This is <strong>bold</strong> and <em>italic</em>.</p>
# <ul>
# <li>Item 1</li>
# <li>Item 2</li>
# </ul>
# 확장 기능 사용
html = markdown.markdown(md_text, extensions=['tables', 'fenced_code', 'toc'])
// JavaScript (marked 라이브러리)
import { marked } from 'marked';
const mdText = `
# Hello
This is **bold** text.
`;
const html = marked.parse(mdText);
console.log(html);

형식 비교 및 선택 가이드

동일한 데이터 표현

JSON

{
  "app": {
    "name": "MyApp",
    "version": "1.0.0",
    "features": ["auth", "api"]
  },
  "database": {
    "host": "localhost",
    "port": 5432
  }
}

YAML

# 실행 예제
app:
  name: MyApp
  version: 1.0.0
  features:
    - auth
    - api
database:
  host: localhost
  port: 5432

XML

<?xml version="1.0"?>
<config>
  <app>
    <name>MyApp</name>
    <version>1.0.0</version>
    <features>
      <feature>auth</feature>
      <feature>api</feature>
    </features>
  </app>
  <database>
    <host>localhost</host>
    <port>5432</port>
  </database>
</config>

TOML

# 실행 예제
[app]
name = "MyApp"
version = "1.0.0"
features = ["auth", "api"]
[database]
host = "localhost"
port = 5432

INI

[app]
name = MyApp
version = 1.0.0
features = auth,api
[database]
host = localhost
port = 5432

비교표

특성JSONYAMLXMLTOMLINIMarkdown
가독성⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
파싱 속도⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
주석❌✅✅✅✅✅
타입 안정성⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐N/A
중첩 구조✅✅✅✅제한적✅
파일 크기작음중간큼중간작음중간
스키마 검증✅✅✅❌❌❌

표의 별점은 대략적인 경향일 뿐입니다. “스키마 검증”도 TOML·INI에 표준 스키마 언어가 없다는 뜻이지 검증을 할 수 없다는 뜻은 아닙니다. 어떤 형식이든 파싱한 결과는 결국 딕셔너리이므로, 애플리케이션에서 JSON Schema나 Pydantic 같은 도구로 파싱 후 검증하는 것이 형식과 무관하게 가장 확실한 방법입니다. 에디터 지원도 비슷해서, VS Code의 YAML·TOML 확장은 JSON Schema를 연결해 자동 완성과 오류 표시를 제공합니다.

사용 사례별 추천

flowchart TD
    Start[파일 형식 선택] --> Q1{용도는?}
    
    Q1 -->|API 응답| JSON["✅ JSON\n빠른 파싱"]
    Q1 -->|설정 파일| Q2{복잡도는?}
    Q1 -->|문서| Markdown["✅ Markdown\nREADME, 블로그"]
    Q1 -->|빌드 설정| Q3{언어는?}
    
    Q2 -->|간단| INI["✅ INI\n간단한 설정"]
    Q2 -->|중간| TOML["✅ TOML\nRust, Python"]
    Q2 -->|복잡| YAML["✅ YAML\nDocker, K8s"]
    
    Q3 -->|Java| XML["✅ XML\nMaven, Spring"]
    Q3 -->|JavaScript| JSON["✅ JSON\npackage.json"]
    Q3 -->|Rust| TOML["✅ TOML\nCargo.toml"]
    Q3 -->|Python| TOML2["✅ TOML\npyproject.toml"]

프로젝트별 사용 현황

프로젝트형식파일명
Node.jsJSONpackage.json
PythonTOMLpyproject.toml
RustTOMLCargo.toml
GoGogo.mod
DockerYAMLdocker-compose.yml
KubernetesYAMLdeployment.yaml
AnsibleYAMLplaybook.yml
MavenXMLpom.xml
GradleGroovybuild.gradle
NginxCustomnginx.conf
ApacheCustomhttpd.conf

실전 변환 및 검증

형식 간 변환

#!/usr/bin/env python3
"""
설정 파일 형식 변환 도구
"""
import json
import yaml
import tomli
import tomli_w
import xml.etree.ElementTree as ET
from pathlib import Path
class ConfigConverter:
    @staticmethod
    def json_to_yaml(json_file, yaml_file):
        """JSON → YAML"""
        with open(json_file, 'r') as f:
            data = json.load(f)
        
        with open(yaml_file, 'w') as f:
            yaml.dump(data, f, default_flow_style=False, allow_unicode=True)
        
        print(f"✅ Converted: {json_file} → {yaml_file}")
    
    @staticmethod
    def yaml_to_json(yaml_file, json_file):
        """YAML → JSON"""
        with open(yaml_file, 'r') as f:
            data = yaml.safe_load(f)
        
        with open(json_file, 'w') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        
        print(f"✅ Converted: {yaml_file} → {json_file}")
    
    @staticmethod
    def json_to_toml(json_file, toml_file):
        """JSON → TOML"""
        with open(json_file, 'r') as f:
            data = json.load(f)
        
        with open(toml_file, 'wb') as f:
            tomli_w.dump(data, f)
        
        print(f"✅ Converted: {json_file} → {toml_file}")
    
    @staticmethod
    def toml_to_json(toml_file, json_file):
        """TOML → JSON"""
        with open(toml_file, 'rb') as f:
            data = tomli.load(f)
        
        with open(json_file, 'w') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        
        print(f"✅ Converted: {toml_file} → {json_file}")
# 사용
converter = ConfigConverter()
converter.json_to_yaml('config.json', 'config.yaml')
converter.yaml_to_json('config.yaml', 'config.json')
converter.json_to_toml('config.json', 'config.toml')

검증 도구

# JSON 검증
jq . config.json
# 또는
python -m json.tool config.json
# YAML 검증
yamllint config.yaml
# YAML → JSON (yq)
yq eval -o=json config.yaml
# XML 검증
xmllint --noout config.xml
# TOML 검증 (Python)
python -c "import tomli; tomli.load(open('config.toml', 'rb'))"

온라인 도구

# jq (JSON 쿼리)
cat config.json | jq '.database.host'
# "localhost"
# 배열 필터링
cat config.json | jq '.servers[] | select(.role == "primary")'
# 변환
cat config.json | jq '.database'
# yq (YAML 쿼리)
cat config.yaml | yq '.database.host'
# xmllint (XML 쿼리)
xmllint --xpath '//database/host/text()' config.xml

기타 형식

Properties (Java)

# application.properties (Spring Boot)
server.port=8080
server.address=0.0.0.0
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=pass
# 배열
spring.profiles.active=dev,debug
# 멀티라인 (백슬래시)
app.description=This is a very long \
                description that spans \
                multiple lines.

HCL (HashiCorp Configuration Language)

# Terraform
variable "region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  
  tags = {
    Name = "WebServer"
    Environment = "production"
  }
}
output "instance_ip" {
  value = aws_instance.web.public_ip
}

Jsonnet (JSON 템플릿)

// config.jsonnet
local env = std.extVar('env');
local base = {
  name: 'MyApp',
  version: '1.0.0',
};
local envConfig = {
  dev: {
    apiUrl: 'http://localhost:3000',
    debug: true,
  },
  prod: {
    apiUrl: 'https://api.example.com',
    debug: false,
  },
};
base + envConfig[env]
# 실행
jsonnet -V env=dev config.jsonnet
# {
#   "name": "MyApp",
#   "version": "1.0.0",
#   "apiUrl": "http://localhost:3000",
#   "debug": true
# }

ENV 파일

# .env
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
REDIS_URL=redis://localhost:6379
API_KEY=secret-key-123
DEBUG=false
# 배열 (비표준)
ALLOWED_HOSTS=localhost,example.com,*.example.com
# Python (python-dotenv)
from dotenv import load_dotenv
import os
load_dotenv()
port = int(os.getenv('PORT', 3000))
database_url = os.getenv('DATABASE_URL')
debug = os.getenv('DEBUG', 'false').lower() == 'true'
print(f"Port: {port}")
print(f"Database: {database_url}")
print(f"Debug: {debug}")

.env 파일은 사실상 표준 명세가 없다는 점을 기억해야 합니다. 따옴표 안의 #를 주석으로 볼지, export 접두어를 허용할지, ${VAR} 치환과 여러 줄 값을 지원할지가 python-dotenv, Node의 dotenv, Docker Compose의 env_file마다 조금씩 다릅니다. 같은 .env를 여러 도구가 함께 읽는다면 값에 공백·따옴표·#·$가 들어가지 않도록 단순하게 유지하는 편이 안전합니다. 또 모든 값이 문자열로 들어오므로, 위 예제처럼 DEBUG=false를 불린으로 쓰려면 직접 비교해야 합니다. if os.getenv('DEBUG'):로 검사하면 "false"라는 비어 있지 않은 문자열이 참으로 판정되어 운영 환경에서 디버그 모드가 켜지는 사고가 납니다.


주석과 환경별 설정에서 형식마다 막히는 지점

JSON에 주석이 필요할 때

// JSON은 주석 불가 → JSON5 사용
// package.json5
{
  // 프로젝트 정보
  name: "myapp",
  version: "1.0.0",
  
  // 의존성
  dependencies: {
    express: "^4.18.0",  // 후행 쉼표 가능
  },
}
// 주의: npm은 package.json5를 읽지 않음. JSON5는 이를 지원하는 도구의 설정 파일에만 사용

tsconfig.json이나 VS Code의 settings.json에 주석을 써도 에러가 나지 않는 것은 이 파일들이 표준 JSON이 아니라 주석을 허용하는 JSONC로 읽히기 때문입니다. 같은 내용을 JSON.parse()나 Python json.loads()에 넣으면 첫 //에서 바로 실패합니다. “이 파일에는 주석이 되던데”라는 경험을 다른 JSON 파일로 옮기면 안 되는 이유입니다. "_comment" 같은 키를 넣는 우회법도 있지만, 스키마 검증을 켜 둔 도구에서는 알 수 없는 키로 거부될 수 있습니다.

YAML 앵커로 환경별 설정 묶기

# config.yaml
default: &default
  timeout: 30
  retries: 3
development:
  <<: *default
  apiUrl: http://localhost:3000
  debug: true
production:
  <<: *default
  apiUrl: https://api.example.com
  debug: false

&default/*default 앵커는 YAML 스펙의 일부지만, << 병합 키는 YAML 1.1의 확장 타입이고 1.2 스펙에서는 빠졌습니다. PyYAML·js-yaml처럼 널리 쓰는 파서는 지원하지만, 1.2만 엄격히 따르는 파서는 <<를 그냥 문자열 키로 읽어 설정값이 조용히 누락될 수 있습니다. 또 병합은 얕은 병합이라, default 아래 중첩된 맵을 한 키만 바꾸려고 다시 쓰면 그 맵 전체가 대체됩니다. 환경별 차이가 깊은 구조까지 내려간다면 앵커보다 파일을 나눠 애플리케이션에서 병합하는 편이 추적하기 쉽습니다.


실전 설정 파일 예시

Node.js 프로젝트

// package.json
{
  "name": "myapp",
  "version": "1.0.0",
  "description": "My awesome application",
  "main": "dist/index.js",
  "scripts": {
    "start": "node dist/index.js",
    "dev": "nodemon src/index.ts",
    "build": "tsc",
    "test": "jest",
    "lint": "eslint src/**/*.ts"
  },
  "dependencies": {
    "express": "^4.18.0",
    "dotenv": "^16.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "nodemon": "^3.0.0",
    "jest": "^29.0.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Python 프로젝트

# pyproject.toml
[project]
name = "myapp"
version = "1.0.0"
description = "My Python application"
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [
    {name = "John Doe", email = "[email protected]"}
]
dependencies = [
    "fastapi>=0.104.0",
    "uvicorn[standard]>=0.24.0",
    "sqlalchemy>=2.0.0",
    "pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
    "pytest>=7.4.0",
    "black>=23.0.0",
    "mypy>=1.7.0",
    "ruff>=0.1.0",
]
[project.scripts]
myapp = "myapp.cli:main"
[tool.black]
line-length = 88
target-version = ['py311']
include = '\.pyi?$'
[tool.pytest.ini_options]
testpaths = [tests]
python_files = [test_*.py]
python_functions = [test_*]
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
[build-system]
requires = ["setuptools>=68.0.0", "wheel"]
build-backend = "setuptools.build_meta"

Rust 프로젝트

# Cargo.toml
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["John Doe <[email protected]>"]
description = "My Rust application"
license = "MIT"
repository = "https://github.com/user/myapp"
[dependencies]
tokio = { version = "1.35", features = [full] }
axum = "0.7"
serde = { version = "1.0", features = [derive] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] }
[dev-dependencies]
criterion = "0.5"
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
strip = true
[[bin]]
name = "myapp"
path = "src/main.rs"
[[bench]]
name = "my_benchmark"
harness = false

Kubernetes 설정

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
    version: v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
        version: v1
    spec:
      containers:
      - name: myapp
        image: myapp:1.0.0
        ports:
        - containerPort: 8080
          protocol: TCP
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
        - name: REDIS_URL
          value: redis://redis:6379
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
      imagePullSecrets:
      - name: registry-secret
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

YAML 파싱이 JSON보다 느린 이유

같은 데이터를 파싱해도 YAML이 JSON보다 눈에 띄게 느린 경우가 많은데, 이는 형식 자체의 복잡도와 구현 차이가 겹친 결과입니다. JSON 문법은 몇 가지 토큰만으로 끝나지만, YAML 파서는 들여쓰기로 구조를 판단하고, 앵커·별칭·태그를 처리하고, 따옴표 없는 값마다 숫자·불리언·날짜인지 타입을 추론해야 합니다. 여기에 Python에서는 표준 json 모듈이 C로 가속되는 반면, PyYAML의 yaml.safe_load()는 기본적으로 순수 Python 구현을 씁니다. libyaml이 설치되어 있다면 yaml.load(f, Loader=yaml.CSafeLoader)로 C 구현을 쓸 수 있습니다.

다만 설정 파일은 보통 애플리케이션 시작 시 한 번 읽으므로 이 차이가 체감되는 일은 드뭅니다. 파싱 속도가 문제가 되는 것은 YAML을 요청마다 파싱하거나 대량 데이터 교환 형식으로 쓸 때이고, 그런 용도라면 처음부터 JSON을 고르는 것이 맞습니다.


어떤 형식을 고를까

형식 선택 플로우차트

flowchart TD
    Start[설정 파일 형식 선택] --> Q1{주석 필요?}
    
    Q1 -->|No| Q2{파싱 속도 중요?}
    Q1 -->|Yes| Q3{복잡도는?}
    
    Q2 -->|Yes| JSON["✅ JSON\n- API 응답\n- 빠른 파싱"]
    Q2 -->|No| Q3
    
    Q3 -->|간단| INI["✅ INI\n- 간단한 설정\n- 레거시 호환"]
    Q3 -->|중간| TOML["✅ TOML\n- Python/Rust\n- 명확한 문법"]
    Q3 -->|복잡| YAML["✅ YAML\n- Docker/K8s\n- 가독성 최고"]
    
    Start --> Q4{문서 작성?}
    Q4 -->|Yes| MD["✅ Markdown\n- README\n- 블로그"]
    
    Start --> Q5{Java 프로젝트?}
    Q5 -->|Yes| XML["✅ XML\n- Maven\n- Spring"]

장단점 요약

형식최고 장점최대 단점추천 용도
JSON빠른 파싱주석 없음API, 데이터 교환
YAML가독성느린 파싱Docker, K8s, CI/CD
XML스키마 검증장황함Java, SOAP, RSS
TOML명확한 문법제한적 지원Rust, Python 설정
INI단순함표준 없음간단한 설정
Markdown읽기 쉬움데이터 구조 표현 제한문서, README

변환 치트시트

명령줄 도구

# JSON → YAML
cat config.json | yq -P > config.yaml
# YAML → JSON
cat config.yaml | yq -o=json > config.json
# JSON 포맷팅
cat config.json | jq . > formatted.json
# YAML 검증
yamllint config.yaml
# XML 포맷팅
xmllint --format config.xml
# JSON 병합
jq -s '.[0] * .[1]' base.json override.json > merged.json
# YAML 병합
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' base.yaml override.yaml

프로그래밍 언어별

# Python: 모든 형식 지원
import json        # 내장
import yaml        # pip install pyyaml
import tomli       # pip install tomli
import configparser  # 내장 (INI)
import xml.etree.ElementTree as ET  # 내장
# JavaScript/Node.js
const json = require('./config.json');  // 네이티브
const yaml = require('js-yaml');
const toml = require('toml');
const ini = require('ini');
# Go
import (
    "encoding/json"
    "gopkg.in/yaml.v3"
    "github.com/BurntSushi/toml"
    "gopkg.in/ini.v1"
)
# Rust
use serde_json;  // JSON
use serde_yaml;  // YAML
use toml;        // TOML

실전 시나리오

시나리오 1: 마이크로서비스 설정

# config/base.yaml
app:
  name: MyService
  version: 1.0.0
  
logging:
  level: info
  format: json
# config/dev.yaml
app:
  debug: true
database:
  host: localhost
  port: 5432
# config/prod.yaml
app:
  debug: false
database:
  host: db.example.com
  port: 5432
  ssl: true
  pool_size: 20
# Python으로 병합
import yaml
from pathlib import Path
def load_config(env='dev'):
    """환경별 설정 로드"""
    base = yaml.safe_load(Path('config/base.yaml').read_text())
    env_config = yaml.safe_load(Path(f'config/{env}.yaml').read_text())
    
    # 딥 머지
    def deep_merge(base, override):
        for key, value in override.items():
            if key in base and isinstance(base[key], dict) and isinstance(value, dict):
                deep_merge(base[key], value)
            else:
                base[key] = value
        return base
    
    return deep_merge(base, env_config)
config = load_config('prod')
print(config)

시나리오 2: CI/CD 파이프라인

# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
env:
  NODE_VERSION: '18'
  REGISTRY: ghcr.io
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage/lcov.info
  
  build:
    needs: test
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Build Docker image
      run: |
        docker build -t ${{ env.REGISTRY }}/myapp:${{ github.sha }} .
    
    - name: Push to registry
      if: github.ref == 'refs/heads/main'
      run: |
        echo ${{ secrets.GITHUB_TOKEN }} | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin
        docker push ${{ env.REGISTRY }}/myapp:${{ github.sha }}
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Deploy to production
      run: |
        kubectl set image deployment/myapp myapp=${{ env.REGISTRY }}/myapp:${{ github.sha }}

에디터 설정

VS Code

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.tabSize": 2,
  "files.associations": {
    "*.yaml": "yaml",
    "*.yml": "yaml",
    "Dockerfile*": "dockerfile"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.tabSize": 2
  },
  "[yaml]": {
    "editor.defaultFormatter": "redhat.vscode-yaml",
    "editor.insertSpaces": true,
    "editor.tabSize": 2,
    "editor.autoIndent": "advanced"
  },
  "[markdown]": {
    "editor.wordWrap": "on",
    "editor.quickSuggestions": false
  },
  "yaml.schemas": {
    "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
  }
}

EditorConfig

# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,jsx,ts,tsx,json}]
indent_style = space
indent_size = 2
[*.{yaml,yml}]
indent_style = space
indent_size = 2
[*.py]
indent_style = space
indent_size = 4
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

자주 묻는 질문 (FAQ)

Q. YAML에서 version: 1.0이나 time: 12:30을 따옴표 없이 쓰면 어떤 문제가 생기나요?

A. YAML은 따옴표 없는 값의 타입을 파서가 추론하므로, 1.0은 문자열이 아니라 숫자로 읽혀 1로 바뀌거나 1.10과 1.1이 같은 값이 될 수 있습니다. 콜론이 들어간 값은 키-값 구분자와 헷갈릴 수 있고, 파서 버전에 따라 12:30을 60진수 숫자로 해석하는 경우도 있습니다. 버전 번호, 시각, URL처럼 문자열로 다뤄야 하는 값은 항상 따옴표로 감싸는 것이 안전합니다.

참고 자료


같이 보면 좋은 글