Token Versioning & Migration
이 문서가 답하는 질문: 토큰 이름을 한 번 정하면 영원히 못 바꾸는가? 바꿔야 한다면 수백 개 소비자를 어떻게 안전하게 옮기는가. 한 줄 답 (Pyramid Top): 토큰 이름 변경은 공개 API의 breaking change다. SemVer로 신호하고,
$deprecated플래그로 유예 기간을 두고, codemod로 자동 마이그레이션한다 — “두 이름이 한동안 공존” 단계를 반드시 거친다.
Why — 토큰 이름은 왜 영원에 가까운 결정인가
토큰은 공개 API다. 일단 export한 토큰은 내가 모르는 사용처를 만든다.
| 사용처 | 영향 범위 |
|---|---|
| 사내 앱 코드 | 추적 가능, codemod 가능 |
| 디자인 도구 (Figma Variables) | 디자이너가 손으로 매핑 |
| 외부 파트너 앱 (B2B 화이트라벨) | 코드 접근 없음 |
| 문서·블로그 코드 예시 | 검색·교체 |
| Storybook 스토리·테스트 | 자동 마이그레이션 가능 |
| 사내 wiki·noting 도구 | 사람이 받아쓰기 |
color.primary를 color.brand로 바꾸는 작은 정리가, 알려지지 않은 100곳을 깰 수 있다.
중요한 관찰: 토큰은 컴포넌트 API보다 훨씬 더 깊이 박힌다.
- 컴포넌트 (
<Button>)를 바꾸면 컴포넌트 사용처만 깨짐 - 토큰 (
color.primary)을 바꾸면 컴포넌트 내부·CSS·Tailwind 클래스·Panda config·테스트·문서 모두 깨짐
How — 안전한 마이그레이션의 4단계
| 단계 | 버전 | 무엇을 하는가 | 사용자 영향 |
|---|---|---|---|
| 1. Add new | v2.5.0 (minor) | 새 alias 추가, 옛 이름 그대로 동작 | 없음 |
| 2. Deprecate old | v2.6.0 (minor) | 옛 토큰에 $deprecated: true, lint 경고 | 경고만 |
| 3. Migrate | 팀별 자유 | codemod로 코드 자동 교체 | 코드 일부 변경 |
| 4. Remove old | v3.0.0 (major) | 옛 이름 삭제 | breaking |
핵심: 1~2 사이에 공존 기간을 둔다. 짧게는 한 sprint, 길게는 두 분기.
What — DTCG $deprecated 플래그
DTCG는 deprecation을 위한 공식 플래그를 정의한다.
{
"color": {
"primary": {
"$value": "{color.brand.default}",
"$type": "color",
"$deprecated": true,
"$description": "Deprecated since v2.6. Use color.brand.default instead."
},
"brand": {
"default": {
"$value": "{color.blue.500}",
"$type": "color",
"$description": "Primary brand color (renamed from color.primary in v2.6)"
}
}
}
}또는 교체 대상을 명시적으로 가리키는 확장:
{
"color": {
"primary": {
"$value": "{color.brand.default}",
"$type": "color",
"$deprecated": true,
"$extensions": {
"co.example.migration": {
"replacedBy": "color.brand.default",
"since": "2.6.0",
"removeIn": "3.0.0"
}
}
}
}
}Style Dictionary의 deprecation 경고
StyleDictionary.registerAction({
name: 'warn/deprecated',
do: (dictionary) => {
dictionary.allTokens
.filter((t) => t.$deprecated)
.forEach((t) => {
const replacedBy = t.$extensions?.['co.example.migration']?.replacedBy
console.warn(`[deprecated] ${t.name} → ${replacedBy ?? '(see $description)'}`)
})
},
})또는 빌드 출력 자체에 경고를 박는 format:
// build/ts/tokens.ts (생성된)
/**
* @deprecated Use ColorBrandDefault instead. Will be removed in v3.0.0.
*/
export const ColorPrimary = "#3b82f6"
export const ColorBrandDefault = "#3b82f6"TypeScript에서 @deprecated JSDoc은 IDE가 취소선으로 표시 — 사용자가 바로 알아챔.
What — SemVer 매트릭스
| 변경 유형 | 예시 | SemVer | 사용자 액션 |
|---|---|---|---|
| 새 토큰 추가 | color.warning 신규 | minor | 없음 |
| 값 변경 (의미 동일) | color.primary가 더 푸르게 | minor | 시각 검토 |
| 값 변경 (의미 다름) | color.primary가 red로 (브랜드 변경) | major | 시각 전체 검토 |
| 이름 추가 (alias) | color.brand가 color.primary 가리킴 | minor | 없음 |
| 이름 deprecation | color.primary에 $deprecated | minor | 경고만 |
| 이름 제거 | color.primary 삭제 | major | codemod |
$type 변경 | color → dimension | major | 사용처 재검토 |
| 그룹 구조 변경 | color.fg.* → text.color.* | major | codemod |
규칙: 제거와 구조 변경은 항상 major. 추가와 deprecation은 minor.
시각 변경의 함정
토큰 값만 살짝 바꿔도 시각적으로는 breaking일 수 있다.
{
"color": {
"primary": {
- "$value": "#3b82f6" // 더 푸른 파랑
+ "$value": "#2563eb" // 더 어두운 파랑
}
}
}코드는 안 깨지지만, 스크린샷 테스트가 전부 실패한다. 시각적 SemVer를 위해:
- 값 변경 시 시각 회귀 테스트 (Chromatic, Percy) 필수
- 큰 색 변경은 major bump 권장 (브랜드 리브랜딩)
- 작은 색 조정은 minor + changelog에 명시
What — Codemod 작성
토큰 이름 일괄 교체는 grep & replace로도 가능하지만, 다음과 같은 컨텍스트를 놓친다:
- TypeScript import 경로
- 문자열 안의 토큰 이름 vs 변수명 우연 일치
- Tailwind 클래스 (
bg-primary→bg-brand) - Panda
css({ color: 'primary' })의 value 위치
jscodeshift 예: TS import 변환
// codemods/rename-color-primary.js
export default function transformer(file, { jscodeshift: j }) {
const root = j(file.source)
// ColorPrimary → ColorBrandDefault (import 이름)
root.find(j.ImportSpecifier, { imported: { name: 'ColorPrimary' } })
.forEach((p) => { p.value.imported.name = 'ColorBrandDefault' })
// 사용처 식별자
root.find(j.Identifier, { name: 'ColorPrimary' })
.forEach((p) => { p.value.name = 'ColorBrandDefault' })
// 문자열 토큰 이름 (Panda css 등)
root.find(j.Literal, { value: 'primary' })
.filter((p) => {
// css({ color: 'primary' }) 패턴에 한정
const parent = p.parent.value
return parent.type === 'Property' && parent.key.name === 'color'
})
.forEach((p) => { p.value.value = 'brand.default' })
return root.toSource()
}실행:
npx jscodeshift -t codemods/rename-color-primary.js src/Tailwind 클래스 교체
# CSS 클래스는 정규식이 안전
rg -l 'bg-primary|text-primary|border-primary' src/ \
| xargs sd 'bg-primary' 'bg-brand-default' \
&& sd 'text-primary' 'text-brand-default' \
&& sd 'border-primary' 'border-brand-default'(sd는 sed의 안전한 대안)
What — 마이그레이션 체크리스트 (실전)
브랜드 색 리네이밍 시나리오 (color.primary → color.brand.default):
-
사전 조사
-
ripgrep 'color\.primary'→ 사용처 개수 파악 -
ripgrep 'bg-primary|text-primary'→ Tailwind 클래스 개수 -
ripgrep "'primary'|\"primary\""→ Panda css value - Figma Variables 사용처 (디자이너 협조)
-
-
DTCG 파일 변경 (v2.6.0)
- 새 토큰 추가:
color.brand.default = {color.blue.500} - 옛 토큰 alias 변경:
color.primary = {color.brand.default}+$deprecated: true - CHANGELOG에 deprecation 명시
- 새 토큰 추가:
-
Style Dictionary 빌드 검증
- 새 CSS variable
--color-brand-default생성 확인 - 옛
--color-primary가 새 변수를 가리킴 확인 - 빌드 경고가 콘솔에 떠야
- 새 CSS variable
-
Codemod 작성·실행
- dry-run으로 변경 미리보기
- 작은 모듈부터 적용
- PR 단위로 분리 (리뷰 가능 크기)
-
시각 회귀 테스트
- Storybook 스크린샷 변화 0
- E2E 스크린샷 변화 0
-
공존 기간 운영 (2~4주)
- 새 이름으로 새 코드 작성
- 주간 진행률 추적 (
rg 'color\.primary' | wc -l감소)
-
제거 (v3.0.0)
- 0건이 되면 옛 토큰 삭제
- major bump + migration guide
- codemod를 npm publish에 동봉 (옛 버전 사용자용)
What-if — 마이그레이션이 깨지는 곳
함정 1: 공존 기간 생략
증상: v2.6에서 바로 옛 토큰 삭제 → 사용자가 빌드 깨짐 호소. 원인: 사용자가 경고를 보고 옮길 시간을 안 줌. 대응: 최소 한 minor 버전은 deprecation 상태로 유지.
함정 2: 변경을 minor로 표기
증상: npm update했더니 빌드 깨짐 — 사용자는 minor라 안전한 줄.
원인: 제거를 minor로 출시.
대응: 제거는 반드시 major. 변경 시점에 automated SemVer linter 도입.
함정 3: codemod가 우연 일치 변환
const variant = 'primary' // 버튼 variant
const color = 'primary' // 토큰 이름증상: 무차별 replace가 버튼 variant까지 바꿈. 원인: 문자열 동등성만 본 정규식. 대응: AST 기반 codemod (jscodeshift, ast-grep). 컨텍스트까지 본다.
함정 4: 시각 회귀 테스트 부재
증상: 값을 살짝 바꿨는데 수개월 후 디자이너가 발견. 원인: snapshot test 없음. 대응: Chromatic/Percy 같은 비주얼 회귀 CI 필수.
함정 5: 외부 사용자 무시
증상: 사내는 codemod로 옮겼지만 외부 파트너가 깨짐 호소. 원인: 토큰 변경의 광역 영향을 과소평가. 대응: deprecation 기간 최소 한 분기. migration guide + codemod 공개.
함정 6: Figma와 불일치
증상: 코드는 새 이름, Figma는 옛 스타일 이름 — 디자이너·개발자가 다른 용어로 같은 색을 부름. 원인: Figma Variables 동기화 누락. 대응: Tokens Studio 같은 양방향 동기화 도구. 매주 sync 검토.
Insight — Hyrum’s Law와 디자인 토큰
Hyrum’s Law: “공개 API의 사용자가 충분히 많아지면, spec에 안 적힌 모든 관찰 가능한 동작에 누군가가 의존한다.”
토큰의 경우 spec에 안 적힌 것까지 의존이 생긴다:
- 토큰 이름 자체 (
color.primary)에 의존 - 토큰 값 자체 (
#3b82f6)에 코드가 의존 — equality check - 토큰 순서 (JSON 안의 토큰 정렬)에 의존
- 토큰 그룹 path에 의존 (자동완성에서 그룹이 보이는 위치)
이 모든 예상치 못한 의존이 마이그레이션을 어렵게 한다. 그래서 디자인 시스템 팀은 **“토큰 이름은 새 신생아 작명만큼 신중하라”**는 농담을 한다.
흥미로운 통찰 하나: Material Design은 v2 → v3에서 거의 모든 토큰 이름을 바꿨다. primary → primary, surface → surface 정도만 살아남고, secondaryContainer, onSurface 같은 수십 개 신규 이름이 등장했다. Google은 이를 major version으로 처리하고, 1년 이상의 공존 기간과 공식 migration guide를 제공했다. 그럼에도 커뮤니티의 불만이 작지 않았다 — 작명의 비용은 그만큼 크다.
**최선의 전략은 “처음에 잘 짓는 것”**이다. semantic 층의 이름을 신중히 결정하고, 컴포넌트 토큰은 자유롭게 바꾸도록 설계하라. 변경 빈도가 높은 곳일수록 안정적 이름을 부여하라 — 이 역설이 토큰 거버넌스의 핵심이다.
요약
- 토큰 이름 변경은 공개 API의 breaking change — SemVer로 신호.
- 4단계 흐름: Add new → Deprecate old → Migrate → Remove.
- DTCG
$deprecated플래그 +$extensions.replacedBy메타로 교체 대상 명시. - codemod는 AST 기반으로. 단순 grep replace는 컨텍스트 놓침.
- 공존 기간 최소 한 minor 버전. 시각 회귀 테스트 필수.
- 처음 작명할 때 신중하라 — semantic 층은 안정, 컴포넌트 층은 유연.