Rust 프로젝트를 열어봤다면 TOML을 만났을 것입니다. pyproject.toml을 사용하는
Python 패키지를 만져봤다면 이미 사용한 것입니다. TOML — Tom's Obvious Minimal Language —
은 YAML의 공백 함정과 JSON의 주석 거부에 지친 개발자들이 조용히 선택하는 설정 파일 형식입니다.
GitHub 공동창업자 Tom Preston-Werner이 만든 TOML은 하나의 아이디어를 중심으로 설계되었습니다:
설정 형식은 명세 없이도 읽을 수 있을 만큼 명확해야 합니다. 그 약속을 지키는지 살펴봅시다.
TOML이란 정확히 무엇인가?
TOML은 세 가지 명시적 설계 목표를 가진 설정 파일 형식으로, 공식 명세 상단에 바로 나와 있습니다: 명확하게 읽힐 수 있어야 하고, 최소 복잡성을 가져야 하며, 모호하지 않게 해시 테이블 (대부분의 언어에서 딕셔너리/맵)에 매핑되어야 합니다. 세 번째 목표가 핵심입니다 — 모든 유효한 TOML 파일은 정확히 하나의 올바른 파싱 결과를 가집니다. 놀라운 유형 강제 변환 없음, YAML 스타일 불리언 함정 없음, 값이 문자열인지 숫자인지에 대한 모호함 없음.
형식은 오래된 INI 파일에서 섹션 헤더 스타일을 빌려왔지만 적절한 유형, 배열, 중첩 테이블을 추가합니다. 결과는 이전에 설정 파일을 편집해본 사람에게 친숙하게 느껴지지만, 파서가 실제 유형이 있는 데이터 모델을 제공할 수 있는 충분한 구조를 가집니다. 명세 버전 1.0.0은 수년의 개선 후 2021년 1월에 출시되었습니다 — 엣지 케이스를 파헤치고 싶다면 GitHub의 전체 TOML 명세를 확인할 수 있습니다.
기본: 키-값 쌍과 주석
TOML 파일은 키-값 쌍으로 구성됩니다. 키와 값은 =로 구분되고,
주석은 #으로 시작합니다. 간단합니다.
# Cargo.toml — Rust package manifest
[package]
name = "image-resizer"
version = "0.4.2"
edition = "2021"
authors = ["Ada Lovelace <[email protected]>"]
description = "Fast image resizing with Lanczos3 resampling"
license = "MIT"
repository = "https://github.com/example/image-resizer"
# Integers, floats, booleans — all native types
max_threads = 8
quality_default = 0.85
verbose_logging = false대부분의 키에 따옴표가 필요 없습니다. 값에는 유형이 있습니다 — 8은 정수,
0.85는 부동소수점, false는 불리언. 추측 없음, 값이 어떻게 보이는지에 따른
암묵적 강제 변환 없음. 이것이 90%의 시간에 작성하는 일상적인 TOML입니다.
문자열 유형: 기본, 리터럴, 멀티라인
TOML에는 네 가지 문자열 유형이 있습니다. 이것은 모든 실제 사례를 깔끔하게 처리합니다:
# Basic strings — double quotes, support escape sequences
greeting = "Hello, \nworld!"
path = "C:\\Users\\ada\\Documents"
# Literal strings — single quotes, no escape processing at all
regex_pattern = '\d{4}-\d{2}-\d{2}'
windows_path = 'C:\Users\ada'
# Multiline basic string — triple double quotes
sql_query = """
SELECT user_id, email, created_at
FROM users
WHERE active = true
AND created_at > '2024-01-01'
ORDER BY created_at DESC
"""
# Multiline literal string — triple single quotes, no escapes
shell_script = '''
#!/bin/bash
echo "Deploying $APP_NAME to $ENV"
kubectl apply -f k8s/
'''리터럴 문자열 유형('작은따옴표')은 사람들이 존재를 잊어버리는 것인데,
정말 유용합니다 — 정규식 패턴과 Windows 경로는 이스케이프 두 배 없이 훨씬 더 깔끔합니다.
백슬래시를 더 적게 쓰게 해주는 따옴표 스타일을 선택하세요.
숫자, 불리언, 날짜/시간
TOML의 네이티브 유형은 설정 파일에 실제로 넣을 모든 것을 처리합니다. 특히 1등급 날짜/시간 지원이 있습니다 — YAML이 기술적으로 가지고 있지만 파서마다 일관성 없이 처리하는 것.
# Integers — underscores allowed as separators (like numeric literals in code)
max_connections = 1_000_000
port = 5432
hex_color = 0xFF6B6B # hex prefix supported
octal_permissions = 0o755 # octal prefix supported
# Floats
pi = 3.14159265
compression_ratio = 1.5e-3
infinity_val = inf # special values: inf, -inf, nan
# Booleans — lowercase only (not True, TRUE, yes, on)
ssl_enabled = true
dry_run = false
# Datetimes — RFC 3339 format
created_at = 2024-03-15T09:30:00Z
updated_at = 2024-03-15T14:22:10+05:30
log_date = 2024-03-15 # local date (no time)
backup_time = 03:00:00 # local time (no date)true와 false만 —
True, TRUE, yes, on, 또는 YAML 1.1이 허용하는
다른 변형들은 안 됩니다. 이것은 의도적입니다. 문자열 "true"가 필요하다면 따옴표를 사용하세요.테이블: INI 스타일 섹션 헤더
TOML의 테이블은 [헤더] 구문으로 정의됩니다. 헤더 아래의 모든 것은 다음 헤더가
나타날 때까지 그 테이블에 속합니다. 이것이 TOML을 친숙하게 보이게 만드는 기능입니다 —
본질적으로 유형이 있는 INI 파일입니다.
[database]
host = "db.internal"
port = 5432
name = "app_production"
pool_size = 20
[database.credentials]
username = "app_user"
# Don't put real passwords here — use env vars or a secrets manager
password_env = "DB_PASSWORD"
[server]
host = "0.0.0.0"
port = 8080
workers = 4
[server.tls]
enabled = true
cert_file = "/etc/ssl/certs/app.crt"
key_file = "/etc/ssl/private/app.key"[database.credentials] 같은 점으로 구분된 헤더는 중첩 테이블을 만듭니다.
파싱 결과는 예상한 그대로입니다: 중첩된 credentials 객체가 있는 database 객체.
단순한 경우에는 인라인 테이블로 작성할 수도 있습니다 — 아래에서 더 자세히 설명합니다.
배열과 테이블 배열
TOML의 배열은 대괄호를 사용하며 여러 줄에 걸쳐 있을 수 있습니다. 정말 독특한 TOML 기능은
테이블 배열입니다 — 이중 괄호 [[헤더]]로 정의됩니다. 이것은 TOML이
"객체 목록을 어떻게 표현하는가?"에 대한 답으로 JSON처럼 보이지 않게 합니다.
# Regular arrays — can be split across lines, trailing comma is fine
allowed_origins = [
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]
supported_formats = ["jpeg", "png", "webp", "avif"]
retry_delays_ms = [100, 250, 500, 1000, 2000]
# Array of Tables — [[double brackets]]
# Each [[servers]] header appends a new object to the servers array
[[servers]]
name = "web-01"
ip = "10.0.1.10"
role = "primary"
tags = ["web", "prod"]
[[servers]]
name = "web-02"
ip = "10.0.1.11"
role = "replica"
tags = ["web", "prod"]
[[servers]]
name = "db-01"
ip = "10.0.2.10"
role = "primary"
tags = ["database", "prod"][[servers]] 구문은 세 개의 객체 배열로 파싱됩니다 — JSON에서
"servers": [{...}, {...}, {...}]에 해당합니다. JSON 객체 배열에 비해 장황하지만,
각 항목에 많은 필드가 있을 때 가독성이 장점입니다. 여러 이진 대상, 예제, 벤치 항목을 정의하는
Cargo.toml 매니페스트에서
이 패턴을 많이 볼 수 있습니다.
인라인 테이블: 간결한 한 줄
테이블에 필드가 몇 개뿐이고 전체 섹션 헤더를 원하지 않을 때, 인라인 테이블을 사용하면 한 줄로 작성할 수 있습니다:
[build]
# Inline table — must stay on one line
target = { arch = "x86_64", os = "linux", libc = "musl" }
# Equivalent to writing:
# [build.target]
# arch = "x86_64"
# os = "linux"
# libc = "musl"
[feature_flags]
auth = { enabled = true, rollout_pct = 100 }
search = { enabled = true, rollout_pct = 50 }
beta = { enabled = false, rollout_pct = 0 }인라인 테이블은 한 줄에 있어야 하며 나중에 [헤더]로 확장할 수 없습니다.
좌표 쌍, 빌드 대상, 단순 플래그 설정 같은 것들에 좋습니다. 세 개나 네 개 이상의 필드가 있을 때는
사용하지 마세요; 그 시점에서는 일반 테이블이 더 잘 읽힙니다.
실제 세계에서의 TOML
TOML은 Rust와 Python 생태계에서 강력한 틈새를 개척했으며, 다른 곳에서도 인기를 얻고 있습니다. 일상에서 어디서 만날지 알아봅시다:
- Rust — Cargo.toml: 모든 Rust 프로젝트에 있습니다. 패키지 메타데이터, 의존성, 기능, 빌드 대상을 정의합니다. Cargo 매니페스트 참조는 찾을 수 있는 가장 자세한 실제 TOML 사용 가이드입니다.
- Python — pyproject.toml: PEP 518과 PEP 621이 TOML을 Python 프로젝트 메타데이터 형식으로 표준화했습니다. Poetry, Hatch, PDM, setuptools 모두 이것을 읽습니다.
- Deno: Deno 설정 파일은 JSON 외에 TOML도 지원합니다.
- Hugo: Hugo 정적 사이트 생성기는 설정 및 전면 메타데이터 형식으로 TOML을 허용합니다 — Markdown 파일 상단에서
+++구분자 사이에서 볼 수 있습니다. - uv: Astral의 빠른 Python 패키지 매니저는 모든 설정에 pyproject.toml을 사용하여 TOML을 새로운 Python 도구의 실질적인 표준으로 만들었습니다.
기존 설정을 형식 간에 변환해야 한다면, TOML을 JSON으로와 JSON을 TOML로 변환기가 구조적 매핑을 처리합니다. 또는 TOML 포매터로 기존 파일의 일관성 없는 간격을 정리하고, TOML 검증기로 프로덕션에서 나타나기 전에 구문 오류를 잡으세요.
Python에서 TOML 파싱
Python 3.11부터 표준 라이브러리에
tomllib가
포함됩니다 — 외부 의존성이 필요 없습니다. Python 3.9와 3.10에서는
tomli 백포트가
동일한 API를 가지므로 단일 import 별칭으로 전환할 수 있습니다.
import sys
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib # pip install tomli
# tomllib only reads binary mode — open with "rb"
with open("pyproject.toml", "rb") as f:
config = tomllib.load(f)
# Types match TOML exactly: str, int, float, bool, datetime, list, dict
project_name = config["project"]["name"] # str
python_requires = config["project"]["requires-python"] # str
dependencies = config["project"]["dependencies"] # list[str]
print(f"Project: {project_name}")
print(f"Requires Python: {python_requires}")
print(f"Dependencies ({len(dependencies)}):")
for dep in dependencies:
print(f" {dep}")
# Parse from a string with tomllib.loads()
raw = """
[server]
host = "localhost"
port = 8080
debug = true
"""
server_config = tomllib.loads(raw)
print(server_config["server"]["port"]) # 8080 (int, not "8080")tomllib.load()는 바이너리 모드("rb")를 요구합니다. 이것은 의도적입니다 —
TOML은 UTF-8 인코딩을 요구하며, 바이너리 모드로 열면 파서가 스스로 인코딩 검사를 처리할 수 있습니다.
처음으로 사람들이 걸리는 작은 함정입니다.
Rust에서 TOML 파싱
Rust에서는 toml 크레이트가
표준 선택입니다. serde와 긴밀하게 통합되므로 최소한의 보일러플레이트로 자체 구조체로
직접 역직렬화할 수 있습니다:
use serde::Deserialize;
use std::fs;
#[derive(Debug, Deserialize)]
struct AppConfig {
server: ServerConfig,
database: DatabaseConfig,
feature_flags: FeatureFlags,
}
#[derive(Debug, Deserialize)]
struct ServerConfig {
host: String,
port: u16,
workers: usize,
}
#[derive(Debug, Deserialize)]
struct DatabaseConfig {
host: String,
port: u16,
name: String,
pool_size: u32,
}
#[derive(Debug, Deserialize)]
struct FeatureFlags {
enable_beta: bool,
max_upload_mb: u32,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let raw = fs::read_to_string("config.toml")?;
let config: AppConfig = toml::from_str(&raw)?;
println!("Server: {}:{}", config.server.host, config.server.port);
println!("DB pool size: {}", config.database.pool_size);
println!("Beta enabled: {}", config.feature_flags.enable_beta);
Ok(())
}Cargo.toml의 [dependencies]에 toml = "0.8"과
serde = { version = "1", features = ["derive"] }를 추가하면 준비됩니다.
serde 파생 매크로가 모든 필드 매핑을 처리합니다. 구조체 필드 이름이 snake_case를 사용하지만
TOML 키가 kebab-case를 사용하는 경우, 구조체 수준에서 #[serde(rename_all = "kebab-case")]를
추가하면 모든 것이 자동으로 매핑됩니다.
TOML vs YAML vs JSON — 언제 무엇을 선택할까
이 질문은 모든 새 프로젝트에서 나옵니다. 솔직한 요약입니다:
- TOML: 인간이 작성하고 유지 관리하는 설정 파일로, 유형이 있는 값이 중요하고 중첩이 얕은 경우에 최적입니다. 최적 지점: 앱 설정, 빌드 매니페스트, 도구 설정. 4단계 이상의 깊은 중첩에서는 반복 섹션 헤더로 어색해집니다.
- YAML: 많은 객체 목록으로 구조화된 데이터를 작성할 때 최적입니다(Kubernetes 매니페스트, GitHub Actions 워크플로우). 멀티라인 문자열 지원이 TOML보다 실제로 더 좋습니다. 단점: 공백 민감성과 YAML 1.1 유형 강제 변환이 실제로 버그를 만듭니다.
- JSON: 기계 간 데이터 교환, API, 가장 넓은 도구 체인 지원이 필요할 때 최적입니다. 인간이 유지 관리하는 설정에는 적합하지 않습니다 — 주석 없음, 문자열 이스케이핑이 번거롭습니다.
실제 pyproject.toml
모든 것을 합쳐서, 여기 Python 라이브러리의 현실적인 pyproject.toml이 있습니다 —
현대 오픈 소스 프로젝트에서 찾을 수 있는 종류. 형식이 한 눈에 스캔하기 쉬우면서 많은 구조적 정보를
담고 있는 방식을 주목하세요:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "httpx-cache"
version = "1.2.0"
description = "Transparent HTTP caching layer for httpx"
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.9"
authors = [
{ name = "Ada Lovelace", email = "[email protected]" },
]
keywords = ["http", "cache", "httpx", "async"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"httpx>=0.25.0",
"anyio>=4.0.0",
]
[project.optional-dependencies]
redis = ["redis>=5.0.0"]
dev = [
"pytest>=7.4.0",
"pytest-asyncio>=0.23.0",
"coverage[toml]>=7.3.0",
"ruff>=0.1.0",
"mypy>=1.7.0",
]
[project.urls]
Homepage = "https://github.com/example/httpx-cache"
Changelog = "https://github.com/example/httpx-cache/blob/main/CHANGELOG.md"
[tool.ruff]
line-length = 100
target-version = "py39"
[tool.ruff.lint]
select = ["E", "F", "I", "UP"]
[tool.mypy]
strict = true
python_version = "3.9"
[tool.coverage.run]
source = ["httpx_cache"]
branch = true
[tool.coverage.report]
fail_under = 90이것은 실제 작업을 수행하는 실제 TOML입니다. 각 [tool.x] 섹션은
다른 도구 — ruff, mypy, coverage — 를 위한 별도 네임스페이스로, 서로 발을 밟지 않고 하나의 파일에 있습니다.
깊은 중첩이 필요 없고, 모든 것이 한 눈에 읽힙니다.
마무리
TOML은 약속을 이행합니다: 읽기 쉽고, 모호하지 않으며, 깔끔하게 유형이 있습니다. Rust, Python, 또는 Go로 새 프로젝트를 시작한다면, TOML이 설정 파일의 기본값으로 시도해볼 가치가 있습니다 — 특히 소스 제어에 커밋되고 여러 사람이 편집하는 파일에서. 공백 민감성이 없다는 것만으로도 YAML에 비해 안도감을 줍니다. 브라우저에서 직접 TOML 파일을 다루려면 TOML 포매터와 TOML 검증기가 가장 일반적인 작업을 처리합니다. 그리고 기존 프로젝트를 JSON에서 마이그레이션하거나 TOML을 JSON 기반 파이프라인에 연결해야 한다면, TOML을 JSON으로와 JSON을 TOML로 변환기가 도움이 됩니다.