이 글은 누구를 위한 것인가
- 수천 줄의 하드코딩 CSS를 디자인 토큰으로 전환해야 하는 프론트엔드 팀
- 기존 서비스를 운영하면서 디자인 시스템을 처음 도입하는 팀
- 마이그레이션 중 팀 저항과 일정 부담을 최소화하고 싶은 테크 리더
들어가며
"디자인 토큰을 도입하겠다"고 결정했다. 그런데 프로덕션에는 수십만 줄의 CSS가 있고, 하드코딩된 색상(#4A90E2, rgba(0,0,0,0.12))과 크기 값(14px, 1.5rem)이 수백 곳에 흩어져 있다. 어디서부터 시작해야 할까?
빅뱅 마이그레이션(한 번에 전부 전환)은 위험하다. 서비스 중단 없이 점진적으로 전환하는 전략이 필요하다. 이 글에서는 팀이 실제로 써온 레거시 CSS를 토큰 기반으로 전환하는 단계별 방법을 설명한다.
이 글은 bluefoxdev.kr의 디자인 토큰 도입 전략 을 참고하고, 레거시 마이그레이션 관점에서 확장하여 작성했습니다.
1. 마이그레이션 전 현황 파악
1.1 레거시 코드 감사 (Audit)
마이그레이션 전에 현재 상태를 정량화해야 한다.
# 프로젝트 내 하드코딩된 색상 값 수 파악
grep -r "#[0-9a-fA-F]\{3,6\}" src/ --include="*.css" --include="*.scss" | wc -l
# 고정 픽셀 값 파악
grep -r "[0-9]\+px" src/ --include="*.css" | grep -v "0px" | wc -l
# 중복 색상 값 찾기
grep -roh "#[0-9a-fA-F]\{6\}" src/ | sort | uniq -c | sort -rn | head -20
1.2 감사 결과를 토큰으로 매핑
발견된 색상 → 시맨틱 토큰 매핑 계획
#4A90E2 (23회 사용) → color.primary.500
#357ABD (8회 사용) → color.primary.600
#F5F5F5 (45회 사용) → color.background.secondary
#333333 (67회 사용) → color.text.primary
#666666 (34회 사용) → color.text.secondary
#E5E5E5 (29회 사용) → color.border.default
2. 토큰 구조 설계
2.1 3계층 토큰 구조
Primitive Tokens (원시값)
color.blue.50: #EFF6FF
color.blue.500: #3B82F6
color.blue.900: #1E3A8A
space.1: 4px
space.2: 8px
Semantic Tokens (의미 기반)
color.primary.default: {color.blue.500}
color.primary.hover: {color.blue.600}
color.text.primary: {color.gray.900}
space.component.padding.sm: {space.2}
Component Tokens (컴포넌트별)
button.primary.background: {color.primary.default}
button.primary.background.hover: {color.primary.hover}
2.2 CSS 변수로 출력
/* 자동 생성: tokens/output/variables.css */
:root {
/* Primitive */
--color-blue-50: #eff6ff;
--color-blue-500: #3b82f6;
/* Semantic */
--color-primary-default: var(--color-blue-500);
--color-text-primary: var(--color-gray-900);
/* Component */
--button-primary-bg: var(--color-primary-default);
}
[data-theme="dark"] {
--color-text-primary: var(--color-gray-100);
--color-background-default: var(--color-gray-900);
}
3. 점진적 마이그레이션 전략
3.1 Strangler Fig 패턴 적용
레거시 시스템을 즉시 교체하지 않고, 새 토큰 시스템이 점차 레거시를 대체하도록 한다.
[Phase 1] 토큰 정의 + 새 컴포넌트에만 적용
- 새로운 기능 개발 시 반드시 토큰 사용
- 기존 코드는 건드리지 않음
[Phase 2] 가장 자주 변경되는 컴포넌트 우선 마이그레이션
- 버튼, 입력 필드, 카드 등
- PR 단위로 작은 범위만 변경
[Phase 3] 자동화 스크립트로 대량 치환
- 정규식 기반 하드코딩 → 변수 치환
- 시각적 회귀 테스트로 변경 검증
[Phase 4] 레거시 값 완전 제거
- 더 이상 사용되지 않는 하드코딩 값 lint 규칙 추가
3.2 자동 치환 스크립트
// scripts/migrate-colors.js
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const colorMap = {
'#4a90e2': 'var(--color-primary-default)',
'#357abd': 'var(--color-primary-hover)',
'#f5f5f5': 'var(--color-background-secondary)',
'#333333': 'var(--color-text-primary)',
'#666666': 'var(--color-text-secondary)',
'#e5e5e5': 'var(--color-border-default)',
};
function migrateFile(filePath) {
let content = fs.readFileSync(filePath, 'utf8');
let changed = false;
for (const [oldValue, newValue] of Object.entries(colorMap)) {
const regex = new RegExp(oldValue.replace('#', '\\#'), 'gi');
if (regex.test(content)) {
content = content.replace(regex, newValue);
changed = true;
}
}
if (changed) {
fs.writeFileSync(filePath, content);
console.log(`✅ Migrated: ${filePath}`);
}
}
const files = glob.sync('src/**/*.{css,scss}');
files.forEach(migrateFile);
4. 팀 저항 최소화 전략
4.1 흔한 반대 의견과 대응
| 반대 의견 | 대응 방법 |
|---|---|
| "지금도 잘 돌아가는데 왜 바꿔?" | 다크모드 추가, 브랜드 리뉴얼 시 비용 절감 데이터 제시 |
| "마이그레이션 할 시간이 없다" | 새 개발에만 적용, 기존 코드는 점진적으로 |
| "토큰 이름을 외워야 해?" | 자동완성 지원 IDE 플러그인, Figma에서 바로 확인 |
| "버그 생기면 어떻게?" | 시각적 회귀 테스트(Chromatic)로 사전 검출 |
4.2 성공 지표 설정
KPI 예시:
- 토큰 커버리지: 전체 CSS 중 토큰 사용 비율 (목표: 80%)
- 하드코딩 색상 수: 마이그레이션 전 대비 감소율
- 테마 전환 시간: 새 테마 적용에 걸리는 개발 시간 (목표: 1시간 이내)
- 디자인-코드 불일치 버그: 분기별 감소율
5. Lint 규칙으로 회귀 방지
// .stylelintrc.json
{
"rules": {
"color-no-invalid-hex": true,
"declaration-property-value-disallowed-list": {
"color": ["/^#/", "/^rgb/"],
"background-color": ["/^#/", "/^rgb/"],
"border-color": ["/^#/", "/^rgb/"]
}
}
}
이 lint 규칙을 CI에 추가하면 새로운 하드코딩 색상이 PR에서 차단된다.
마무리: 마이그레이션 성공 조건
디자인 시스템 마이그레이션의 성공 조건은 기술이 아닌 사람과 프로세스다.
- 팀 공감대: 왜 해야 하는지 모두가 이해
- 작은 시작: 첫 PR은 버튼 하나의 색상 변수 교체만도 충분
- 자동화: 수작업 마이그레이션은 지속되지 않음
- 검증: 시각적 회귀 테스트로 신뢰 확보
레거시를 완전히 없애는 것이 목표가 아니다. 새로운 개발이 100% 토큰 기반으로 이루어지는 것이 첫 번째 목표다.