|
| 1 | +# Design System Toolkit — 설계 문서 |
| 2 | + |
| 3 | +**날짜**: 2026-04-14 |
| 4 | +**작성자**: seongwon seo |
| 5 | +**상태**: 승인됨 |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 배경 및 목표 |
| 10 | + |
| 11 | +### 문제 |
| 12 | + |
| 13 | +- 코드베이스 전반에 하드코딩된 색상값(`#3B82F6`, `rgb(...)` 등)이 산재해 있어 UI 일관성 유지가 어렵다. |
| 14 | +- 디자인 토큰이 TypeScript 파일로만 관리되어 단일 진실 공급원(single source of truth)이 불명확하다. |
| 15 | +- 토큰 위반을 감지하는 자동화 수단이 없어 코드 리뷰에서 사람이 직접 확인해야 한다. |
| 16 | + |
| 17 | +### 목표 |
| 18 | + |
| 19 | +1. 디자인 토큰을 JSON 파일로 중앙 관리하고, 빌드 시 TypeScript 파일로 자동 변환한다. |
| 20 | +2. ESLint 커스텀 룰로 하드코딩된 값을 개발 중 실시간 감지한다. |
| 21 | +3. GitHub Actions로 PR마다 토큰 위반을 자동으로 리포트한다. |
| 22 | + |
| 23 | +### 범위 외 (이번 버전) |
| 24 | + |
| 25 | +- Figma API 직접 연동 (향후 확장) |
| 26 | +- AI 기반 컴포넌트 코드 자동 생성 (향후 확장) |
| 27 | +- 간격(spacing), 그림자(shadow) 등 색상 외 토큰의 ESLint 룰 (향후 확장) |
| 28 | + |
| 29 | +--- |
| 30 | + |
| 31 | +## 아키텍처 |
| 32 | + |
| 33 | +``` |
| 34 | +[tokens/] ← 단일 진실 공급원 (JSON) |
| 35 | + ├── colors.json |
| 36 | + ├── typography.json |
| 37 | + └── spacing.json |
| 38 | + ↓ |
| 39 | +[Style Dictionary] ← 빌드 타임 변환 (npm run ds:build) |
| 40 | + ↓ |
| 41 | +[src/styles/theme/] ← 자동 생성 (직접 편집 금지) |
| 42 | + ├── colors.ts ✦ generated |
| 43 | + ├── typography.ts ✦ generated |
| 44 | + └── index.ts |
| 45 | + ↓ |
| 46 | +[ESLint custom rule] ← 개발 중 실시간 위반 감지 |
| 47 | + ↓ |
| 48 | +[GitHub Actions] ← PR마다 audit 결과 코멘트 |
| 49 | +``` |
| 50 | + |
| 51 | +**핵심 원칙**: |
| 52 | + |
| 53 | +- `tokens/`가 단일 진실 공급원. 토큰 수정 시 반드시 JSON 파일만 수정. |
| 54 | +- 기존 styled-components + ThemeProvider 사용 방식은 변경 없음. |
| 55 | +- 3단계를 독립적으로 배포 가능 — 1단계만 완료해도 팀에 가치가 있음. |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +## 1단계: 토큰 파이프라인 (Style Dictionary) |
| 60 | + |
| 61 | +### 파일 구조 |
| 62 | + |
| 63 | +``` |
| 64 | +tokens/ |
| 65 | +├── colors.json |
| 66 | +├── typography.json |
| 67 | +└── spacing.json |
| 68 | +
|
| 69 | +config/ |
| 70 | +└── style-dictionary.config.js |
| 71 | +
|
| 72 | +src/styles/theme/ ← Style Dictionary 출력 (기존 경로 유지) |
| 73 | +├── colors.ts ← ⚠️ AUTO-GENERATED. Edit tokens/ instead. |
| 74 | +├── typography.ts ← ⚠️ AUTO-GENERATED. Edit tokens/ instead. |
| 75 | +├── transitions.ts ← 수동 관리 (애니메이션은 토큰화 범위 외) |
| 76 | +└── index.ts |
| 77 | +``` |
| 78 | + |
| 79 | +### 토큰 JSON 형식 |
| 80 | + |
| 81 | +```json |
| 82 | +// tokens/colors.json |
| 83 | +{ |
| 84 | + "color": { |
| 85 | + "primary": { "value": "#3B82F6", "type": "color" }, |
| 86 | + "primary-hover": { "value": "#2563EB", "type": "color" }, |
| 87 | + "text-default": { "value": "#111827", "type": "color" }, |
| 88 | + "text-muted": { "value": "#6B7280", "type": "color" }, |
| 89 | + "background": { "value": "#FFFFFF", "type": "color" }, |
| 90 | + "border": { "value": "#E5E7EB", "type": "color" } |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +### Style Dictionary 설정 |
| 96 | + |
| 97 | +```js |
| 98 | +// config/style-dictionary.config.js |
| 99 | +module.exports = { |
| 100 | + source: ['tokens/**/*.json'], |
| 101 | + platforms: { |
| 102 | + ts: { |
| 103 | + transformGroup: 'js', |
| 104 | + buildPath: 'src/styles/theme/', |
| 105 | + files: [ |
| 106 | + { |
| 107 | + destination: 'colors.ts', |
| 108 | + format: 'javascript/es6', |
| 109 | + filter: { type: 'color' }, |
| 110 | + }, |
| 111 | + { |
| 112 | + destination: 'typography.ts', |
| 113 | + format: 'javascript/es6', |
| 114 | + filter: { type: 'typography' }, |
| 115 | + }, |
| 116 | + ], |
| 117 | + }, |
| 118 | + }, |
| 119 | +}; |
| 120 | +``` |
| 121 | + |
| 122 | +### npm 스크립트 |
| 123 | + |
| 124 | +```json |
| 125 | +{ |
| 126 | + "ds:build": "style-dictionary build --config config/style-dictionary.config.js", |
| 127 | + "ds:watch": "style-dictionary build --watch --config config/style-dictionary.config.js", |
| 128 | + "prebuild": "npm run ds:build" |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +`prebuild` 훅으로 `npm run build` 전에 자동 실행. (기존 `prebuild` 훅 없음 — 충돌 없이 추가 가능) |
| 133 | + |
| 134 | +### 마이그레이션 전략 |
| 135 | + |
| 136 | +1. 기존 `src/styles/theme/colors.ts` 값을 `tokens/colors.json`으로 이관 |
| 137 | +2. Style Dictionary로 동일한 `colors.ts` 재생성 (출력 검증) |
| 138 | +3. 기존 파일 상단에 `// ⚠️ AUTO-GENERATED. Edit tokens/ instead.` 주석 추가 |
| 139 | +4. `.gitignore`에 추가하지 않음 — 생성 파일도 git에 포함해 CI 없이도 팀이 바로 사용 가능 |
| 140 | + |
| 141 | +--- |
| 142 | + |
| 143 | +## 2단계: ESLint 커스텀 룰 |
| 144 | + |
| 145 | +### 감지 대상 |
| 146 | + |
| 147 | +styled-components 템플릿 리터럴 내부의 하드코딩된 색상값: |
| 148 | + |
| 149 | +- HEX: `#rgb`, `#rrggbb`, `#rrggbbaa` |
| 150 | +- RGB/RGBA: `rgb(...)`, `rgba(...)` |
| 151 | +- HSL/HSLA: `hsl(...)`, `hsla(...)` |
| 152 | +- Named colors: `red`, `blue` 등 CSS 색상 키워드 |
| 153 | + |
| 154 | +```tsx |
| 155 | +// ❌ 위반 — ESLint 경고 |
| 156 | +const Button = styled.button` |
| 157 | + color: #3b82f6; |
| 158 | + background: rgb(0, 0, 0); |
| 159 | +`; |
| 160 | + |
| 161 | +// ✅ 통과 |
| 162 | +const Button = styled.button` |
| 163 | + color: ${({ theme }) => theme.colors.primary}; |
| 164 | +`; |
| 165 | +``` |
| 166 | + |
| 167 | +### 구현 |
| 168 | + |
| 169 | +``` |
| 170 | +src/eslint-rules/ |
| 171 | +└── no-hardcoded-design-tokens.js |
| 172 | +``` |
| 173 | + |
| 174 | +- AST에서 `TaggedTemplateExpression` (styled-components) 탐지 |
| 175 | +- 템플릿 리터럴 문자열에서 색상 패턴 정규식 매칭 |
| 176 | +- 위반 시 토큰 역매핑으로 자동 제안 (`#3B82F6` → `theme.colors.primary`) |
| 177 | +- `tokens/colors.json`을 읽어 제안 목록 동적 생성 |
| 178 | + |
| 179 | +### ESLint 설정 |
| 180 | + |
| 181 | +```js |
| 182 | +// eslint.config.mjs ← 프로젝트 기존 파일에 추가 |
| 183 | +import noHardcodedTokens from './src/eslint-rules/no-hardcoded-design-tokens.js'; |
| 184 | + |
| 185 | +export default [ |
| 186 | + { |
| 187 | + plugins: { |
| 188 | + 'design-system': { rules: { 'no-hardcoded-tokens': noHardcodedTokens } }, |
| 189 | + }, |
| 190 | + rules: { |
| 191 | + 'design-system/no-hardcoded-tokens': 'warn', // 안정화 후 'error'로 승격 |
| 192 | + }, |
| 193 | + }, |
| 194 | +]; |
| 195 | +``` |
| 196 | + |
| 197 | +### 에러 메시지 형식 |
| 198 | + |
| 199 | +``` |
| 200 | +Hardcoded color '#3B82F6' found. Use theme token instead: theme.colors.primary |
| 201 | +``` |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## 3단계: GitHub Actions CI |
| 206 | + |
| 207 | +### 워크플로우 |
| 208 | + |
| 209 | +```yaml |
| 210 | +# .github/workflows/design-audit.yml |
| 211 | +name: Design System Audit |
| 212 | + |
| 213 | +on: |
| 214 | + pull_request: |
| 215 | + paths: |
| 216 | + - 'src/**' |
| 217 | + - 'tokens/**' |
| 218 | + |
| 219 | +jobs: |
| 220 | + design-audit: |
| 221 | + runs-on: ubuntu-latest |
| 222 | + permissions: |
| 223 | + pull-requests: write |
| 224 | + |
| 225 | + steps: |
| 226 | + - uses: actions/checkout@v4 |
| 227 | + - uses: actions/setup-node@v4 |
| 228 | + with: |
| 229 | + node-version: '20' |
| 230 | + cache: 'npm' |
| 231 | + |
| 232 | + - run: npm ci |
| 233 | + - run: npm run ds:build |
| 234 | + |
| 235 | + - name: Run ESLint (design tokens audit) |
| 236 | + id: lint |
| 237 | + run: npm run lint -- --format json --output-file lint-results.json || true |
| 238 | + |
| 239 | + - name: Post PR comment |
| 240 | + uses: actions/github-script@v7 |
| 241 | + with: |
| 242 | + script: | |
| 243 | + const fs = require('fs'); |
| 244 | + const results = JSON.parse(fs.readFileSync('lint-results.json', 'utf8')); |
| 245 | + const violations = results |
| 246 | + .flatMap(r => r.messages |
| 247 | + .filter(m => m.ruleId === 'design-system/no-hardcoded-tokens') |
| 248 | + .map(m => ` ${r.filePath.replace(process.cwd(), '')}:${m.line} ${m.message}`) |
| 249 | + ); |
| 250 | +
|
| 251 | + const body = violations.length === 0 |
| 252 | + ? '✅ **Design System Audit**: No hardcoded token violations found.' |
| 253 | + : `🎨 **Design System Audit**: ${violations.length} violation(s) found.\n\n\`\`\`\n${violations.join('\n')}\n\`\`\``; |
| 254 | +
|
| 255 | + github.rest.issues.createComment({ |
| 256 | + issue_number: context.issue.number, |
| 257 | + owner: context.repo.owner, |
| 258 | + repo: context.repo.repo, |
| 259 | + body |
| 260 | + }); |
| 261 | +``` |
| 262 | +
|
| 263 | +### PR 코멘트 예시 |
| 264 | +
|
| 265 | +``` |
| 266 | +🎨 Design System Audit: 3 violation(s) found. |
| 267 | + |
| 268 | + /src/pages/MainPage/components/ClubCard/ClubCard.styles.ts:12 Hardcoded color '#3B82F6'. Use theme.colors.primary |
| 269 | + /src/components/common/Modal/Modal.styles.ts:34 Hardcoded color '#111827'. Use theme.colors.text-default |
| 270 | + /src/pages/AdminPage/components/SideBar/SideBar.styles.ts:8 Hardcoded color '#E5E7EB'. Use theme.colors.border |
| 271 | +``` |
| 272 | +
|
| 273 | +--- |
| 274 | +
|
| 275 | +## 구현 순서 |
| 276 | +
|
| 277 | +``` |
| 278 | +Phase 1: 토큰 파이프라인 ~3일 |
| 279 | + ├── tokens/ 디렉토리 + JSON 파일 작성 |
| 280 | + ├── Style Dictionary 설치 및 설정 |
| 281 | + ├── 기존 theme 파일 마이그레이션 |
| 282 | + └── npm 스크립트 연결 + prebuild 훅 |
| 283 | + |
| 284 | +Phase 2: ESLint 커스텀 룰 ~3일 |
| 285 | + ├── AST 기반 룰 작성 |
| 286 | + ├── 토큰 역매핑 제안 로직 |
| 287 | + ├── ESLint config 연결 |
| 288 | + └── 기존 위반 목록 확인 (warn으로 시작) |
| 289 | + |
| 290 | +Phase 3: GitHub Actions ~1일 |
| 291 | + ├── 워크플로우 파일 작성 |
| 292 | + ├── PR 코멘트 스크립트 작성 |
| 293 | + └── 테스트 PR로 동작 검증 |
| 294 | +``` |
| 295 | +
|
| 296 | +--- |
| 297 | +
|
| 298 | +## Figma 연동 전략 |
| 299 | +
|
| 300 | +토큰 파이프라인은 입력이 JSON이면 출처에 무관하게 동작하도록 설계되어 있다. Figma 연동은 파이프라인의 전제조건이 아니며, 단계적으로 도입한다. |
| 301 | +
|
| 302 | +### 현재 상태 진단 기준 |
| 303 | +
|
| 304 | +| Figma 상태 | 대응 방법 | |
| 305 | +| ----------------------- | -------------------------------------- | |
| 306 | +| Variables/Styles 미정의 | 코드에서 역추출 → `tokens/*.json` 작성 | |
| 307 | +| Styles만 있음 | Tokens Studio 플러그인으로 내보내기 | |
| 308 | +| Variables까지 있음 | Figma Variables API 자동 동기화 | |
| 309 | + |
| 310 | +### 단계별 전략 |
| 311 | + |
| 312 | +**Phase 0 (지금)**: 코드 역추출 |
| 313 | + |
| 314 | +현재 `src/styles/theme/colors.ts`, `typography.ts`에 이미 정의된 값을 `tokens/*.json`으로 이관. Figma 연동 없이 파이프라인 먼저 구축. |
| 315 | + |
| 316 | +``` |
| 317 | +코드(theme/*.ts) → tokens/*.json → Style Dictionary → theme/*.ts (자동 생성) |
| 318 | +``` |
| 319 | +
|
| 320 | +**Phase 1 (파이프라인 안정화 후)**: Tokens Studio 반자동 연동 |
| 321 | +
|
| 322 | +디자이너가 Figma에 Styles/Variables를 정의하면, [Tokens Studio](https://tokens.studio/) 플러그인으로 `tokens.json` 내보내기. 수동이지만 디자이너 주도로 동기화 가능. |
| 323 | +
|
| 324 | +``` |
| 325 | +Figma (Tokens Studio) → tokens/*.json export → PR → ds:build |
| 326 | +``` |
| 327 | +
|
| 328 | +**Phase 2 (선택)**: Figma Variables API 자동화 |
| 329 | +
|
| 330 | +Figma Professional 플랜 이상에서 Variables API 사용 가능. `scripts/sync-figma-tokens.js` 스크립트로 완전 자동화. |
| 331 | +
|
| 332 | +```bash |
| 333 | +# Personal Access Token 필요 |
| 334 | +FIGMA_TOKEN=xxx FILE_ID=yyy npm run ds:figma-sync |
| 335 | +``` |
| 336 | + |
| 337 | +``` |
| 338 | +Figma Variables API → scripts/sync-figma-tokens.js → tokens/*.json → ds:build |
| 339 | +``` |
| 340 | + |
| 341 | +### 핵심 원칙 |
| 342 | + |
| 343 | +- **파이프라인 입력 포맷(JSON)은 고정** — Figma 연동 방식이 바뀌어도 이후 과정은 변경 없음 |
| 344 | +- **디자이너와의 싱크 시점**: 파이프라인이 작동하기 시작하면 디자이너에게 Figma Styles/Variables 정의 요청. 코드 기준 토큰을 Figma에 역으로 반영하는 것도 가능. |
| 345 | + |
| 346 | +--- |
| 347 | + |
| 348 | +## 향후 확장 가능성 |
| 349 | + |
| 350 | +- **AI 코드 생성**: Claude API로 토큰 스펙 기반 컴포넌트 초안 생성 |
| 351 | +- **spacing/shadow 룰**: ESLint 룰을 색상 외 토큰으로 확장 |
| 352 | +- **토큰 사용 리포트**: 각 토큰이 몇 곳에서 쓰이는지 통계 생성 |
| 353 | + |
| 354 | +--- |
| 355 | + |
| 356 | +## 성공 기준 |
| 357 | + |
| 358 | +- [ ] `npm run ds:build` 실행 시 `tokens/*.json` → `src/styles/theme/*.ts` 변환 성공 |
| 359 | +- [ ] 하드코딩된 색상값에 ESLint 경고가 표시됨 |
| 360 | +- [ ] PR 오픈 시 GitHub Actions가 자동으로 위반 목록을 코멘트로 남김 |
| 361 | +- [ ] 기존 `ThemeProvider` 기반 코드가 마이그레이션 후에도 정상 동작 |
0 commit comments