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)

---
수평선
| 컬럼 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
비교표
| 특성 | JSON | YAML | XML | TOML | INI | Markdown |
|---|---|---|---|---|---|---|
| 가독성 | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 파싱 속도 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 주석 | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 타입 안정성 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | 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.js | JSON | package.json |
| Python | TOML | pyproject.toml |
| Rust | TOML | Cargo.toml |
| Go | Go | go.mod |
| Docker | YAML | docker-compose.yml |
| Kubernetes | YAML | deployment.yaml |
| Ansible | YAML | playbook.yml |
| Maven | XML | pom.xml |
| Gradle | Groovy | build.gradle |
| Nginx | Custom | nginx.conf |
| Apache | Custom | httpd.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처럼 문자열로 다뤄야 하는 값은 항상 따옴표로 감싸는 것이 안전합니다.
참고 자료
- JSON Specification
- YAML Specification
- XML Specification
- TOML Specification
- CommonMark (Markdown)
- JSON Schema
- YAML Lint