🧩 Design System1. Tokens (DTCG·계층)Token Versioning & Migration — 이름 변경의 비용·SemVer·codemod

Token Versioning & Migration

이 문서가 답하는 질문: 토큰 이름을 한 번 정하면 영원히 못 바꾸는가? 바꿔야 한다면 수백 개 소비자를 어떻게 안전하게 옮기는가. 한 줄 답 (Pyramid Top): 토큰 이름 변경은 공개 API의 breaking change다. SemVer로 신호하고, $deprecated 플래그로 유예 기간을 두고, codemod로 자동 마이그레이션한다 — “두 이름이 한동안 공존” 단계를 반드시 거친다.


Why — 토큰 이름은 왜 영원에 가까운 결정인가

토큰은 공개 API다. 일단 export한 토큰은 내가 모르는 사용처를 만든다.

사용처영향 범위
사내 앱 코드추적 가능, codemod 가능
디자인 도구 (Figma Variables)디자이너가 손으로 매핑
외부 파트너 앱 (B2B 화이트라벨)코드 접근 없음
문서·블로그 코드 예시검색·교체
Storybook 스토리·테스트자동 마이그레이션 가능
사내 wiki·noting 도구사람이 받아쓰기

color.primarycolor.brand로 바꾸는 작은 정리가, 알려지지 않은 100곳을 깰 수 있다.

중요한 관찰: 토큰은 컴포넌트 API보다 훨씬 더 깊이 박힌다.

  • 컴포넌트 (<Button>)를 바꾸면 컴포넌트 사용처만 깨짐
  • 토큰 (color.primary)을 바꾸면 컴포넌트 내부·CSS·Tailwind 클래스·Panda config·테스트·문서 모두 깨짐

How — 안전한 마이그레이션의 4단계

단계버전무엇을 하는가사용자 영향
1. Add newv2.5.0 (minor)새 alias 추가, 옛 이름 그대로 동작없음
2. Deprecate oldv2.6.0 (minor)옛 토큰에 $deprecated: true, lint 경고경고만
3. Migrate팀별 자유codemod로 코드 자동 교체코드 일부 변경
4. Remove oldv3.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.brandcolor.primary 가리킴minor없음
이름 deprecationcolor.primary$deprecatedminor경고만
이름 제거color.primary 삭제majorcodemod
$type 변경color → dimensionmajor사용처 재검토
그룹 구조 변경color.fg.*text.color.*majorcodemod

규칙: 제거구조 변경은 항상 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-primarybg-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'

(sdsed의 안전한 대안)


What — 마이그레이션 체크리스트 (실전)

브랜드 색 리네이밍 시나리오 (color.primarycolor.brand.default):

  1. 사전 조사

    • ripgrep 'color\.primary' → 사용처 개수 파악
    • ripgrep 'bg-primary|text-primary' → Tailwind 클래스 개수
    • ripgrep "'primary'|\"primary\"" → Panda css value
    • Figma Variables 사용처 (디자이너 협조)
  2. DTCG 파일 변경 (v2.6.0)

    • 새 토큰 추가: color.brand.default = {color.blue.500}
    • 옛 토큰 alias 변경: color.primary = {color.brand.default} + $deprecated: true
    • CHANGELOG에 deprecation 명시
  3. Style Dictionary 빌드 검증

    • 새 CSS variable --color-brand-default 생성 확인
    • --color-primary새 변수를 가리킴 확인
    • 빌드 경고가 콘솔에 떠야
  4. Codemod 작성·실행

    • dry-run으로 변경 미리보기
    • 작은 모듈부터 적용
    • PR 단위로 분리 (리뷰 가능 크기)
  5. 시각 회귀 테스트

    • Storybook 스크린샷 변화 0
    • E2E 스크린샷 변화 0
  6. 공존 기간 운영 (2~4주)

    • 새 이름으로 새 코드 작성
    • 주간 진행률 추적 (rg 'color\.primary' | wc -l 감소)
  7. 제거 (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에서 거의 모든 토큰 이름을 바꿨다. primaryprimary, surfacesurface 정도만 살아남고, 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 층은 안정, 컴포넌트 층은 유연.