디자인 토큰은 디자이너와 개발자 사이의 공통 어휘입니다. Salesforce Lightning 시대부터 Style Dictionary, Theo, W3C Design Token Community Group 스펙 초안 등 다양한 형태로 존재해 왔습니다. 최근 달라진 점은 이제 코드베이스와 AI 코딩 에이전트 사이의 공통 어휘이기도 하다는 것입니다.
color.accent = #d96a3a를 아는 에이전트는 그 값을 사용합니다. 스크린샷을 받은 에이전트는 "따뜻한 주황색"으로 추측하고 비슷하지만 틀린 값을 생성합니다. 이 차이는 수십 개의 컴포넌트에 걸쳐 누적됩니다.
figmascope는 모든 컨텍스트 번들의 일부로 tokens.json을 내보냅니다. 이 글은 형식을 자세히 설명하고, figmascope가 Figma에서 토큰을 가져오는 방법, Variables가 없는 파일을 위한 빈도 추론 폴백, 그리고 일반 프레임워크에 출력을 연결하는 방법을 다룹니다. 전체 내보내기 워크플로우에서 토큰이 어떻게 맞는지 보려면 figmascope 앱을 방문하거나 Figma to Cursor와 Figma to Claude Code를 읽어보세요.
tokens.json 형식
figmascope는 W3C Design Token Community Group에서 영감을 받은 형식을 사용합니다. 각 토큰은 $value와 $type 필드를 가진 객체로, 의미론적 카테고리 키 아래 중첩됩니다:
{
"color": {
"surface": { "$value": "#f6f2ea", "$type": "color" },
"surface-2": { "$value": "#efe9dc", "$type": "color" },
"ink": { "$value": "#1f1d1a", "$type": "color" },
"ink-muted": { "$value": "#4a4641", "$type": "color" },
"accent": { "$value": "#d96a3a", "$type": "color" },
"accent-soft": { "$value": "#f2c7a8", "$type": "color" },
"good": { "$value": "#6a8f5a", "$type": "color" },
"warn": { "$value": "#c89a3a", "$type": "color" },
"bad": { "$value": "#b8553a", "$type": "color" }
},
"spacing": {
"1": { "$value": "4px", "$type": "dimension" },
"2": { "$value": "8px", "$type": "dimension" },
"3": { "$value": "12px", "$type": "dimension" },
"4": { "$value": "16px", "$type": "dimension" },
"6": { "$value": "24px", "$type": "dimension" },
"8": { "$value": "32px", "$type": "dimension" },
"12": { "$value": "48px", "$type": "dimension" },
"16": { "$value": "64px", "$type": "dimension" }
},
"radius": {
"sm": { "$value": "4px", "$type": "dimension" },
"md": { "$value": "8px", "$type": "dimension" },
"lg": { "$value": "16px", "$type": "dimension" },
"full": { "$value": "9999px", "$type": "dimension" }
},
"typography": {
"body": {
"fontFamily": { "$value": "Inter", "$type": "fontFamily" },
"fontSize": { "$value": "14px", "$type": "dimension" },
"fontWeight": { "$value": 400, "$type": "number" },
"lineHeight": { "$value": 1.45, "$type": "number" }
},
"heading": {
"sm": {
"fontFamily": { "$value": "Inter", "$type": "fontFamily" },
"fontSize": { "$value": "18px", "$type": "dimension" },
"fontWeight": { "$value": 600, "$type": "number" },
"lineHeight": { "$value": 1.25, "$type": "number" }
}
},
"mono": {
"fontFamily": { "$value": "JetBrains Mono", "$type": "fontFamily" },
"fontSize": { "$value": "13px", "$type": "dimension" },
"fontWeight": { "$value": 400, "$type": "number" }
}
}
}
W3C 유사인 이유 — 스펙을 정확히 따르지 않는 이유
W3C Design Token Community Group 스펙은 여전히 초안입니다. figmascope는 핵심 컨벤션($value, $type, 중첩 그룹)을 따르지만 복합 타입과 별칭 해석 체인 같은 엣지 케이스는 구현하지 않습니다. 출력은 Style Dictionary, 직접 JSON 순회, 또는 언어 모델 — 주요 소비자 — 이 사용하기에 충분히 안정적입니다.
사용 중인 토큰 타입
| $type | 카테고리 | $value 형식 | 예시 |
|---|---|---|---|
color |
색상 | hex 문자열 | "#d96a3a" |
dimension |
간격, 반경 | 단위가 있는 CSS 문자열 | "16px" |
fontFamily |
타이포그래피 | 폰트 이름 문자열 | "Inter" |
number |
타이포그래피 | 단위 없는 숫자 | 400, 1.45 |
색상은 항상 hex 문자열로 출력됩니다. Figma 소스가 완전하지 않은 불투명도의 RGBA를 사용하면 알파 채널이 8자리 hex(#d96a3a80)로 보존됩니다.
figmascope의 토큰 소스 전략
figmascope는 2단계 소스 전략을 사용합니다. Figma Variables가 선호 소스이며, 빈도 추론이 폴백입니다.
1단계: Figma Variables
Figma 파일에 Variables가 정의되어 있으면(Professional 및 Organization 플랜에서 사용 가능한 Figma의 네이티브 토큰 시스템), figmascope는 REST API의 /v1/files/:key/variables/local 엔드포인트를 통해 읽습니다. Variable 이름은 토큰 키로 사용되며 kebab-case로 정규화됩니다. 모드는 기본 모드로 축소되며, 파일에 단일 모드가 있는 경우 해당 모드의 값이 직접 사용됩니다.
Variable 컬렉션은 tokens.json의 최상위 카테고리 키에 매핑됩니다. "Color"라는 컬렉션은 tokens.color.*를 생성하고, "Spacing"은 tokens.spacing.*를 생성합니다. Figma 파일에 비표준 컬렉션 이름이 있으면 figmascope는 variable의 해석된 타입에서 카테고리를 추론합니다.
2단계: 빈도 추론
많은 Figma 파일 — 특히 오래된 파일이나 Variables를 도입하지 않은 클라이언트의 파일 — 에는 variable 정의가 없습니다. figmascope는 전체 노드 트리를 순회하고 채우기 색상, 간격 값, 모서리 반경, 타이포그래피 속성의 빈도 히스토그램을 구축하여 이를 처리합니다.
빈도 임계값 이상으로 나타나는 값은 후보 토큰이 됩니다. 값이 어떻게 사용되는지에서 사람이 읽을 수 있는 이름을 추론할 수 없는 경우 카테고리와 순차 인덱스로 이름이 붙습니다(color.0, color.1...). 여러 프레임에서 배경에만 사용된 색상은 color.surface가 됩니다.
_meta.json 매니페스트는 tokensSource를 "variables" 또는 "inferred"로 기록하므로 에이전트 프롬프트에서 추론이 사용된 경우를 표시할 수 있습니다:
// _meta.json — inferred fallback
{
"tokensSource": "inferred",
"warnings": [
{
"code": "tokens-inferred",
"message": "No Figma Variables found. Tokens were inferred from usage frequency."
}
]
}
추론된 토큰은 유용하지만 권위가 있지는 않습니다. 디자인 시스템 스펙이 아닌 시작점으로 취급하세요.
레이아웃 IR에서의 토큰 참조
화면별 IR 파일(screens/*.json)은 원시 값을 내장하는 대신 $ref 문자열을 사용하여 경로로 토큰을 참조합니다. 이는 IR을 컴팩트하게 유지하고 에이전트가 항상 단일 진실의 원천에서 값을 해석하도록 합니다:
{
"kind": "stack",
"name": "PrimaryButton",
"direction": "horizontal",
"gap": { "$ref": "spacing.2" },
"padding": {
"top": 10, "right": 16, "bottom": 10, "left": 16
},
"background": { "$ref": "color.accent" },
"radius": { "$ref": "radius.sm" },
"children": [
{
"kind": "leaf",
"name": "ButtonLabel",
"type": "text",
"style": { "$ref": "typography.body" },
"color": { "$ref": "color.surface" }
}
]
}
이 노드를 처리하는 에이전트는 병렬 tokens.json 파일에서 color.accent를 #d96a3a로, spacing.2를 8px로 해석합니다. 모호함도 없고 값의 환각도 없습니다.
전체 노드 스키마 문서는 화면별 IR 설명을 참고하세요.
Style Dictionary와 tokens.json 사용
figmascope의 출력은 최소한의 설정으로 Style Dictionary와 호환됩니다. 이미 $value/$type 컨벤션을 사용하므로 tokens.json을 직접 Style Dictionary에 지정할 수 있습니다:
// style-dictionary.config.js
module.exports = {
source: ['design/tokens.json'],
platforms: {
css: {
transformGroup: 'css',
prefix: 'fs',
buildPath: 'src/styles/',
files: [{ destination: 'tokens.css', format: 'css/variables' }]
},
js: {
transformGroup: 'js',
buildPath: 'src/',
files: [{ destination: 'tokens.js', format: 'javascript/es6' }]
}
}
};
style-dictionary build를 실행하면 에이전트가 사용하는 동일한 소스 파일에서 CSS custom properties와 ES 모듈 exports를 생성합니다.
Tailwind와 tokens.json 사용
작은 스크립트로 tokens.json을 Tailwind theme extension으로 변환합니다. 구조는 일반적인 경우에 재귀 없이 순회할 만큼 충분히 평평합니다:
// scripts/tokens-to-tailwind.js
const fs = require('fs');
const tokens = JSON.parse(fs.readFileSync('design/tokens.json', 'utf8'));
function flattenGroup(group) {
return Object.fromEntries(
Object.entries(group).map(([k, v]) => [k, v.$value])
);
}
const extend = {
colors: flattenGroup(tokens.color),
spacing: flattenGroup(tokens.spacing),
borderRadius: flattenGroup(tokens.radius),
};
console.log(JSON.stringify(extend, null, 2));
node scripts/tokens-to-tailwind.js > design/tailwind-extend.json
그런 다음 tailwind.config.ts에서:
import extend from './design/tailwind-extend.json';
export default {
content: ['./src/**/*.{ts,tsx}'],
theme: { extend },
};
Jetpack Compose와 tokens.json 사용
Android 프로젝트의 경우 토큰 구조가 Kotlin object에 깔끔하게 매핑됩니다. 프로그래밍 방식으로 생성하거나 Claude Code에게 요청할 수 있습니다. 색상 토큰의 경우:
// Generated from design/tokens.json
object DesignTokens {
object Color {
val surface = Color(0xFFF6F2EA)
val ink = Color(0xFF1F1D1A)
val accent = Color(0xFFD96A3A)
val accentSoft = Color(0xFFF2C7A8)
}
object Spacing {
val s1 = 4.dp
val s2 = 8.dp
val s4 = 16.dp
val s8 = 32.dp
}
}
MaterialTheme 통합을 포함한 완전한 가이드는 Jetpack Compose from Figma를 참고하세요.
tokens.json에 포함되지 않는 것
범위 한계를 알면 올바른 기대치를 설정하는 데 도움이 됩니다:
- 그림자 — figmascope는 현재 drop-shadow나 inner-shadow 토큰을 내보내지 않습니다. 그림자 값은 해당 노드의 IR에 인라인으로 나타납니다.
- 그라디언트 — 그라디언트 채우기는 명명된 토큰이 아닌 IR의 leaf 노드에 있는 그대로 내보내집니다.
- 애니메이션 — 타이밍, 이징, 전환 값은 범위 밖입니다. Figma의 애니메이션 지원은 Prototype 모드에 있으며 figmascope는 읽지 않습니다.
- 브레이크포인트 — 반응형 제약은 variable 수준의 Figma 개념이 아닙니다. 별도의 디자인 결정이 필요합니다.
_meta.json의 warnings 배열은 깔끔하게 내보낼 수 없었던 값을 기록합니다. 번들을 에이전트에 전달하기 전에 검토하세요.
토큰 이름 도출 방법
figmascope가 Figma Variables를 읽을 때 Figma에서 variable의 이름이 토큰 키가 됩니다. "Color"라는 컬렉션에서 Colors/Surface/Primary라는 variable은 tokens.json에서 tokens.color.surface.primary가 됩니다 — 경로 구분자는 Figma에서 /이고 JSON 중첩 키에서는 .입니다.
Figma는 공백, 대문자, 특수문자가 있는 variable 이름을 허용합니다. figmascope는 이를 정규화합니다:
- 공백은 하이픈으로 변환:
Primary Surface→primary-surface - 대문자는 소문자로:
AccentOrange→accent-orange - 제어 문자와 Unicode bidi 마크는
sanitizeName으로 제거 - 키릴 문자 및 기타 비 라틴 문자는 슬러그 호환성을 위해 음역
음역 단계는 국제 팀에게 중요합니다. Фон(배경)과 같은 키릴 variable 이름을 사용하는 우크라이나 디자인 팀은 출력에서 안정적인 ASCII 키(fon)를 얻으며, 이는 인코딩 문제 없이 CSS 클래스 이름과 JSON에서 사용 가능합니다. 원본 이름은 Figma Variable 메타데이터에 있는 경우 $description 필드로 보존됩니다.
코드젠 전에 토큰 커버리지 확인
번들을 에이전트에 전달하기 전에 토큰 커버리지가 합리적인지 확인하는 것이 좋습니다. 화면 IR에서 참조된 모든 색상 값이 tokens.json의 항목으로 해석되는지 확인하는 간단한 Node 스크립트:
// scripts/check-token-coverage.js
const fs = require('fs');
const glob = require('glob');
const tokens = JSON.parse(fs.readFileSync('design/tokens.json', 'utf8'));
const screenFiles = glob.sync('design/screens/*.json');
function getRef(obj) {
const refs = [];
if (obj && typeof obj === 'object') {
if (obj.$ref) refs.push(obj.$ref);
for (const v of Object.values(obj)) refs.push(...getRef(v));
}
return refs;
}
function resolveRef(ref, tokens) {
return ref.split('.').reduce((o, k) => o?.[k], tokens);
}
let missing = 0;
for (const file of screenFiles) {
const screen = JSON.parse(fs.readFileSync(file, 'utf8'));
for (const ref of getRef(screen)) {
if (!resolveRef(ref, tokens)) {
console.error(`Missing token: ${ref} (in ${file})`);
missing++;
}
}
}
if (missing === 0) console.log('All token refs resolve.');
else console.error(`${missing} unresolved token refs.`);
누락된 참조가 없으면 에이전트가 레이아웃 IR의 모든 $ref를 값을 만들어내지 않고 해석할 수 있습니다. 누락된 참조가 있다면 보통 번들을 내보낸 후 Figma 파일이 변경되었다는 의미입니다 — figmascope를 다시 실행하여 최신 내보내기를 받으세요. 디자인 반복 전반에 걸친 토큰 커버리지 유지에 대한 더 많은 팁은 figmascope 블로그를 확인하세요.