04 — 토큰 이름 변경 deprecation: alias·warn·codemod
이 문서가 답하는 질문:
color.brand를color.primary로 바꾸려고 하는데, 그 이름을 200곳에서 쓰고 있다면 어떻게 깨뜨리지 않고 옮기는가. 한 줄 답 (Pyramid Top): 토큰 이름 변경은 4-스텝 deprecation 사이클 —(1) 새 이름 추가 + 기존을 alias로 유지 (2) 기존 이름 사용 시 console.warn (3) codemod 동봉 (4) 다음 메이저에서 기존 이름 제거— 으로 사용자를 최소 하나의 메이저 버전 동안 양립시킨다.
Why — 토큰 이름은 공개 API다
디자인 시스템에서 가장 비싼 변경은 컴포넌트 prop이 아니라 토큰 이름이다.
| 토큰 이름의 특성 | 함의 |
|---|---|
Tailwind class에 직접 박힘 (bg-brand-500) | grep 가능하지만 수백 곳 |
Panda recipe에 직접 박힘 (bg: 'brand.500') | 타입 안전 → 깨지면 컴파일 에러 |
CSS 변수에 박힘 (var(--color-brand)) | 동적 — 런타임에 조용히 undefined |
| Figma 변수에 박힘 | 디자이너 작업물도 같이 마이그레이션 필요 |
토큰 이름은 공개 API의 일부다 — 변경은 메이저 버전에서만 허용되며, 그 메이저에 도달하기 전 충분한 deprecation이 필요하다.
| 풀려는 문제 | 이전 해법 | 한계 |
|---|---|---|
| 토큰 rename | 메이저 bump + breaking note | 사용자가 200곳을 수동 수정 |
| ”어디서 쓰는지 모름” | grep | Panda recipe·CSS var·Figma는 grep 어려움 |
| ”디자이너도 같이 옮겨야” | 슬랙 공지 | 누락·시점 어긋남 |
| 점진적 마이그레이션 | — | 4-스텝 deprecation 사이클 |
How — 4-스텝 deprecation 사이클
각 단계가 최소 1개 minor 버전에 머문다 — 사용자에게 옮길 시간을 주는 것.
What — 각 단계의 구체 코드
Step 1: 새 이름 + alias
packages/tokens/src/semantic/color.json:
{
"color": {
"primary": {
"$value": "{color.blue.500}",
"$type": "color",
"$description": "Primary action color (Buttons, links, focus rings)"
},
"brand": {
"$value": "{color.primary}",
"$type": "color",
"$description": "@deprecated Use color.primary instead. Will be removed in v2.0.",
"$extensions": {
"com.org.deprecated": {
"since": "1.5.0",
"replacement": "color.primary",
"removeIn": "2.0.0"
}
}
}
}
}DTCG $extensions에 deprecation 메타를 박는다 — Style Dictionary가 이를 읽어 자동 처리할 수 있다.
Step 2: console.warn
Panda preset에서 deprecated 토큰 사용 시 dev mode에서 경고:
// packages/panda-preset/src/index.ts
import { definePreset } from '@pandacss/dev';
import tokens from '@org/tokens/panda';
import deprecated from '@org/tokens/deprecated.json';
export default definePreset({
name: '@org/panda-preset',
theme: {
extend: {
tokens: tokens.tokens,
semanticTokens: {
colors: {
...tokens.semanticTokens.colors,
// Deprecated alias — warn on read
...(process.env.NODE_ENV !== 'production'
? wrapDeprecated(deprecated)
: deprecated),
},
},
},
},
});
function wrapDeprecated(map: Record<string, any>) {
const out: Record<string, any> = {};
for (const [name, value] of Object.entries(map)) {
out[name] = {
...value,
// Panda extension that hooks read
$extensions: {
'com.org.warn': () =>
console.warn(
`[@org/tokens] color.${name} is deprecated. ` +
`Use color.${value.$extensions['com.org.deprecated'].replacement} instead. ` +
`Will be removed in ${value.$extensions['com.org.deprecated'].removeIn}.`
),
},
};
}
return out;
}Tailwind preset 측은 빌드 시점에 경고:
// packages/tailwind-preset/src/index.ts
import type { Config } from 'tailwindcss';
import tokens from '@org/tokens/tailwind';
const deprecatedColors = new Set(['brand']);
export default {
theme: {
extend: {
colors: new Proxy(tokens.colors, {
get(target, prop: string) {
if (deprecatedColors.has(prop) && process.env.NODE_ENV !== 'production') {
console.warn(
`[@org/tailwind-preset] colors.${prop} is deprecated. ` +
`Use colors.primary instead.`
);
}
return target[prop];
},
}),
},
},
} satisfies Partial<Config>;ESLint 규칙으로 컴파일 타임 에러도 가능:
// .eslintrc.cjs (사용자 측이 깔 수 있음)
module.exports = {
rules: {
'no-restricted-syntax': [
'warn',
{
selector: 'Literal[value=/^(bg|text|border)-brand-/]',
message: 'colors.brand is deprecated. Use colors.primary.',
},
],
},
};Step 3: codemod 동봉
packages/codemods/src/v2/rename-brand-to-primary.ts (jscodeshift):
import type { Transform } from 'jscodeshift';
const RENAME_MAP: Record<string, string> = {
'color.brand': 'color.primary',
'colors.brand': 'colors.primary',
'--color-brand': '--color-primary',
};
const TAILWIND_CLASS_MAP: Record<string, string> = {
'bg-brand': 'bg-primary',
'text-brand': 'text-primary',
'border-brand': 'border-primary',
};
const transform: Transform = (file, api) => {
const j = api.jscodeshift;
const root = j(file.source);
// 1) String literals — Panda recipe, CSS-in-JS
root.find(j.Literal).forEach((path) => {
const value = path.node.value;
if (typeof value !== 'string') return;
// "color.brand" 같은 토큰 reference
for (const [from, to] of Object.entries(RENAME_MAP)) {
if (value === from || value.includes(from)) {
path.node.value = value.replaceAll(from, to);
}
}
// Tailwind class — "bg-brand-500" → "bg-primary-500"
for (const [from, to] of Object.entries(TAILWIND_CLASS_MAP)) {
const regex = new RegExp(`\\b${from}-(\\d+)\\b`, 'g');
path.node.value = (value as string).replace(
regex,
(_, num) => `${to}-${num}`,
);
}
});
// 2) Template literals — `bg-brand-${shade}`
root.find(j.TemplateLiteral).forEach((path) => {
path.node.quasis.forEach((q) => {
for (const [from, to] of Object.entries(TAILWIND_CLASS_MAP)) {
q.value.raw = q.value.raw.replaceAll(from, to);
q.value.cooked = q.value.cooked?.replaceAll(from, to);
}
});
});
return root.toSource({ quote: 'single' });
};
export default transform;사용자가 실행:
# 한 번에 마이그레이션
pnpm dlx @org/codemods v2/rename-brand-to-primary "src/**/*.{ts,tsx,css}"packages/codemods/bin.ts (CLI):
#!/usr/bin/env node
import { run as jscodeshift } from 'jscodeshift/src/Runner';
import { resolve } from 'node:path';
const [, , transformName, ...paths] = process.argv;
const transformPath = resolve(
__dirname,
`transforms/${transformName}.js`,
);
jscodeshift(transformPath, paths, {
parser: 'tsx',
extensions: 'ts,tsx,js,jsx,css',
verbose: 1,
});package.json:
{
"name": "@org/codemods",
"bin": "./dist/bin.js",
"scripts": {
"test": "vitest"
}
}각 transform에는 입력·출력 예시 fixture와 vitest 테스트가 동봉된다:
packages/codemods/
└── src/v2/rename-brand-to-primary/
├── transform.ts
├── transform.test.ts
└── __fixtures__/
├── input.tsx
└── output.tsxStep 4: 메이저에서 제거
v2.0 publish 시 changeset:
---
"@org/tokens": major
"@org/react": major
---
**BREAKING**: `color.brand`를 제거합니다.
v1.5에서 deprecated 되었습니다. 마이그레이션:
```bash
pnpm dlx @org/codemods v2/rename-brand-to-primary "src/**/*.{ts,tsx,css}"수동으로 옮기려면:
color.brand.*→color.primary.*bg-brand-*→bg-primary-*--color-brand-*→--color-primary-*
---
## Figma 측 마이그레이션
토큰은 *디자인 도구에도* 살아 있다. Tokens Studio 또는 Figma Variables의 이름도 같이 옮겨야 한다.
```mermaid
sequenceDiagram
participant FE as Figma 디자이너
participant TS as Tokens Studio
participant GH as GitHub (tokens)
participant SD as Style Dictionary
participant USER as 사용자 앱
FE->>TS: brand → primary로 rename
TS->>GH: push to tokens.json
GH->>SD: 빌드 트리거
SD->>GH: dist/ 업데이트 (alias 포함)
GH->>USER: npm publish v1.5
Note over USER: alias 유지 → 사용자 코드 안 깨짐핵심: Tokens Studio에서 이름 변경이 아니라 새 이름 추가 + 기존 alias로 작업한다. Figma 측에서도 1.5는 두 이름 모두 살아 있음.
What-if — 잘못 쓰면 어떻게 깨지는가
-
함정 1 — alias 누락: 새 이름만 추가하고 기존을 안 두면 즉시 깨짐. 대응: deprecation PR 템플릿에 체크리스트 —
[ ] 기존 이름이 alias로 살아 있는가. -
함정 2 — warn이 prod에 노출:
process.env.NODE_ENV체크 누락 → 사용자 prod console 오염. 대응: warn은 반드시NODE_ENV !== 'production'가드. CI에서 빌드 후 grep으로 검증. -
함정 3 — codemod가 문자열만 옮김:
<div className={isActive ? 'bg-brand' : 'bg-gray'}>처럼 조건부 표현식은 잘 잡지만,clsx({'bg-brand-500': isActive})같은 객체 키는 jscodeshift가 놓치기 쉬움. 대응: fixture에 모든 사용 패턴을 미리 등록 + 테스트. 그래도 missed가 있으니 ESLint 규칙을 함께 동봉. -
함정 4 — Tailwind safelist에 옛 이름: 사용자가
tailwind.config.ts의safelist에bg-brand-500을 박아뒀음 → codemod가 동적 클래스 생성 코드를 못 찾음. 대응:safelist별도 grep 가이드를 release notes에 명시. -
함정 5 — Figma·코드 시점 어긋남: 디자이너는 새 이름으로 작업 중인데 사용자 앱은 아직 v1.5 미만 → Figma export가 알 수 없는 토큰. 대응: Figma 측에도 동일한 deprecation 사이클 (Tokens Studio의 alias 기능 활용). 디자이너와 동시 릴리스가 원칙.
-
함정 6 — 메이저에서 제거 후 사용자가 v1으로 회귀 못 함: v1 → v2 마이그레이션 도중 v2에 새 버그 발견. 그런데 v1에선 새 컴포넌트가 없어서 못 돌아감. 대응: v1을 최소 6개월 maintenance (security/critical patch만). v2는 완벽히 검증된 시점에만 v1을 EOL.
Insight — React의 componentWillMount 사례
토큰 deprecation 사이클은 React가 lifecycle method를 처리한 방식의 디자인 시스템 버전이다.
React 16.3 (2018):
componentWillMount→UNSAFE_componentWillMountrename- 기존 이름은 console.warn과 함께 살려둠
- codemod 동봉 (
react-codemod rename-unsafe-lifecycles) - React 17에서 기존 이름 제거
같은 사이클 — 새 이름 + warn + codemod + 메이저 제거.
반전: React 18은 결국 기존 lifecycle을 그대로 둠. Concurrent rendering이 추가됐을 뿐. 이유는 수많은 레거시 코드의 마이그레이션 비용. Deprecation은 제거의 보장이 아니라 이동의 권장이다 — 사용자가 충분히 옮기지 않으면 언제든 연장될 수 있다.
디자인 시스템도 같은 교훈: Deprecation 발표는 자유, 제거는 점유율 95% 이하일 때.
요약
- 토큰 이름 변경은
alias → warn → codemod → 메이저 제거의 4-스텝 사이클. - DTCG
$extensions에 deprecation 메타를 박아 기계가 추적하게 한다. - codemod는 문자열·템플릿·objects keys를 모두 다뤄야 하며, fixture·test가 동봉되어야 한다.
- Figma 측 동기화는 Tokens Studio alias로 같은 사이클을 만든다.
- 메이저 제거는 사용자 점유율 95% 이하일 때만 — 깰 자유는 있지만 제거의 의무는 없다.