Figma 퍼블리싱 파이프라인 07. v5: LLM이 비교하지 않는 설계
"비교는 기계가 하고, LLM은 설명한다."
Figma 시안을 코드로 옮기는 작업에서 가장 큰 병목은 "LLM이 Figma JSON과 코드를 직접 비교하는 것"이다. 추측이 끼어들고, sublayer를 생략하고, 색상을 틀리게 읽는다. v5는 이 문제를 구조적으로 해결한다.
왜 만들었나
기존 v4까지의 문제:
- Figma JSON + 코드를 LLM에 던져서 비교 → 부정확
- 스타일 추측 기반 → 색상, 간격 오류 빈번
- sublayer 생략 가능성 → 누락된 컴포넌트
v5의 해법은 단순하다. LLM이 비교하는 구간을 없앤다.
Figma JSON → Design IR
Code → Code IR
↓
Mapping Engine
↓
Diff Engine
↓
diff.json
↓
LLM (설명 / 설계 / 코드 생성)비교는 규칙 기반 Python 엔진이 하고, LLM은 결과를 읽고 코드만 수정한다.
전체 구조 (13 Phase)
Phase 0─3 : Python (0토큰) ── Figma REST API 수집
Phase 4─8 : Python (0토큰) ── IR 생성 + 정규화 + 비교
Phase 9─11 : LLM (~3,500토큰) ── 리포트 + 설계 + 코드 수정
Phase 12─13: Shell (0토큰) ── 검증 + 정리전체 토큰의 95%를 차지하던 "비교" 구간이 0토큰이다.
Phase 0: Figma 원본 수집
Figma URL에서 fileKey와 nodeId를 파싱해서 REST API 1회 호출.
GET https://api.figma.com/v1/files/{fileKey}/nodes?ids={nodeId}
X-FIGMA-TOKEN: $FIGMA_TOKEN전체 노드 트리를 /tmp/figma_v5_raw.json으로 저장. LLM 없음, 순수 HTTP.
Phase 1: 섹션 선택
raw.json의 직접 자식 노드를 분류해서 사용자에게 제시한다.
[v] 마케터 정보 (7개 노드)
[v] 광고 정보 (8개 노드)
[ ] 콘텐츠 게시 채널 (6개 노드)multiSelect 방식으로 원하는 섹션만 골라서 이후 분석 범위를 제한한다. 전체 페이지를 분석하지 않아도 된다.
Phase 2: 선택 섹션 완전 분석
Phase 1에서 선택된 nodeId마다 REST API를 개별 호출해서 모든 속성을 추출한다.
추출 항목:
- 기하학: x, y, width, height, rotation
- 레이아웃: layoutMode, padding(상하좌우), gap
- 스타일: fills, strokes, fontSize, fontWeight, cornerRadius
- 컴포넌트: variant, colorPalette 등 props
- 상태: visible, hidden, opacity
- 자식 노드 재귀 전부
Phase 1 → Phase 2 연결은 자동이다. semantic_areas에서 nodeId 배열을 파싱해서 자동 전달.
Phase 3: 에셋 추출
exportSettings가 있는 노드만 추려서 다운로드한다.
- 아이콘 →
public/icons/ - 이미지 →
public/images/ - 매니페스트 →
asset_manifest.json
수동 다운로드 없음.
Phase 4: Design IR 생성 (핵심)
Figma JSON을 정규화된 중간 표현(IR) 으로 변환한다. 이게 v5의 핵심이다.
type DesignIRNode = {
nodeId: string
name: string
role: 'text' | 'button' | 'input' | 'container' | 'image' | 'icon' | ...
visible: boolean // 숨겨진 노드도 포함
opacity: number
textContent?: {
value: string
fontSize: number // 정규화된 px
fontWeight: number
color: string // 정규화된 hex
lineHeight: number
}
layout: {
mode: 'flex' | 'grid' | 'absolute' | 'none'
direction?: 'row' | 'column'
gap?: number
padding: { top, right, bottom, left }
width?: number
height?: number
}
style: {
color?: string
backgroundColor?: string
borderRadius?: number
borderColor?: string
}
state: {
variant?: string
disabled?: boolean
}
children: DesignIRNode[]
}숨겨진 노드도 visible: false로 포함하는 것이 중요하다. 기존 파이프라인은 숨긴 노드를 생략해서 코드와 비교할 때 "누락"으로 잘못 판정하는 경우가 있었다.
Phase 5: Code IR 생성
현재 코드베이스의 TSX/JSX 파일을 AST 파싱해서 Design IR과 동일한 구조로 변환한다.
// 프로젝트 코드
<Text fontSize="md" color="grey.900">제목</Text>
<Box px={4} gap="2">...</Box>// Code IR
{
"role": "text",
"textContent": { "fontSize": 16, "color": "#1b1c1d" },
"layout": { "padding": { "left": 16, "right": 16 }, "gap": 8 }
}Chakra UI props, Tailwind 클래스, 디자인 토큰 모두 해석해서 실제 값으로 변환한다.
Phase 6: Normalize
Design IR과 Code IR 양쪽의 모든 값을 동일 단위로 통일한다. 이 단계 없이는 비교가 불가능하다.
| 입력 | 통일 값 |
|---|---|
px={4}, padding="16px", gap="2" | 16 |
"gray.700", #374151, rgb(55,65,81) | "#374151" |
"text-sm", fontSize="14px", fontSize={14} | 14 |
rounded="md", borderRadius={6} | 6 |
Chakra UI의 1 unit = 4px 규칙, 토큰 → hex 변환 등이 여기서 처리된다.
Phase 7: Mapping Engine
정규화된 Design IR ↔ Code IR을 1:1 매핑한다. 신뢰도(confidence)를 포함해서 매핑하는 것이 핵심이다.
7단계 우선순위:
| 순위 | 방법 | confidence |
|---|---|---|
| 1 | 텍스트 exact match | 1.0 |
| 2 | role + 구조 일치 | 0.95 |
| 3 | 컴포넌트 이름 exact match | 0.85 |
| 4 | 구조 Fingerprint (자식 수 + 타입 패턴) | 0.75 |
| 5 | LLM fallback | 0.5 |
{
"mappings": [
{
"designNodeId": "123:456",
"codeNodePath": "Table > Row[1] > Cell[2]",
"confidence": 0.95
}
]
}LLM fallback은 위 4단계가 모두 실패했을 때만 발동한다. 대부분의 경우 0토큰.
Phase 8: Diff Engine (핵심)
매핑된 쌍을 정규화된 값으로 정확 비교한다. LLM이 전혀 개입하지 않는다.
8가지 diff 타입:
| Type | 설명 | Severity |
|---|---|---|
missing_in_code | Figma에 있는데 코드에 없음 | HIGH |
extra_in_code | 코드에만 있고 Figma에 없음 | MEDIUM |
text_mismatch | 텍스트 내용 다름 | HIGH |
color_mismatch | 색상 다름 | MEDIUM |
spacing_mismatch | 간격/패딩 다름 | MEDIUM |
font_mismatch | 폰트 크기/굵기 다름 | MEDIUM |
asset_mismatch | 에셋 참조 다름 | HIGH |
visibility_mismatch | visible 상태 다름 | MEDIUM |
{
"summary": { "total": 15, "high": 3, "medium": 8, "low": 4 },
"diffs": [
{
"type": "spacing_mismatch",
"field": "gap",
"design": 16,
"code": 12,
"severity": "MEDIUM"
}
]
}출력: diff.json. 이게 이후 모든 LLM 단계의 입력이 된다.
Phase 9: LLM 비교 리포트 (~500토큰)
diff.json을 사람이 읽을 수 있는 형식으로 변환하는 유일한 역할.
# 비교 리포트
## Summary
- 총 불일치: 15개
- HIGH: 3개 (즉시 수정 필수)
- MEDIUM: 8개 (권장)
- LOW: 4개 (선택)
## HIGH Priority
| 컴포넌트 | 문제 | 해결책 |
|---------|------|--------|
| Title | missing_in_code | 추가 필요 |
## MEDIUM Priority
| 컴포넌트 | 문제 | Design | Code | 해결책 |
|---------|------|--------|------|--------|
| Button | font_mismatch | 16px | 14px | fontSize 수정 |Phase 10: 설계서 작성 (~1,000토큰)
diff 리포트 기반으로 구체적인 구현 설계서를 만든다.
- 어느 컴포넌트를 추가/수정할지
- 변경 전/후 코드 예시
- 디자인 값 → 프로젝트 토큰 매핑 (
#374151→grey.700) - 구현 우선순위 (HIGH → MEDIUM → LOW)
// 토큰 매핑 예시
const colorMap = {
"#374151": "grey.700",
"#111827": "grey.10",
}
const spacingMap = {
16: "4", // 4 * 4px
8: "2",
}Phase 11: 코드 수정 (~1,500토큰)
설계서를 보고 diff에서 지적된 항목만 수정한다. 전체 파일 재작성이 아니다.
핵심 규칙:
- diff 항목만 수정 (O)
- 기존 로직 100% 보존 (O)
- TypeScript 타입 안정성 유지 (O)
- 리팩터링 금지 (X)
- 추측에 의한 수정 금지 (X)
v5.3 변경사항: Phase 11은 더 이상 anthropic 패키지로 별도 API를 호출하지 않는다. /tmp/figma_v5_phase11_context.md를 생성한 뒤 현재 Claude Code 세션이 직접 파일을 편집한다. 별도 API 키 불필요.
Phase 12: 검증
pnpm type-check # .publishrc.json의 validateCommand 사용HIGH 항목이 남아 있으면 Phase 11 재실행. type-check 통과 필수.
Phase 13: 정리
rm /tmp/figma_v5_*.json최종 보고서 출력:
퍼블리싱 완료
- 시작: 15개 불일치
- 완료: 2개 불일치 (LOW만 남음)
- 해결율: 86%
- 총 토큰: ~3,200
- 총 소요시간: 52초다른 프로젝트에서 쓰는 방법
스킬은 ~/.claude/skills/publish-v5/에 있는 글로벌 스킬이다. 별도 설치 없이 어느 프로젝트에서나 호출 가능하다.
필요한 것
# 환경변수
export FIGMA_TOKEN="your-figma-personal-access-token"
# 시스템
# Python 3.9+, Node.js 18+, pnpm첫 실행 (.publishrc.json 없을 때)
# 프로젝트 루트에서
"퍼블리싱해줘 https://figma.com/design/ABC123/...?node-id=1-2 --v5".publishrc.json탐색 (현재 디렉터리 → git root까지 상위)- 없으면 자동 감지 시작
- 모노레포면 앱 선택 제시
- 단일 프로젝트면 현재 디렉터리 분석
- framework, componentFramework, tokensPath 자동 감지
.publishrc.json저장 여부 확인
프레임워크별 지원
Phase 5 Code IR이 다음을 모두 해석한다:
- Chakra UI:
fontSize="md"→ 16,px={4}→ 16px - Tailwind:
text-sm→ 14,gap-4→ 16px - 디자인 토큰:
tokens.text.primary→ 실제 hex 값
매핑이 안 될 때
"퍼블리싱해줘 [URL] --v5 \
--map '21074:123=>components/custom-row' \
--map '21074:456=>components/payment-info'"토큰 효율
| Phase | 방식 | 토큰 |
|---|---|---|
| 0-8 | Python (규칙 기반) | 0 |
| 9 | LLM (리포트) | ~500 |
| 10 | LLM (설계서) | ~1,000 |
| 11 | LLM (코드 수정) | ~1,500 |
| 12-13 | Shell | 0 |
| 합계 |
v4 대비 토큰은 비슷하지만 정확도가 다르다. v4는 LLM이 Figma JSON과 코드를 동시에 읽고 머릿속에서 비교했다. v5는 Python이 정규화된 값으로 비교한 결과를 LLM에 전달한다.
핵심 요약
| 항목 | v4 | v5 |
|---|---|---|
| 비교 주체 | LLM | Python (Diff Engine) |
| 정규화 | 없음 | Phase 6에서 통일 |
| 매핑 방식 | LLM 추론 | 7-tier 규칙 + confidence |
| sublayer 누락 | 가능 | 불가 (재귀 전부 포함) |
| 숨김 노드 | 생략 | visible: false로 포함 |
| 프레임워크 | 프로젝트 종속 | Chakra/Tailwind 모두 지원 |
| 첫 실행 설정 | 수동 | auto-detect |
비교는 데이터 구조 문제다. LLM에게 비교를 맡기면 언어 모델의 한계가 그대로 정확도의 한계가 된다.