W3C DTCG Format
이 문서가 답하는 질문: W3C Design Tokens Community Group 포맷의 정확한 문법은?
$value,$type,$description,$extensions는 각각 어떤 역할이고 alias{ref}구문은 어떻게 동작하는가. 한 줄 답 (Pyramid Top): DTCG는 **“토큰을 JSON 객체로 표현하되, value`를 가지면 토큰이다.
Why — 왜 또 다른 JSON 포맷이 필요했나
2014~2020년 사이, 디자인 토큰 포맷은 벤더마다 달랐다.
| 벤더 / 도구 | 포맷 예시 | 문제 |
|---|---|---|
| Salesforce Theo | { "props": [{ "name": "COLOR_PRIMARY", "value": "#3b82f6" }] } | 배열 기반, 그룹 표현 어색 |
| Style Dictionary (v3) | { "color": { "primary": { "value": "#3b82f6" } } } | value가 일반 키와 충돌 가능 |
| Tokens Studio (Figma) | 자체 포맷, set 개념 추가 | 도구 lock-in |
| Adobe Spectrum | 별도 YAML | 다른 도구가 못 읽음 |
문제: Figma에서 export한 JSON을 Style Dictionary가 못 읽고, Style Dictionary가 만든 JSON을 Tokens Studio가 못 읽는다. N × N 변환이 필요했다.
2021년 W3C Design Tokens Community Group(DTCG)이 결성된 동기는 단순했다: “하나의 JSON을 모두가 읽게 하자”. Figma, Salesforce, Adobe, Atlassian, Microsoft, Google 등 주요 벤더가 참여했다.
How — DTCG의 5가지 규칙
규칙 1: 일반 키는 그룹, $value를 가진 객체는 토큰
{
"color": { // 그룹
"primary": { // 토큰 (아래에 $value 있음)
"$value": "#3b82f6",
"$type": "color"
},
"brand": { // 그룹 (안에 또 토큰들)
"blue": {
"$value": "#3b82f6",
"$type": "color"
}
}
}
}토큰 식별 규칙: “$value 키가 있으면 토큰, 없으면 그룹”. 그룹은 임의 깊이로 중첩 가능.
규칙 2: $로 시작하는 키만 예약어
| 예약 키 | 의무 | 위치 | 의미 |
|---|---|---|---|
$value | 필수 (토큰일 때) | 토큰 | 실제 값 또는 alias |
$type | 강력 권장 | 토큰 또는 그룹 | 값의 타입 |
$description | 선택 | 토큰 또는 그룹 | 사람용 docs |
$extensions | 선택 | 토큰 또는 그룹 | 벤더 확장 |
$deprecated | 선택 | 토큰 | deprecated 마커 |
$가 없는 키는 항상 그룹 이름 또는 토큰 이름. 충돌 없음.
규칙 3: alias는 {path.to.token}
{
"color": {
"blue": {
"500": { "$value": "#3b82f6", "$type": "color" }
},
"primary": {
"$value": "{color.blue.500}", // ← alias
"$type": "color"
}
}
}중괄호 안에 그룹 경로를 점(.)으로 잇는다. resolver는 재귀적으로 따라가 최종 값에 도달한다.
규칙 4: $type은 부모 그룹에서 상속
{
"color": {
"$type": "color", // ← 그룹 레벨에 한 번
"blue": {
"500": { "$value": "#3b82f6" } // $type 생략 가능 — 그룹에서 상속
},
"red": {
"500": { "$value": "#ef4444" }
}
}
}$type을 매 토큰에 쓰지 않아도 그룹 레벨에 한 번 적으면 모든 자식 토큰이 상속한다.
규칙 5: composite token은 $value가 객체
{
"shadow": {
"card": {
"$type": "shadow",
"$value": {
"color": "{color.shadow.subtle}",
"offsetX": "0px",
"offsetY": "2px",
"blur": "8px",
"spread": "0px"
}
}
}
}color, dimension 같은 원자 타입은 $value가 string. shadow, typography, border, transition, gradient 같은 composite 타입은 $value가 객체다.
What — $type의 완전한 카탈로그
원자 타입 (string 또는 number $value)
$type | $value 형식 | 예시 |
|---|---|---|
color | hex / rgb / hsl / oklch | "#3b82f6", "oklch(0.6 0.2 240)" |
dimension | "<number><unit>" | "16px", "1.5rem" |
duration | "<number>ms" | "150ms" |
fontFamily | string 또는 string 배열 | "Inter", ["Inter", "sans-serif"] |
fontWeight | number 또는 string | 400, "bold" |
number | number | 1.5 |
cubicBezier | 4-tuple 배열 | [0.4, 0, 0.2, 1] |
strokeStyle | enum 또는 dashArray 객체 | "solid", { "dashArray": ["2px","4px"] } |
Composite 타입 ($value가 객체)
$type | 필드 |
|---|---|
shadow | color, offsetX, offsetY, blur, spread, inset? |
border | color, width, style |
transition | duration, delay, timingFunction |
gradient | [{ color, position }] 배열 |
typography | fontFamily, fontWeight, fontSize, lineHeight, letterSpacing, textTransform, textDecoration |
typography 예시
{
"typography": {
"heading-lg": {
"$type": "typography",
"$value": {
"fontFamily": "{font.family.sans}",
"fontWeight": "{font.weight.bold}",
"fontSize": "{dim.32}",
"lineHeight": "1.2",
"letterSpacing": "-0.02em"
}
}
}
}composite의 각 필드 자체도 alias 가능. typography.heading-lg.fontSize가 dim.32를 가리키고, 그것이 "32px"로 풀린다.
What — $extensions로 벤더 확장
DTCG 표준에 없는 메타데이터는 $extensions 안에 역도메인 네임스페이스로 넣는다.
{
"color": {
"primary": {
"$value": "{color.blue.500}",
"$type": "color",
"$extensions": {
"com.figma": {
"styleId": "S:abc123",
"componentVariant": "Primary/Default"
},
"studio.tokens": {
"modify": {
"type": "darken",
"value": "0.1"
}
},
"co.example.brand": {
"wcagContrast": 4.7
}
}
}
}
}규약:
- 키는 reverse domain. 충돌 방지.
- DTCG는 값을 검증하지 않음. 도구가 자기 영역만 읽음.
- 알 수 없는
$extensions키는 무시 (forward compatibility).
What — 완전한 실전 예시
다음은 실제로 동작하는 최소 토큰 세트.
tokens/color.primitive.json:
{
"color": {
"$type": "color",
"blue": {
"400": { "$value": "#60a5fa" },
"500": { "$value": "#3b82f6" },
"600": { "$value": "#2563eb" }
},
"gray": {
"50": { "$value": "#f9fafb" },
"900": { "$value": "#111827" }
}
}
}tokens/color.semantic.light.json:
{
"color": {
"$type": "color",
"primary": {
"$value": "{color.blue.500}",
"$description": "Primary brand — buttons, links, focus rings",
"$extensions": {
"com.figma": { "styleId": "S:primary" }
}
},
"primary-hover": { "$value": "{color.blue.600}" },
"bg": {
"surface": { "$value": "{color.gray.50}" }
},
"fg": {
"default": { "$value": "{color.gray.900}" }
}
}
}tokens/spacing.json:
{
"dim": {
"$type": "dimension",
"0": { "$value": "0px" },
"1": { "$value": "4px" },
"2": { "$value": "8px" },
"4": { "$value": "16px" },
"8": { "$value": "32px" }
}
}tokens/shadow.json:
{
"shadow": {
"$type": "shadow",
"sm": {
"$value": {
"color": "rgba(0,0,0,0.05)",
"offsetX": "0px",
"offsetY": "1px",
"blur": "2px",
"spread": "0px"
}
},
"md": {
"$value": {
"color": "rgba(0,0,0,0.08)",
"offsetX": "0px",
"offsetY": "4px",
"blur": "8px",
"spread": "0px"
}
}
}
}What-if — DTCG의 함정
함정 1: $type 누락
// ❌ $type 없음
{
"color": {
"primary": { "$value": "#3b82f6" }
}
}증상: Style Dictionary가 #3b82f6을 string으로 취급해 iOS UIColor 생성 실패.
원인: transform이 어떤 변환을 적용할지를 $type으로 결정.
대응: 그룹 레벨에 $type 한 번 적기.
함정 2: alias 경로 오타
{
"color": {
"primary": { "$value": "{color.bluuue.500}" } // 오타
}
}증상: Style Dictionary가 Reference doesn't exist 에러. 또는 일부 builder는 그대로 출력 → CSS에 var(--color-bluuue-500) 등장.
대응: 빌드에 usesReferences() 체크 활성화. CI에서 dangling reference 실패.
함정 3: alias cycle
{
"color": {
"a": { "$value": "{color.b}" },
"b": { "$value": "{color.a}" }
}
}증상: 무한 루프 또는 stack overflow. 대응: Style Dictionary v4는 cycle detection 내장. 명시적 에러로 빠짐.
함정 4: composite 토큰의 alias 깊이
{
"typography": {
"heading": {
"$type": "typography",
"$value": {
"fontFamily": "{font.family}", // ← 이 토큰이 또 alias라면?
"fontSize": "{dim.32}"
}
}
}
}증상: composite 안의 alias가 깊어지면 resolve 순서에 따라 결과가 달라질 수 있음. 대응: composite 안의 alias는 한 단계까지만. 깊은 체인 금지.
함정 5: $value라는 이름의 그룹
// ❌ 절대 금지
{
"color": {
"$value": { // ← 이게 토큰? 그룹?
"primary": { ... }
}
}
}DTCG는 $value를 예약했다. 그룹 이름으로 쓰면 파서가 토큰으로 오해.
함정 6: spec 버전 혼동
- draft-1(2022): 일부 도구가
value(no$)도 허용 — 비표준 - 현재 (2024 Editor’s Draft):
$value만. Style Dictionary v4는 strict
레거시 토큰 파일이 value로 되어 있다면 $value로 마이그레이션 필수.
Insight — 왜 $인가, 왜 alias는 {}인가
이 두 가지 작은 선택이 DTCG의 우아함을 결정한다.
$의 선택
JSON에는 예약어가 없다. 어떤 키 이름이든 토큰 이름이 될 수 있다 — color, value, type, description. DTCG가 만약 value, type(no $)을 메타 키로 썼다면, 디자이너가 “value”라는 색 이름을 못 쓰게 됐을 것이다.
$ 접두사는 GraphQL의 __typename, BSON의 $set, JSON Schema의 $ref와 같은 escape 전략이다. 사용자 공간과 메타 공간을 문자 하나로 분리한다.
{}의 선택
alias 문법으로 다른 후보들이 있었다:
"$ref": "color.blue.500"— JSON Schema 방식"$alias": "color.blue.500"— 명시적"color.blue.500"— alias인지 값인지 구분 불가
DTCG는 string 안에 {}로 감싸는 길을 택했다. 이유:
- 문법적으로 안전:
#3b82f6과{color.blue.500}은 string 패턴으로 구분 가능 - CSS variables와의 시각적 유사성:
var(--color-blue-500)와 같은 치환의 인상 - composite 안에서도 동작:
"fontSize": "{dim.32}"처럼 객체 필드에서도 자연스러움
이 작은 선택 덕에 DTCG JSON을 사람이 읽을 때, alias가 한눈에 보인다. 도구가 아니라 사람을 위한 포맷이라는 철학이 여기 있다.
표준화의 정치
DTCG는 기술적으로 평범하지만 정치적으로 어려운 사례다. Figma, Adobe, Salesforce, Microsoft가 자기 포맷을 양보해야 했다. 2021~2024년의 협상 과정이 GitHub 토론에 공개되어 있다. “왜 value가 아니라 $value인가?”에만 100개 댓글이 달렸다. 표준은 기술이 아니라 정치다.
요약
- DTCG는 JSON 한 벌로 모든 디자인 토큰을 표현하는 W3C 커뮤니티 표준.
- 5가지 규칙: ①
$value있으면 토큰 ②$는 예약어 ③ alias는{path}④$type은 그룹 상속 ⑤ composite는 객체$value. $extensions로 벤더별 확장. reverse domain 키.- 모든 주요 도구(Figma, Style Dictionary v4, Tokens Studio, Penpot)가 채택.