Style Dictionary v4 × GitHub Actions: 파일 복제 없이 다크 모드·멀티브랜드 디자인 토큰 자동 배포하기
디자인 시스템을 운영하다 보면 어느 순간 깨닫게 됩니다. 브랜드가 둘로 늘어나고 다크 모드 요구가 생기자마자, 거의 동일한 토큰 파일이 4개로 복제되기 시작한다는 사실을. 값 하나를 바꾸면 4개 파일을 모두 수정해야 하고, 누군가 하나를 빠뜨리면 프로덕션에서 색상 불일치가 발생합니다. 이것은 파일 관리 습관의 문제가 아니라 아키텍처의 문제입니다.
Style Dictionary v4는 기존 v3의 CTI(Category/Type/Item) 구조 의존성을 완전히 걷어내고 ESM 기반으로 재작성된 버전으로, $extends 그룹 상속이라는 강력한 기능을 독자적으로 지원합니다. 이 기능을 활용하면 브랜드 5개·모드 3개 환경에서 테마별로 수정해야 하는 파일이 15개에서 1개로 줄어듭니다. 여기에 GitHub Actions 파이프라인을 결합하면, 디자이너가 토큰 파일을 수정하는 순간 빌드와 npm 배포까지 자동으로 완성됩니다.
이 글에서는 $extends 기반 계층 설계부터 멀티브랜드 빌드 매트릭스, 그리고 GitHub Actions 자동 배포 파이프라인까지 하나의 실전 아키텍처로 연결하는 방법을 살펴봅니다. 완성된 파이프라인 코드와 복사해서 바로 쓸 수 있는 디렉터리 구조를 함께 다룹니다.
핵심 개념
Style Dictionary v4와 DTCG 스펙
Style Dictionary는 Amazon이 만든 디자인 토큰 빌드 시스템입니다. JSON 또는 JS 형태의 토큰 파일을 입력받아 CSS 변수, iOS Swift, Android XML 등 다양한 플랫폼 산출물로 변환해 줍니다. 2024년 Q2에 정식 출시된 v4는 ESM 기반으로 완전히 재작성되었으며, token.$type으로 토큰 타입을 결정하는 방식으로 전환해 DTCG 스펙과의 정렬을 완성했습니다.
DTCG(Design Tokens Community Group): W3C 산하 커뮤니티 그룹으로, 디자인 토큰의 교환 형식을 표준화합니다. 2025년 10월 첫 번째 안정 버전(Format Module 1.0)을 공개했으며, Style Dictionary·Tokens Studio·Terrazzo가 레퍼런스 구현체로 명시되었습니다.
$extends 그룹 상속의 동작 원리
한 가지 중요한 사실을 먼저 짚어 두겠습니다. $extends는 현재 DTCG 공식 스펙에 포함된 기능이 아니라 Style Dictionary v4가 독자적으로 지원하는 확장 기능입니다. DTCG 표준에서는 토큰 파일 간 상속을 source 배열 합성으로 처리하며, $extends 키워드는 Style Dictionary v4의 고유 문법입니다. 이 점을 팀 내에서 명확히 인식하고 사용해야 합니다.
$extends는 한 토큰 그룹이 다른 그룹을 상속받아 토큰과 속성을 재사용하되, 일부만 오버라이드할 수 있는 기능입니다. 깊은 병합(deep merge) 방식으로 동작하며, 같은 경로의 로컬 속성이 상속된 속성을 덮어씁니다.
// tokens/themes/dark.json
{
"semantic": {
"$extends": "./light.json",
"$type": "color",
"background": { "$value": "{color.gray-900}" },
"text-primary": { "$value": "{color.gray-50}" }
}
}경로 해석 주의:
"$extends": "./light.json"에서 상대 경로는 파일이 위치한 디렉터리 기준으로 해석됩니다. 빌드 명령을 실행하는 CWD(현재 작업 디렉터리) 기준이 아니므로, 파일을 옮길 때 경로도 함께 수정해야 합니다.
주의:
$extends는 순환 참조를 허용하지 않습니다. A가 B를 상속하고 B가 다시 A를 참조하는 구조는 빌드 오류를 발생시킵니다. 상속 관계는 반드시 단방향으로 설계해야 합니다.
3계층 토큰 아키텍처
효과적인 토큰 설계는 일반적으로 세 계층으로 구성됩니다. 계층 간 참조는 글로벌 → 시맨틱 → 컴포넌트 방향으로만 허용됩니다. 역방향으로 참조하면 Style Dictionary가 빌드 시점에 순환 참조 오류를 발생시킵니다.
| 계층 | 이름 | 역할 | 예시 |
|---|---|---|---|
| 1 | 글로벌(Primitive) | 원시 값 정의 | blue-500: #3B82F6 |
| 2 | 시맨틱(Alias) | 의미 기반 참조 | color-background-primary: {color.blue-500} |
| 3 | 컴포넌트 | UI 컴포넌트 전용 | button-bg: {semantic.color-background-primary} |
$extends는 주로 2계층(시맨틱) 수준에서 라이트/다크 분기를 처리하는 데 활용됩니다. 1계층은 모든 테마가 공유하고, 2계층에서 테마별로 참조 대상만 교체하는 구조입니다.
$extendsvssource딥 머지: Style Dictionary v4에서는 두 방식이 공존합니다.$extends는 토큰 파일 내 그룹 수준 상속이고,source배열 딥 머지는 파일 수준 합성입니다. 팀 내에서 어느 레이어에 어느 방식을 쓸지 명확한 규약을 정해두는 것을 권장합니다.
실전 적용
개념을 이해했으니 이제 실제 파일 구조부터 설계해 보겠습니다. 아래는 이 글에서 다루는 전체 디렉터리 구조입니다.
design-tokens/
├── tokens/
│ ├── base/
│ │ └── colors.json # 글로벌(Primitive) 원시 토큰
│ ├── themes/
│ │ ├── light.json # 라이트 시맨틱 토큰
│ │ └── dark.json # 다크 시맨틱 토큰 ($extends 사용)
│ └── brands/
│ ├── brand-a.json # 브랜드 A 오버라이드
│ └── brand-b.json # 브랜드 B 오버라이드
├── dist/ # 빌드 산출물 (.gitignore에 추가)
│ ├── brand-a/light/variables.css
│ ├── brand-a/dark/variables.css
│ └── ...
├── style-dictionary.config.mjs # 빌드 설정
├── .github/workflows/tokens.yml # CI/CD 파이프라인
└── package.json예시 1: 라이트/다크 모드 토큰 구조 설계
먼저 공통 원시 토큰 파일을 정의합니다.
// tokens/base/colors.json
{
"color": {
"$type": "color",
"blue-500": { "$value": "#3B82F6" },
"gray-50": { "$value": "#F9FAFB" },
"gray-900": { "$value": "#111827" }
}
}라이트 테마는 시맨틱 레이어에서 원시 토큰을 참조합니다.
// tokens/themes/light.json
{
"semantic": {
"$type": "color",
"background": { "$value": "{color.gray-50}" },
"text-primary": { "$value": "{color.gray-900}" },
"brand": { "$value": "{color.blue-500}" }
}
}다크 테마는 $extends로 라이트를 상속받고, 변경되는 값만 선언합니다.
// tokens/themes/dark.json
{
"semantic": {
"$extends": "./light.json",
"$type": "color",
"background": { "$value": "{color.gray-900}" },
"text-primary": { "$value": "{color.gray-50}" }
}
}| 토큰 | 라이트 | 다크 | 관리 방식 |
|---|---|---|---|
background |
gray-50 |
gray-900 |
dark.json에서 오버라이드 |
text-primary |
gray-900 |
gray-50 |
dark.json에서 오버라이드 |
brand |
blue-500 |
blue-500 |
$extends 상속으로 자동 공유 |
예시 2: 멀티브랜드 빌드 매트릭스 구성
브랜드별 파일은 글로벌 토큰의 일부만 오버라이드합니다.
// tokens/brands/brand-a.json
{
"color": {
"$type": "color",
"blue-500": { "$value": "#6366F1" }
}
}Style Dictionary v4의 ESM 설정 파일에서 브랜드와 모드를 조합해 전체 산출물을 생성합니다.
// style-dictionary.config.mjs
import StyleDictionary from 'style-dictionary';
const brands = ['brand-a', 'brand-b'];
const modes = ['light', 'dark'];
// 병렬 빌드: 브랜드·모드 수가 늘어날 경우 Promise.all()로 처리 시간을 단축할 수 있습니다
const buildTasks = brands.flatMap(brand =>
modes.map(async mode => {
const sd = new StyleDictionary({
source: [
'tokens/base/**/*.json',
`tokens/brands/${brand}.json`,
`tokens/themes/${mode}.json`,
],
platforms: {
css: {
transformGroup: 'css',
buildPath: `dist/${brand}/${mode}/`,
files: [{
destination: 'variables.css',
format: 'css/variables',
}],
},
},
});
await sd.buildAllPlatforms();
})
);
await Promise.all(buildTasks);파일 4개(base, brand-*, light, dark)로 2 브랜드 × 2 모드 = 4개의 독립적인 CSS 산출물이 생성됩니다. 브랜드가 5개로 늘어나도 추가할 파일은 브랜드 파일 하나뿐입니다.
예시 3: GitHub Actions 자동 배포 파이프라인
토큰 파일이 변경될 때만 파이프라인이 실행되도록 paths 필터를 적용하고, PR과 main 브랜치를 각각 다르게 처리합니다.
# .github/workflows/tokens.yml
name: Build & Publish Design Tokens
on:
push:
branches: [main]
paths: ['tokens/**']
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- run: pnpm install --frozen-lockfile
# JSON 구조 및 토큰 참조 유효성 검사
# tokens:lint는 token-validator 또는 팀 커스텀 스크립트로 구성할 수 있습니다
- name: Lint tokens
run: pnpm run tokens:lint
- name: Build tokens
run: pnpm run tokens:build
# PR에서는 결과물을 아티팩트로 업로드해 리뷰 가능하게
- name: Upload artifacts
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: token-dist
path: dist/
# main 병합 시에만 npm 배포
# 버전 관리는 Changesets 또는 semantic-release 도입을 권장합니다
- name: Publish to npm
if: github.ref == 'refs/heads/main'
run: pnpm publish --no-git-checks
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}| 단계 | 트리거 | 역할 |
|---|---|---|
| Lint tokens | PR + Push | JSON 구조·참조 유효성 검사 |
| Build tokens | PR + Push | 전체 브랜드×모드 빌드 |
| Upload artifacts | PR only | dist/ diff를 리뷰어가 확인 |
| Publish to npm | main push only | npm 패키지 배포 |
버전 관리 팁:
pnpm publish --no-git-checks는 버전 태그 없이 배포하는 구조입니다. 실제 파이프라인에서는 Changesets 또는 semantic-release를 함께 도입해 버전 범프와 CHANGELOG 자동화까지 연결하는 것을 권장합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 단일 소스 | 브랜드·모드 조합 수와 관계없이 원시 토큰 파일은 하나로 유지됩니다 |
| 타입 안전 | $type 상속으로 토큰 타입 불일치를 빌드 시점에 조기 감지할 수 있습니다 |
| 플랫폼 독립 | CSS·Swift·XML·JS를 동시 생성해 플랫폼 팀별 수동 작업을 없앨 수 있습니다 |
| 완전 자동화 | 토큰 변경 → 빌드 → npm 배포까지 사람의 개입 없이 완료됩니다 |
| DTCG 호환 | 2025년 10월 안정화된 스펙 기반으로 Figma·Tokens Studio 등 도구 간 상호운용성이 확보됩니다 |
단점 및 주의사항
| 항목 | 설명 | 대응 방안 |
|---|---|---|
| 빌드 매트릭스 폭발 | 브랜드 5개 × 모드 3개 × 플랫폼 4개 = 빌드 60회 | Promise.all() 병렬 처리 + Actions 캐시 |
| npm 토큰 보안 | 2025년 12월부로 클래식 토큰이 비활성화됨 — 기존 파이프라인이 있다면 즉시 마이그레이션 필요 | Granular Access Token 또는 Trusted Publishing으로 전환 |
| Figma 동기화 타이밍 | Figma 변경만으로는 파이프라인 트리거 불가 | Tokens Studio 양방향 동기화 플러그인 병행 사용 |
| dist/ git 노이즈 | PR마다 빌드 산출물이 변경돼 리뷰 품질 저하 | .gitignore에 dist/ 추가 후 CI 아티팩트로만 관리 |
| 순환 참조 불가 | $extends 상호 참조 구조 불허 |
상속 관계를 반드시 단방향으로 설계 |
| 버전 관리 공백 | --no-git-checks 단독 사용 시 버전 추적 불가 |
Changesets 또는 semantic-release 도입 |
실무에서 가장 흔한 실수
- 원시 토큰에 직접
$extends적용: 글로벌(Primitive) 계층은 모든 테마가 공유하는 불변 값입니다. 상속은 시맨틱 계층에서만 활용하는 것이 안전합니다. dist/를 git에 커밋: 빌드 산출물을 저장소에 포함하면 PR 리뷰가 어려워지고 충돌이 잦아집니다. CI 아티팩트와 npm 패키지로만 배포하는 구조를 권장합니다.- npm 클래식 토큰 그대로 사용: 2025년 12월 이후 클래식 토큰은 이미 비활성화된 상태입니다. 기존 파이프라인이 있다면 Granular Access Token으로의 교체가 필요합니다.
마치며
$extends 그룹 상속과 GitHub Actions 파이프라인을 결합하면, 파일 복제 없이 브랜드·모드 조합을 무한히 확장하면서도 단 하나의 소스에서 모든 플랫폼 산출물을 자동 배포하는 토큰 아키텍처를 구축할 수 있습니다.
지금 바로 시작해볼 수 있는 3단계:
- 3계층 디렉터리 구조를 먼저 잡아보세요.
tokens/base/,tokens/themes/,tokens/brands/를 만들고pnpm add -D style-dictionary@^4로 v4를 설치한 뒤, 기존 토큰 파일을 계층에 맞게 분리해 보는 것으로 시작할 수 있습니다. - 다크 테마 파일 하나를
$extends로 선언해 보세요.tokens/themes/dark.json에"$extends": "./light.json"을 추가하고 바뀌는 값 2~3개만 오버라이드한 뒤,buildAllPlatforms()를 호출해 라이트/다크 CSS가 각각dist/에 생성되는 것을 확인할 수 있습니다. .github/workflows/tokens.yml을 추가해 파이프라인을 연결해 보세요.paths: ['tokens/**']필터로 토큰 변경 시에만 빌드가 실행되도록 설정하면, PR에서 아티팩트로 diff를 확인하고 main 병합 시에만 npm에 배포되는 전체 흐름이 완성됩니다.
참고 자료
- Style Dictionary v4 공식 문서 | styledictionary.com
- Style Dictionary v4 마이그레이션 가이드 | styledictionary.com
- DTCG 유틸리티 — Style Dictionary v4 | v4.styledictionary.com
- Design Tokens Format Module 2025.10 | W3C DTCG
- W3C DTCG 스펙 안정화 발표 | w3.org
- How to run Style Dictionary with a GitHub Action | Specify
- Implementing Light and Dark Mode with Style Dictionary | Always Twisted
- Implementing Multi-Brand Theming with Style Dictionary | Always Twisted
- Dark Mode with Style Dictionary | dbanks.design
- Creating a design tokens automation pipeline with Figma and Style Dictionary | Medium
- Syncing Figma Variables and Style Dictionary with GitHub Actions | James Ives
- Style Dictionary v4 Release Plans | Tokens Studio
- Tokens Studio sd-transforms | GitHub
- Style Dictionary | GitHub
다음 글: Figma Variables REST API와 Tokens Studio
sd-transforms를 연결해 디자이너의 Figma 변경이 자동으로 Git PR을 생성하고 토큰 파이프라인을 트리거하는 End-to-End 동기화 구축 방법