AI 퍼블리싱의 일관성 문제, 디자인 시스템 맵으로 해결하기
문제: 같은 디자인인데 AI가 매번 다른 코드를 뱉는다
Figma 시안을 AI(Claude, GPT 등)에게 "이거 코드로 만들어줘"라고 하면 겪는 문제가 있다.
같은 UI를 줬는데 매번 다른 코드가 나온다.
// 첫 번째 시도 — AI가 프로젝트 컴포넌트를 모름
<Badge colorScheme="blue">소진</Badge>
// 두 번째 시도 — 컴포넌트는 찾았는데 색상을 추측
<StatusBadge color="green" label="소진" />
// 세 번째 시도 — 드디어 맞음
<StatusBadge color="blue" label="소진" />왜 이런 일이 생길까?
AI는 매 요청마다 처음부터 판단한다. "이 Figma 노드에 어떤 컴포넌트를 쓸까?", "배경색 #e9f2ff는 blue야 green이야?", "import는 어디서 하지?" — 이 질문들을 매번 새로 답한다.
사람이라면 한번 정하면 기억하지만, AI는 기억이 없다. 그래서 같은 디자인을 줘도 결과가 들쭉날쭉하다.
해결 아이디어: 판단 자체를 없앤다
"AI야, 잘 판단해"가 아니라 판단할 필요가 없는 구조를 만들면 된다.
핵심 발상은 이것이다:
사람이 읽는 마크다운 규칙 문서 대신, 스크립트가 읽는 JSON 매핑 파일을 만들자.
이전: Figma 데이터 → AI가 규칙 문서를 읽고 판단 → 코드
이후: Figma 데이터 → 스크립트가 JSON으로 자동 결정 → AI는 조립만 → 코드AI가 "이 색이 뭘까?" 고민하는 대신, 스크립트가 미리 답을 정해놓는 것이다.
핵심: design-system-map.json
모든 매핑 규칙을 하나의 JSON 파일에 모았다. 프로젝트 루트의 .claude/design-system-map.json이다.
프로젝트/
.claude/
design-system-map.json ← 이 파일이 핵심
public/
token.json ← Figma에서 export한 디자인 토큰
src/
components/ ← 프로젝트 컴포넌트들이 맵 안에는 여러 종류의 매핑이 들어있다. 그중 핵심 5가지를 살펴보면 아래와 같다. (이 외에도 buttonVariantMap, fillsToStatusBadgeByText, semanticComponentRules 등이 자동 생성된다)
1. instanceMap — "이 Figma 컴포넌트 → 이 코드 컴포넌트"
Figma에서는 재사용 가능한 UI 조각을 "컴포넌트 인스턴스(INSTANCE)"라고 부른다. React의 컴포넌트와 같은 개념이다.
문제는 AI가 Figma의 "Status Badge"라는 이름만 보고 어떤 React 컴포넌트를 써야 할지 모른다는 것이다. instanceMap이 이걸 직접 알려준다:
{
"instanceMap": {
"Status Badge": {
"component": "StatusBadge",
"import": "@/components/status-badge",
"defaultProps": {},
"colorResolve": "fillsToStatusBadge"
},
"Single Select": {
"component": "Select",
"import": "@/components/select",
"defaultProps": {}
}
}
}Figma에서 "Status Badge"를 만나면 → StatusBadge 컴포넌트를 쓰고, import 경로는 @/components/status-badge다. 고민 끝.
2. fillsToStatusBadge — "이 배경색 → 이 color prop"
StatusBadge 같은 컴포넌트는 color prop으로 색상을 정한다. 그런데 Figma에서 넘어오는 건 #eff7ff 같은 hex 값이다.
AI가 #eff7ff를 보고 "이게 blue일까 green일까?" 추측하는 대신, 미리 정해놓는다:
{
"fillsToStatusBadge": {
"#eff7ff": "blue",
"#e0ffeb": "green",
"#fef2e3": "yellow",
"#fff5f5": "red",
"#f3f4f6": "grey",
"#fcf1f3": "pink"
}
}이 매핑은 어디서 올까? Figma에서 export한 token.json에 답이 있다:
token.json에 accent-blue1: "#eff7ff" 가 있으면
↓ 자동으로
design-system-map에 "#eff7ff" → "blue" 가 생긴다수작업 없이 토큰 파일에서 자동 추출된다.
3. hexContextMap — "같은 색인데 쓰이는 곳이 다르면?"
같은 #e5e7eb라도 배경에 쓰이면 grey.3이고, 테두리에 쓰이면 border.basic.2다. 실무에서 정말 자주 겪는 문제인데, hexContextMap이 이걸 구분한다:
{
"hexContextMap": {
"background": { "#e5e7eb": "background.basic.4" },
"border": { "#e5e7eb": "border.basic.2" },
"text": { "#111827": "grey.10", "#6b7280": "grey.6" }
}
}"이 hex 값이 어디에 쓰이는가"까지 매핑해놓으면 AI가 맥락을 추측할 필요가 없다.
4. importPaths — "import 경로 추측 금지"
AI가 가장 자주 틀리는 것 중 하나가 import 경로다. @/components/StatusBadge인지, @/components/status-badge인지, @/components/ui/status-badge인지.
전체 목록을 그냥 줘버린다:
{
"importPaths": {
"StatusBadge": "@/components/status-badge",
"Select": "@/components/select",
"Table": "@/components/table",
"Box": "@chakra-ui/react/box",
"Flex": "@chakra-ui/react/flex"
}
}5. pageTemplates — "이 페이지는 어떤 유형?"
Figma 화면을 보고 "이건 테이블 페이지다", "이건 대시보드다"를 판별하는 것도 자동화했다. Figma 노드의 children 이름에 특정 키워드가 있으면 페이지 유형이 결정된다:
{
"pageTemplates": {
"table": {
"structure": "Header → Layout → Controls + Table",
"requiredImports": [
{ "component": "PageHeader", "from": "@/components/page-header" },
{ "component": "Table", "from": "@/components/table" }
],
"indicators": ["Table", "Header Row", "Row", "Pagination"]
}
}
}children에 "Table", "Row", "Pagination"이 보이면 → 테이블 페이지로 확정. 필요한 import 목록까지 한 번에 결정된다.
그 외 자동 생성되는 매핑들
위 5가지 외에도 생성기가 자동으로 만드는 매핑이 3개 더 있다.
fillsToStatusBadgeByText — 텍스트 색상으로 뱃지 색상 결정
앞서 본 fillsToStatusBadge가 배경색(accent-blue1)으로 color를 결정한다면, 이건 텍스트 색(accent-blue2)으로 결정한다. 같은 token.json의 accent 패턴에서 추출되지만 레벨이 다르다:
{ "#0066cc": "blue", "#16a34a": "green", "#dc2626": "red" }배경이 투명하고 텍스트만 있는 뱃지에서 유용하다.
buttonVariantMap — 버튼 배경색으로 variant/colorPalette 결정
Figma에서 버튼의 배경색이 #3366ff면 primary 버튼인지, #ef4444면 삭제 버튼인지를 자동 판별한다. token.json의 primary-4, accent-red2, grey-2 등의 시맨틱 토큰에서 추출된다:
{
"#3366ff": { "variant": "solid", "colorPalette": "primary" },
"#ef4444": { "variant": "solid", "colorPalette": "red" },
"#e5e7eb": { "variant": "solid", "colorPalette": "grey" }
}semanticComponentRules — 프레임워크별 금지 패턴
이건 token.json이 아니라 생성기 스크립트 안에 하드코딩된 상수다. 프레임워크 커뮤니티의 베스트 프랙티스를 강제하는 룰이다:
{
"forbidden": [
{ "pattern": "<Box display=\"flex\"", "replacement": "<Flex>" },
{ "pattern": "style={{", "replacement": "className=\"...\"" }
]
}package.json에서 감지된 프레임워크에 따라 Chakra/Tailwind/MUI 각각 다른 금지 패턴이 들어간다.
맵 자동 생성
이 맵을 직접 작성할 필요는 없다. token.json과 src/components/를 스캔하면 전부 자동 생성된다.
python3 ~/.claude/scripts/init_design_system_map.py --project-root /path/to/project이 스크립트가 하는 일을 단계별로 보면 이렇다.
Step 1: token.json에서 색상 패턴 추출
디자인 토큰 파일에는 accent-blue1, accent-green2 같은 이름 규칙이 있다. 생성기가 사용하는 숫자의 의미:
1→ 배경색 (연한 색) →fillsToStatusBadge에 저장2→ 텍스트색 (진한 색) →fillsToStatusBadgeByText에 저장
accent_re = re.compile(r'^accent-(\w+?)(\d)')
for name, hex_val in name_to_hex.items():
m = accent_re.match(name)
if m:
color_name = m.group(1) # blue, green, yellow...
level = int(m.group(2))
if level == 1:
status_badge_bg[hex_val] = color_name
elif level == 2:
status_badge_text[hex_val] = color_nameaccent-blue1: #eff7ff → "#eff7ff": "blue" 매핑이 자동으로 만들어진다.
Step 2: 시맨틱 토큰 이름으로 용도 분류
토큰 이름 자체에 용도 힌트가 있다. background-basic-1은 배경, border-basic-1은 테두리:
for name, hex_val in name_to_hex.items():
if name.startswith('background-basic-'):
bg_map[hex_val] = dot_name
elif name.startswith('border-basic-'):
border_map[hex_val] = dot_name
elif re.match(r'^grey-(\d+)', name):
num = int(...)
if num <= 4: # 밝은 grey → 배경
bg_map[hex_val] = dot_name
else: # 어두운 grey → 텍스트
text_map[hex_val] = dot_nameStep 3: 컴포넌트 파일 스캔
src/components/ 아래 모든 .tsx 파일에서 export된 컴포넌트 이름과 경로를 수집한다:
NAMED_EXPORT_RE = re.compile(
r'export\s+(?:default\s+)?(?:function|const)\s+(\w+)'
)export default StatusBadge → { "StatusBadge": "@/components/status-badge" } 이런 식으로.
Step 4: Figma 이름 ↔ 코드 컴포넌트 매칭
미리 정의된 패턴 테이블을 기반으로, 프로젝트에 실제로 존재하는 컴포넌트만 instanceMap에 추가한다:
FIGMA_INSTANCE_PATTERNS = {
'Status Badge': 'StatusBadge',
'Single Select': 'Select',
'Search Input': 'SearchInput',
}
for figma_name, comp_name in FIGMA_INSTANCE_PATTERNS.items():
if comp_name in import_paths: # 프로젝트에 있을 때만
instance_map[figma_name] = { ... }분석 스크립트에서 어떻게 쓰이나
생성된 맵은 Figma 분석 스크립트(figma_analyze.py)에서 자동으로 사용된다.
분석 스크립트 흐름:
1. Figma API로 노드 트리 가져오기
2. token.json으로 색상/텍스트 토큰 역색인 빌드
3. design-system-map.json 로드
4. INSTANCE → 코드 컴포넌트 자동 결정 ← 맵이 결정
5. hex → 컨텍스트별 시맨틱 토큰 자동 결정 ← 맵이 결정
6. 페이지 유형 자동 판별 ← 맵이 결정
7. 결과 JSON 출력4~6단계가 맵으로 추가된 부분이다. 전부 AI 토큰 0개로 처리된다. 스크립트가 결정적(deterministic)으로 처리하니까.
resolve 전후 비교
맵 적용 전 — AI가 직접 판단해야 한다:
{
"name": "Status Badge",
"type": "INSTANCE",
"fills": [{ "hex": "#eff7ff" }]
}맵 적용 후 — 답이 이미 나와 있다:
{
"name": "Status Badge",
"type": "INSTANCE",
"fills": [{ "hex": "#eff7ff" }],
"resolvedComponent": {
"component": "StatusBadge",
"import": "@/components/status-badge",
"props": { "color": "blue", "label": "소진" },
"resolvedBy": "instanceMap"
}
}AI는 resolvedComponent를 보고 그대로 코드에 넣기만 하면 된다. 판단이 아니라 조립이다.
프레임워크별로 달라지는 것
생성기는 package.json의 dependencies를 보고 UI 프레임워크를 자동 감지한다. Chakra UI, Tailwind CSS, MUI, Ant Design 4종을 지원하며, 프레임워크에 따라 달라지는 설정이 있다:
| Chakra UI | Tailwind CSS | MUI | |
|---|---|---|---|
| 기본 import | @chakra-ui/react/* | @/components/ui/* (shadcn) | @mui/material/* |
| 금지 패턴 | <Box display="flex"> → <Flex> 사용 | style={{}} → className 사용 | <div style={{display:"flex"}} → <Stack> 사용 |
다만 색상 매핑은 프레임워크와 무관하다. token.json 구조가 같으면 Chakra든 Tailwind든 동일한 hexContextMap을 쓴다.
전체 아키텍처
글로벌 (모든 프로젝트에서 공유) 프로젝트별 (각 프로젝트 고유)
~/.claude/scripts/ 프로젝트/.claude/
init_design_system_map.py ← 생성기 design-system-map.json ← 결과물
figma_analyze.py ← 분석기
figma_publish.py ← 오케스트레이터
프로젝트/public/
token.json ← Figma 디자인 토큰 (입력)- 생성기: token.json + 컴포넌트 스캔 → design-system-map.json 생성
- 분석기: Figma API + token.json + map → 컴포넌트/색상/페이지유형 결정 완료된 JSON 출력
- 오케스트레이터: 분석 → 자산 추출 → 코드 생성 → 검증까지 전체 파이프라인 실행
정리
| 원칙 | 설명 |
|---|---|
| AI의 판단 영역 최소화 | 결정할 수 있는 건 스크립트가 결정 |
| token.json이 유일한 입력 | 수작업 매핑 없음 |
| 맵은 프로젝트별, 스크립트는 글로벌 | 재사용 극대화 |
| LLM 토큰 0개로 해결 가능한 건 스크립트로 | 비용 절감 + 일관성 |
결과: 누가, 언제, 어떤 AI 에이전트가 실행해도 같은 디자인 → 같은 코드.