04 — Design System as Contract
디자인 시스템의 본질은 컴포넌트가 아니라 계약이다. 토큰의 이름과 컴포넌트의 공개 API는 SemVer에서 말하는 공개 인터페이스이고, 디자인 시스템 팀은 그 인터페이스를 깨지 않을 의무를 진다. 이 계약 의식이 없으면 디자인 시스템은 “내일 다시 갈아엎을 임시 prefix” 로 전락하고, 의식이 있으면 10년 가는 인프라가 된다. 토큰 이름 = 디자인 시스템의 ABI.
Why — 왜 디자인 시스템을 “계약”이라 부르는가
소프트웨어 엔지니어에게 계약(contract) 이라는 단어는 공개 API의 약속이라는 강한 의미를 갖는다. axios.get()의 시그니처가 바뀌면 수백만 의존 프로젝트가 깨진다. 그래서 axios는 SemVer를 따른다 — 메이저 버전을 올리지 않는 한 시그니처는 안전하다.
디자인 시스템도 동일하다. color.primary.500이라는 토큰 이름은:
- 100개 컴포넌트가 직접 참조한다.
- 1,000개 화면에서 간접 참조한다.
- Figma 라이브러리가 같은 이름으로 가리킨다.
- iOS·Android 앱이 같은 토큰을 빌드한다.
이 이름을 깰 수 있는 권한은 메이저 버전 릴리스 + 마이그레이션 가이드 + 6개월 deprecation 기간을 동반해야 한다. 이게 계약이다.
그런데 대부분의 디자인 시스템 프로젝트는 이 의식이 없다. 디자이너가 “이 이름이 더 좋겠다”고 하면 그날 바꿔버린다. 그 순간 디자인 시스템은 죽는다 — 다른 팀이 “내일 또 바뀔 수 있는 prefix”로 인식하기 시작하고, 결국 자기네 prefix를 따로 만든다.
How — 무엇이 계약이고 무엇이 아닌가
계약에 해당하는 것 (Public API)
계약이 아닌 것 (Private/Implementation)
| 변경해도 되는 것 | 이유 |
|---|---|
토큰의 값 (#3370b8 → #2c66a8) | 이름만 같으면 소비자는 값을 모름. 단, 시각적으로 눈에 띄게 바뀌면 마이너 버전 |
컴포넌트 내부 구현 (div → span) | 외부에서 styling 의존하지 않는 한 OK |
| recipe의 내부 base 스타일 | 결과가 동일하면 OK |
| 빌드 도구·번들러 (Vite → Turbopack) | 빌드 산출물만 같으면 OK |
SemVer로 매핑
[Major].[Minor].[Patch]
| | |
| | └── 버그 픽스, 토큰 값의 미세 조정 (시각 동일)
| │
| └── 새 토큰 추가, 새 variant 추가, deprecation 시작
|
└── 토큰 이름 변경, prop 이름 변경, variant 삭제, slot 이름 변경| 변경 | 버전 |
|---|---|
color.brand.500 추가 | Minor |
color.primary.500 값을 #3370b8 → #2c66a8 (디자이너 의도된 톤 조정) | Minor (시각 차이 있음) |
color.primary.500 값을 #3370b8 → #3370b9 (보정) | Patch |
color.primary.500 이름을 color.brand.500로 변경 | Major |
<Button kind="primary"> → <Button intent="primary"> (prop 이름 변경) | Major |
<Button intent="warning"> 추가 | Minor |
<Button intent="primary"> 삭제 | Major |
토큰 이름 = ABI (Application Binary Interface)
C/C++의 ABI는 함수 시그니처가 컴파일된 바이너리 레벨에서 호환 가능한가를 다룬다. ABI가 깨지면 링커 단계에서 에러가 난다.
디자인 시스템에서 토큰 이름은 ABI에 해당한다:
- 토큰 이름이 컴파일 결과(CSS variables 이름, TS 객체의 키)에 직접 박힌다.
- 이름이 바뀌면 런타임이 아니라 빌드 단계에서 에러 (또는 토큰이 그냥 사라짐).
- 다중 패키지(
@company/ui+@company/marketing-ui)에 같은 토큰 의존성이 있으면, 한 쪽 메이저 업그레이드 시 다른 쪽도 같은 메이저로 끌어올려야 함.
/* tokens.css — 컴파일 결과 */
:root {
--color-primary-500: #3370b8; /* 이 이름이 ABI */
--space-3: 12px; /* 이 이름이 ABI */
}--color-primary-500을 --color-brand-500으로 바꾸면 모든 CSS에서 깨진다. 이게 ABI break.
What — 계약을 지키는 메커니즘
1) RFC 프로세스 (Request for Comments)
토큰 이름이나 컴포넌트 API를 변경할 때 PR 전에 RFC 문서 먼저. Atlassian Design System의 공개 RFC 리포가 모범.
# RFC-0042: Rename `color.primary` → `color.brand`
## Summary
Rename semantic token `color.primary.*` to `color.brand.*`.
## Motivation
"Primary" is overloaded — primary action, primary brand color, primary text.
"Brand" is unambiguous.
## Breaking Change?
**Yes** — Major version bump required.
## Migration Path
1. Add `color.brand.*` as alias of `color.primary.*` (Minor v2.5)
2. Mark `color.primary.*` as deprecated, with codemod (Minor v2.6)
3. Remove `color.primary.*` (Major v3.0, +6 months later)
## Affected Surfaces
- @company/ui (Button, Card)
- @company/marketing-ui (Hero)
- Figma library (Tokens Studio sync needed)2) Deprecation 기간
브레이킹 체인지를 즉시가 아니라 3~6개월 후 적용:
// tokens.ts
export const tokens = {
color: {
primary: {
500: {
$value: '#3370b8',
$deprecated: 'Use color.brand.500 instead. Will be removed in v3.0.0',
},
},
brand: {
500: { $value: '#3370b8' }, // 같은 값, 새 이름
},
},
}빌드 시 deprecated 토큰을 쓰면 console.warn — 단계적 이주가 가능.
3) Codemod (자동 마이그레이션 스크립트)
# 사용자가 실행
npx @company/ui-codemod v3-migration ./src
# 내부 동작: AST 변환
# - className="bg-primary-500" → className="bg-brand-500"
# - var(--color-primary-500) → var(--color-brand-500)
# - <Button kind="primary"> → <Button intent="primary">대표 사례:
- React의
react-codemod(jsx → createElement, class component → hooks) - Next.js의
@next/codemod(메이저 업그레이드마다 제공) - Material UI의
mui-codemod
원칙: 브레이킹 체인지에는 codemod가 동반. 없으면 RFC 통과 안 함.
4) Changeset / Conventional Commits
# 사용자가 PR 작성 시
pnpm changeset
# 인터랙티브 프롬프트
# - which packages? @company/tokens
# - major/minor/patch? major
# - summary? "Rename color.primary to color.brand".changeset/*.md 파일이 PR과 함께 머지 → 자동으로 changelog + npm publish.
5) 시각적 회귀 테스트 (Visual Regression)
값만 바꿨는데 예상보다 큰 시각 변화가 생기는 경우 — 이건 마이너지만 눈에 띄게 다르면 major로 취급해야 한다. Chromatic / Percy 같은 도구로 스토리북 스냅샷 diff 자동화.
# .github/workflows/visual-regression.yml
- name: Run Chromatic
uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: false # 변화 있으면 failWhat-if — 계약을 깨면 어떻게 무너지나
1) “사소한 토큰 이름 변경”의 도미노
- 증상: 디자이너가
color.warning→color.caution으로 이름 변경 요청. 디자인 시스템 팀이 그날 바꿈. 1주일 후, 마케팅 페이지에서 노란색이 다 사라짐. - 원인: 마케팅 팀의
@company/marketing-ui가color.warning을 import. 빌드는 통과(트리쉐이킹), 런타임에 조용히 fallback 색 사용. - 대응: RFC + alias + deprecation. 즉시 변경 금지.
2) “MUI 5에서 6으로 메이저 업그레이드, 6개월간 마비”
- 증상: Material UI 5 → 6 메이저 업그레이드.
<Button color="primary">같은 prop의 시그니처가 바뀜. 1,500개 컴포넌트 사용처를 다 손봐야 함. - 원인: 메이저 업그레이드에 codemod 제공. 하지만 우리 코드가 prop을 간접 참조 (
<MyButton color={someColor}>→<Button color={color}>)하는 부분은 codemod가 못 잡음. - 대응: 디자인 시스템 간접 참조 자체를 줄임. 컴포넌트 prop을 그대로 노출하지 말고, 우리 도메인의 prop으로 추상화.
3) “스타트업 모드 — 매주 토큰 이름 변경”
- 증상: 초기 6개월간 디자인 시스템 팀이 주마다 토큰 이름 변경. 컴포넌트 사용 팀이 복사 + prefix 변경으로 회피 시작. 1년 뒤 디자인 시스템이 사용되지 않음.
- 원인: 계약 의식 부재. 시스템이 안정적이지 않다는 신호가 시장에 전달됨.
- 대응: 처음부터 SemVer 지키기. 초기 6개월은
0.x.x로 둬서 불안정함을 명시 →1.0.0이후 진짜 계약.
4) “Figma와 코드의 토큰 이름 불일치”
- 증상: Figma는
Primary/500, 코드는color.primary.500. 디자이너 핸드오프에서 손으로 매핑. 1년 뒤 둘이 따로 진화. - 원인: 단일 출처가 없음. 둘 다 자기가 source of truth라 주장.
- 대응: DTCG JSON을 단일 출처로 두고 Figma와 코드 둘 다 이걸 읽음. Tokens Studio → JSON export → Style Dictionary → CSS / Figma 양쪽 주입.
5) “버전 없는 디자인 시스템”
- 증상: 디자인 시스템을 monorepo의 한 폴더로 두고 npm publish 안 함. 다른 팀이 경로로 import. 토큰 이름이 자유롭게 바뀜.
- 원인: 공개 API의 경계가 없음. 모든 변경이 내부 변경처럼 처리.
- 대응: 반드시 npm 패키지로 publish. monorepo라도
@company/ui로 import 강제. 패키지 경계가 계약 경계다.
6) 토큰 값의 미세 조정이 시각 회귀를 일으킴
- 증상: 디자이너가 patch 수준이라 생각해
#3370b8→#3372b9변경. 그런데 다크모드에서 contrast 검사가 AA 미달로 떨어짐. 접근성 컴플라이언스 실패. - 원인: 토큰 값 변경의 영향 범위가 단순 시각이 아니라 접근성 계약까지 확장.
- 대응: 토큰 값 변경에 자동 contrast 검사 (axe-core, APCA-check). PR이 PR-block.
Insight — “ABI”라는 비유가 왜 강력한가
“디자인 시스템 사람들은 컴포넌트를 보고, 컴파일러 사람들은 심볼을 본다. 토큰 이름은 둘 다이다.”
C++에서 ABI break는 런타임에 mangled symbol이 안 맞아서 segfault가 난다. 마이크로소프트는 매년 Visual C++의 ABI 호환성 보고서를 낸다 — 어느 심볼이 깨졌는지 공개한다. Linux 커널은 ABI compatibility를 거의 종교 수준으로 지킨다 — 25년 전 컴파일된 바이너리도 오늘 부트 가능. 이게 수명을 만든다.
디자인 시스템에서 수명이 긴 시스템은 모두 ABI 의식이 있다:
- GitHub Primer — 2017년 시작, 토큰 이름이 거의 안 바뀜.
--color-fg-default는 8년째 같은 이름. - Atlassian Design System — 2014년 시작, 메이저 버전 변경에 항상 codemod + RFC + 6개월 deprecation.
- IBM Carbon — 2015년 시작,
$themes객체 구조가 거의 안 바뀜.
반면 5년 안에 폐기된 디자인 시스템들은 거의 모두 임의 이름 변경의 흔적이 있다.
또 하나 — 계약은 디자이너에게도 약속이다. 디자이너가 어제 디자인한 화면이 오늘 무너지지 않는다는 약속. 토큰 이름이 안정적이면 디자이너는 Figma의 토큰 변수를 신뢰하고, 신뢰가 쌓이면 디자이너가 직접 토큰을 추가하는 워크플로가 가능해진다. 이게 디자인 엔지니어링 (design engineering) 의 출발점이다.
마지막으로, “디자인 시스템 = 인프라” 라는 인식이 자리잡고 있다. 인프라는 깨지지 않음이 그 가치다. 데이터베이스가 재미있어서 쓰는 게 아니라 안정적이라서 쓴다. 디자인 시스템도 마찬가지 — 재미가 아니라 안정성이 그 가치다. 이 인식의 전환이 계약 의식을 만든다.
요약 + Mermaid
- 디자인 시스템 = 계약. 토큰 이름·컴포넌트 prop·variant 값이 공개 API.
- 토큰 이름 = ABI. C++의 mangled symbol처럼 빌드 단계에서 깨진다.
- 계약을 지키는 5가지 메커니즘: RFC, Deprecation, Codemod, Changeset, Visual Regression.
- 계약 의식 없는 시스템은 주마다 이름 변경으로 신뢰를 잃고 1~2년 안에 폐기.
- 10년 가는 시스템(Primer, ADS, Carbon)은 모두 ABI 의식이 있다.