🧩 Design System1. Tokens (DTCG·계층)Style Dictionary Pipeline — DTCG JSON에서 5개 플랫폼 코드 생성하기

Style Dictionary Pipeline

이 문서가 답하는 질문: 한 벌의 DTCG JSON을 어떻게 모든 플랫폼이 동시에 소비하는 코드로 변환하는가? Style Dictionary의 source / parser / transform / format / file은 정확히 무엇을 하는가. 한 줄 답 (Pyramid Top): Style Dictionary는 **“DTCG JSON을 입력으로 받아, 토큰별 transform 체인을 적용한 뒤 format 함수로 직렬화하여 platform별 파일을 emit하는 빌드 파이프라인”**이다.


Why — 왜 손으로 변환하지 않는가

토큰을 정의한 뒤 “각 플랫폼 코드는 사람이 받아쓰면 되지 않나?” 라는 유혹은 흔하다. 실제로 다음과 같이 깨진다.

시나리오손 변환의 비용자동화의 결과
색 50개 + light/dark100개 hex를 .scss, .ts, .swift, .xml에 4번 받아쓰기DTCG 한 번 수정 → 4개 파일 자동 갱신
#3b82f6을 OKLCH로 마이그레이션4번 받아쓰기 + 검증transform 한 줄 추가
다크모드 토큰 추가모든 출력 파일을 다시 손댐source 한 파일만 추가
새 플랫폼 추가 (Flutter)기존 토큰을 Flutter 문법으로 전수 변환platform 한 블록만 config에 추가

핵심은 **“이름은 사람이, 값의 형식 변환은 도구가”**다.


How — Style Dictionary의 5단 파이프라인

단계별 책임

단계역할예시
source어떤 JSON 파일을 읽을지tokens/**/*.json
parserJSON 객체를 token tree로 정규화DTCG $value → SD value
transform토큰 하나에 적용되는 변환들color.blue.500.valuergb(59, 130, 246), name → color-blue-500
formattoken tree 전체를 한 파일의 텍스트로 직렬화CSS variables, TS export, XML 등
file출력 경로·파일명·platform 매핑web/tokens.css, ios/Tokens.swift

What — config.js 실전 예시

config.js (Style Dictionary v4, ESM):

import StyleDictionary from 'style-dictionary'
import { register } from '@tokens-studio/sd-transforms' // DTCG 호환 transform set
 
register(StyleDictionary)
 
export default {
  source: ['tokens/**/*.json'],
  preprocessors: ['tokens-studio'],
 
  platforms: {
    css: {
      transformGroup: 'tokens-studio',
      transforms: ['name/kebab'],
      buildPath: 'build/web/',
      files: [
        {
          destination: 'tokens.css',
          format: 'css/variables',
          options: { outputReferences: true }, // var(--color-blue-500) 유지
        },
      ],
    },
 
    cssDark: {
      transformGroup: 'tokens-studio',
      transforms: ['name/kebab'],
      buildPath: 'build/web/',
      files: [
        {
          destination: 'tokens.dark.css',
          format: 'css/variables',
          filter: (t) => t.path[0] === 'color' && t.filePath.includes('dark'),
          options: { selector: '[data-theme="dark"]', outputReferences: true },
        },
      ],
    },
 
    ts: {
      transformGroup: 'tokens-studio',
      transforms: ['name/camel'],
      buildPath: 'build/ts/',
      files: [
        {
          destination: 'tokens.ts',
          format: 'javascript/es6',
        },
        {
          destination: 'tokens.d.ts',
          format: 'typescript/es6-declarations',
        },
      ],
    },
 
    tailwind: {
      transformGroup: 'tokens-studio',
      transforms: ['name/kebab'],
      buildPath: 'build/tailwind/',
      files: [
        {
          destination: 'preset.cjs',
          format: 'custom/tailwind-preset', // 사용자 정의 format (아래)
        },
      ],
    },
 
    panda: {
      transformGroup: 'tokens-studio',
      buildPath: 'build/panda/',
      files: [
        {
          destination: 'tokens.ts',
          format: 'custom/panda-tokens',
        },
      ],
    },
 
    ios: {
      transformGroup: 'ios-swift',
      buildPath: 'build/ios/',
      files: [
        {
          destination: 'Tokens.swift',
          format: 'ios-swift/class.swift',
          options: { className: 'Tokens' },
        },
      ],
    },
 
    android: {
      transformGroup: 'android',
      buildPath: 'build/android/',
      files: [
        {
          destination: 'colors.xml',
          format: 'android/colors',
          filter: { attributes: { category: 'color' } },
        },
        {
          destination: 'dimens.xml',
          format: 'android/dimens',
          filter: { attributes: { category: 'size' } },
        },
      ],
    },
  },
}

빌드 실행:

npx style-dictionary build --config config.js

What — 각 플랫폼의 출력 예시

입력 — tokens/color.json

{
  "color": {
    "$type": "color",
    "blue": {
      "500": { "$value": "#3b82f6" }
    },
    "primary": {
      "$value": "{color.blue.500}",
      "$description": "Primary brand color"
    }
  }
}

출력 1 — build/web/tokens.css

/**
 * Do not edit directly.
 * Generated on Tue, 19 May 2026 12:00:00 GMT
 */
:root {
  --color-blue-500: #3b82f6;
  --color-primary: var(--color-blue-500);
}

outputReferences: true 덕에 --color-primaryraw hex가 아닌 var(--color-blue-500)을 가리킨다 — 런타임 토글 가능.

출력 2 — build/ts/tokens.ts

/**
 * Do not edit directly.
 */
export const ColorBlue500 = "#3b82f6"
export const ColorPrimary = "#3b82f6"
 
export const tokens = {
  color: {
    blue: { 500: ColorBlue500 },
    primary: ColorPrimary,
  },
} as const

출력 3 — build/tailwind/preset.cjs (사용자 정의 format)

먼저 format 정의:

StyleDictionary.registerFormat({
  name: 'custom/tailwind-preset',
  format: ({ dictionary }) => {
    const colors = {}
    dictionary.allTokens
      .filter((t) => t.path[0] === 'color')
      .forEach((t) => {
        const [, ...rest] = t.path
        // color.blue.500 → colors.blue.500 = "var(--color-blue-500)"
        let cursor = colors
        rest.slice(0, -1).forEach((seg) => {
          cursor[seg] = cursor[seg] ?? {}
          cursor = cursor[seg]
        })
        cursor[rest.at(-1)] = `var(--${t.name})`
      })
    return `module.exports = ${JSON.stringify({ theme: { extend: { colors } } }, null, 2)}`
  },
})

출력:

module.exports = {
  "theme": {
    "extend": {
      "colors": {
        "blue": { "500": "var(--color-blue-500)" },
        "primary": "var(--color-primary)"
      }
    }
  }
}

사용:

// tailwind.config.ts
import preset from './build/tailwind/preset.cjs'
export default { presets: [preset] }

출력 4 — build/panda/tokens.ts

export const tokens = {
  colors: {
    blue: {
      500: { value: '#3b82f6' },
    },
  },
}
export const semanticTokens = {
  colors: {
    primary: {
      value: { base: '{colors.blue.500}', _dark: '{colors.blue.400}' },
    },
  },
}

사용:

// panda.config.ts
import { tokens, semanticTokens } from './build/panda/tokens'
export default defineConfig({
  theme: { tokens, semanticTokens },
})

출력 5 — build/ios/Tokens.swift

import UIKit
 
public class Tokens {
    public static let colorBlue500 = UIColor(red: 0.231, green: 0.510, blue: 0.965, alpha: 1.0)
    public static let colorPrimary = UIColor(red: 0.231, green: 0.510, blue: 0.965, alpha: 1.0)
}

출력 6 — build/android/colors.xml

<?xml version="1.0" encoding="UTF-8"?>
<resources>
  <color name="color_blue_500">#FF3B82F6</color>
  <color name="color_primary">#FF3B82F6</color>
</resources>

What — Transform의 작동 원리

transform토큰 하나에 적용되는 함수다. 4가지 종류:

종류역할예시
attribute메타데이터 추가 (token.attributes.category)category 추론
name토큰 이름 변환color.blue.500color-blue-500 (kebab)
value값 변환#3b82f6rgb(59, 130, 246)
transitivealias 풀린 적용최종 값에 색 보정

내장 transform 예시

이름역할
name/kebabcolor.blue.500color-blue-500
name/camelcolorBlue500
name/snakecolor_blue_500
color/hexRGB 객체 → #rrggbb
color/UIColorhex → UIColor(red:..., green:..., blue:..., alpha:...)
color/csshex → #rrggbb 또는 rgba(...)
size/rem16px1rem
size/px1rem16px

사용자 정의 transform 예

hex → OKLCH 변환 (OKLCH 도입 마이그레이션 시):

import { converter, formatCss } from 'culori'
const toOklch = converter('oklch')
 
StyleDictionary.registerTransform({
  name: 'color/oklch',
  type: 'value',
  filter: (token) => token.$type === 'color' || token.attributes?.category === 'color',
  transform: (token) => formatCss(toOklch(token.value)),
})

transforms: ['color/oklch']를 platform에 추가하면 모든 색이 OKLCH로 출력.


What — 다크모드를 한 빌드로 처리하기

tokens/color.semantic.light.json + tokens/color.semantic.dark.json 두 파일을 두고 selector 옵션으로 분리:

platforms: {
  cssLight: {
    transformGroup: 'tokens-studio',
    transforms: ['name/kebab'],
    buildPath: 'build/web/',
    files: [{
      destination: 'tokens.light.css',
      format: 'css/variables',
      filter: (t) => !t.filePath.includes('dark'),
      options: { selector: ':root, [data-theme="light"]', outputReferences: true },
    }],
  },
  cssDark: {
    transformGroup: 'tokens-studio',
    transforms: ['name/kebab'],
    buildPath: 'build/web/',
    files: [{
      destination: 'tokens.dark.css',
      format: 'css/variables',
      filter: (t) => t.filePath.includes('dark'),
      options: { selector: '[data-theme="dark"]', outputReferences: true },
    }],
  },
}

런타임:

<link rel="stylesheet" href="/build/web/tokens.light.css">
<link rel="stylesheet" href="/build/web/tokens.dark.css">
<html data-theme="light">  {/* JS로 토글 */}

또는 한 파일에 두 selector를 합치는 themes plugin도 있음 (Style Dictionary v4 themes API).


What-if — 파이프라인이 깨지는 곳

함정 1: $type 누락으로 transform 미적용

color/hex transform은 attributes.category === 'color'인 토큰에만 적용된다. $type을 누락하면 filter에서 빠져 raw 값이 그대로 출력. 대응: 그룹 레벨에 $type 한 번. CI에서 $type 누락 검출.

함정 2: alias가 풀리지 않은 채 출력

outputReferences: false(기본값)면 --color-primary: #3b82f6로 hex가 박힌다. 런타임 다크모드 토글 실패. 대응: outputReferences: true. 단, 일부 format(특히 iOS/Android)은 지원 안 함 — 그 플랫폼은 alias 풀어서 박는 게 맞음.

함정 3: 한 plaform에 두 transformGroup

transformGroup: 'css' + transforms: ['color/oklch']를 동시에 쓰면 두 transform이 순서대로 적용. 어느 게 먼저인지 명시 안 하면 결과 달라짐. 대응: transformGroup 또는 transforms 둘 중 하나만. 명시적 transform 배열 권장.

함정 4: 파일 분할 시 reference 못 찾음

tokens/colors.json에서 tokens/components.json의 토큰을 alias로 참조하는데, source glob이 components를 안 잡으면 Reference doesn't exist. 대응: source glob을 항상 모든 토큰을 포함하게. 빌드 단계에서 전체를 한 dictionary로 합쳐서 처리.

함정 5: 출력 파일이 git에 체크인됨

build/ 폴더가 git에 들어가면 PR마다 자동 생성 파일 diff로 리뷰가 어려워짐. 대응: .gitignorebuild/ 추가. CI에서 빌드 후 artifact로 배포 또는 npm publish 시점에만 생성.

함정 6: Style Dictionary v3 → v4 마이그레이션

v3는 value(no $), v4는 $value 우선. 둘이 섞이면 어떤 토큰은 인식, 어떤 토큰은 무시. 대응: 마이그레이션 codemod 실행. @tokens-studio/sd-transforms의 preprocessor가 자동 변환.


Insight — Style Dictionary가 사실상 표준이 된 이유

2016년 Amazon이 내부 도구로 만든 Style Dictionary는, 2024년 현재 모든 주요 디자인 시스템의 빌드 백본이다. Salesforce, Adobe Spectrum, IBM Carbon, Material 3 — 거의 모두가 쓴다. 왜?

철학적으로:

  • 플랫폼-중립 입력 + 플러그블 출력 — Unix 철학 (tokens.json | sd build > anywhere)
  • transform 단위가 작음 — 사용자가 새 변환을 한 함수로 추가
  • format은 그저 문자열 함수 — 어떤 출력 형식도 지원 가능

현실적으로:

  • 표준 출력 형식이 수십 개 내장 (CSS, SCSS, Less, JS, TS, Swift, Objective-C, Java, Kotlin, Android XML, Compose, Flutter, JSON, JSON flat…)
  • 활발한 커뮤니티 transform — OKLCH, Tailwind preset, Panda tokens, Tokens Studio 호환
  • v4부터 DTCG 표준 1급 지원$value·$type·alias를 native로 인식

경쟁자들의 운명:

  • Theo (Salesforce): 자기 회사가 Style Dictionary로 갈아탐
  • diez (Haiku Animator): 개발 중단
  • Cosmos: 작은 niche
  • Tokens Studio Build: Style Dictionary를 runtime으로 wrap

이런 흡수의 흐름은 표준화가 만드는 자연스러운 수렴이다. DTCG가 포맷 표준이라면, Style Dictionary는 그 포맷을 다루는 사실상의 reference implementation이다.

흥미로운 사실 하나: Style Dictionary의 이름은 농담 같다. “스타일 사전” — 너무 평범해서 검색이 안 된다는 푸념이 GitHub issue로 남아 있다. 하지만 이름이 평범해서 어떤 분야의 누가 봐도 의미를 안다는 것이, 표준이 되기 위한 흔하지 않은 조건이다.


요약

  • Style Dictionary는 source → parser → transform → format → file의 5단 파이프라인.
  • DTCG JSON 한 벌이 CSS variables / TS / Tailwind preset / Panda config / iOS Swift / Android XML로 동시 emit.
  • v4부터 DTCG 1급 지원 — $value·$type·alias 자동 인식.
  • 사용자 정의 transform·format으로 어떤 출력도 가능 (OKLCH, Flutter, SwiftUI…).
  • 빌드 산출물은 git에 두지 말고 npm publish 또는 CI artifact로.