# Figma Diff Apply

> Figma 변경을 디자이너 검수용 design/* 브랜치에 incremental 적용. /figma-apply 커맨드에서 위임 호출. design-to-ui(7-Step 새 화면 스캐폴딩)와 달리 변경된 컴포넌트만 식별·수정합니다. figma URL 1개(figma-driven) / 2개(diff-driven) 두 모드 지원. 새 화면 변환은 이 스킬 대신 design-to-ui를 사용하세요.

- Skill: `naver/figma-diff-apply` (Agent Skill)
- Install (CLI): `npx skillmds@latest add naver/figma-diff-apply`
- Raw SKILL.md: https://api.skillmd.com/api/skills/naver/figma-diff-apply/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- License: Apache-2.0
- Author: naver (https://skillmd.com/u/naver)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/naver/figma-diff-apply

---


# figma-diff-apply

Figma URL의 현재 디자인과 design/* 브랜치의 현 코드를 대조하여 **변경된 컴포넌트만** 최소 수정으로 반영합니다. 신규 화면 전체 변환이 아니라 iteration(N→N+1) 시점에 사용합니다.

`design-to-ui`의 7-Step 풀 파이프라인을 호출하지 않습니다. 그 파이프라인은 "없던 화면을 처음 만들 때"에 최적화되어 있어, iteration마다 돌리면 토큰이 낭비되고 의도치 않은 전체 리팩토링 위험이 있습니다.

## Prerequisites

- 호출 시점에 `design/*` 브랜치가 체크아웃되어 있을 것 (`/figma-start`가 보장)
- Figma Desktop MCP 연결(`get_design_context` 사용 가능) 또는 `FIGMA_ACCESS_TOKEN` 중 하나
- `project-design-system`이 있으면 토큰 매핑에 사용 (없으면 기존 코드 컨벤션 유지)

---

## Phase 0: 경로 판정 (먼저 이걸 정한다)

**0-1. 먼저 요청 유형을 가른다.** 이 스킬은 "Figma 선언값 ↔ 코드"를 대조하는 도구다. 그 전제가 성립하지 않는 요청에 Phase 1·2를 돌리면 아무 근거도 못 얻고 비용만 쓴다.

| 유형 | 신호 | 처리 |
|---|---|---|
| **A. 확인·캡처** | "~노출되는지 확인", "~케이스 보여줘", "이미지 저장해줘" — **코드를 바꾸라는 말이 없다** | **이 스킬이 할 일이 없다.** Figma 수집·diff 없이 `design-qa`(또는 캡처)로 넘긴다. 대조 대상이 아예 없으므로 Phase 1·2 전체를 건너뛴다 |
| **B. 프롬프트-스펙** | **기준값이 프롬프트에 있고 Figma엔 아직 없다.** ① Figma가 표현 못 하는 것을 수치까지 지정("240ms 등속", "즉시 전환") ② **상대 증감**("5dp 올려줘") ③ 절대값인데 Figma가 아직 그 값이 아님 | **프롬프트 값이 기준이다.** Figma는 diff 원본이 아니라 ⓐ 색·치수 토큰 확인 ⓑ 정지 상태 정합 제약 ⓒ **차이 보고용**으로만 쓴다. Figma 선언값으로 요청을 덮어쓰지 말고, 어긋나면 Phase 4 "디자이너 확인 필요"에 올린다 |
| **C. Figma 대조** | **기준값이 Figma에 있다** — Figma 쪽이 바뀌었으니 코드를 맞춰 달라는 요청 | 아래 0-2로 진행 |

유형 B의 판정은 추측하지 말고 확인한다 — `get_motion_context(nodeId, recursive=true)`가 `{"nodes":[]}`면 Figma에 모션 데이터가 없다는 뜻이고, 그러면 그 요청의 수치는 프롬프트에만 존재한다.

**0-2. 유형 C면 규모로 경로를 가른다.** 잘못 고르면 20dp 한 줄 바꾸는 데 서브에이전트 4홉이 돌거나, 반대로 화면 전체 변경을 메인이 혼자 훑는다.

| | 국소 경로 (기본) | 광범위 경로 |
|---|---|---|
| 조건 | 디자이너 프롬프트가 **요소와 속성을 지목**하고(예: "트랙 리스트 사이 간격 20dp로", "타이틀 색 초록으로"), 대상 파일이 `Grep`으로 1~2개로 확정됨 | 대상 화면·요소가 프롬프트로 특정되지 않음 / 후보 파일 3개+ / "전체적으로 바뀌었어" 류 / 신규 컴포넌트 추가 |
| Phase 2 | 메인이 직접 (서브에이전트 0개) | Diff Agent 팬아웃 + QA Agent |
| 근거 | 국소 변경에서 팬아웃은 요청과 무관한 오탐을 만들고, QA는 그 오탐을 걷어내는 데 다시 비용을 쓴다 (실측: 607s·202k → 53s·11k, 정확도는 국소 경로가 더 높았음) | 대상이 불명확할 때는 컨텍스트 격리·독립 QA가 실제로 누락을 잡는다 (v6.1: QA 제거 시 통과율 5/5 → 2/5) |

애매하면 국소 경로로 시작하고, Phase 1에서 후보가 3개+로 벌어지면 광범위로 승격한다.

### 유형 B의 정지 상태 회귀 검증

Figma에 없는 동작을 넣을 때 가장 깨지기 쉬운 것은 **정지 상태**다. 애니메이션을 얹었더니 시작·끝 프레임이 기존 에셋과 미세하게 달라져 design-qa 오버레이가 전부 어긋나는 식이다. 그래서 유형 B는 구현 후 다음을 **픽셀로** 확인한다:

1. 변경 전 커밋과 변경 후를 각각 같은 정지 상태로 띄운다 (이 프로젝트는 `-e state selected` 처럼 상태를 인자로 받는다).
2. 두 캡처를 diff 해서 **차이가 0인지** 확인한다.

기존 에셋을 재사용해 점진 노출·클립하는 식으로 구현하면 이 검증이 통과한다. 반대로 글리프·도형을 새로 그려 흉내내면 거의 통과하지 못한다.

---

## Phase 1: 수집

### 1-1. Figma 스펙

```javascript
get_metadata(nodeId)      // 먼저: 프레임 목록·좌표·크기
get_design_context(...)   // 그 다음: 대상 노드로 좁혀서
get_variable_defs(nodeId)
get_screenshot(nodeId)
```

- **`get_design_context`는 프레임 또는 그 하위 노드로 좁혀서 호출한다.** 상위 레벨 노드로는 코드가 나오지 않는다 — **페이지(canvas) 노드는 호출 자체가 실패**하고, **section 노드는 sparse metadata만** 돌아온다("개별 노드로 다시 호출하라"는 안내와 함께). `/figma-apply`가 받는 링크는 페이지 노드인 경우가 많으므로, `get_metadata`로 프레임 목록을 먼저 받아 대상 프레임(또는 프롬프트가 지목한 서브노드)을 고른 뒤 호출한다.
- **`get_metadata`의 좌표·크기는 간격·치수 판정의 결정적 근거다.** 형제 노드의 `y` − (앞 노드 `y` + `height`) = 간격. `design_context`를 프레임마다 다시 뜨지 않고도 **한 번의 `get_metadata`로 모든 프레임의 선언값을 비교**할 수 있다.

  판정법: 같은 리스트를 담은 프레임들을 나란히 놓고 `다음 카드 y − (앞 카드 y + height)`를 비교한다. 카드 높이가 같은데 이 값이 프레임마다 다르면 **같은 요소가 프레임별로 다르게 선언된 것**이다. 코드가 라이트/다크 공용 컴포저블 한 벌이면 한쪽에 맞추는 순간 다른 쪽이 어긋난다.

  **이런 불일치는 고쳐놓고 넘어가지 말고 Phase 4 "디자이너 확인 필요"에 올려 회신을 받는다.**

- **읽은 값은 그 시점의 값이다.** Figma는 라이브 문서이고 노드 ID까지 바뀐다 (실측: 같은 세션 1시간 사이에 `TrackList` gap이 20 → 12로, statusbar 노드가 `2009:47` → `176:4`로 변했다). 그래서 ① 판정 근거로 쓴 수치는 Phase 4 표에 **읽은 값 그대로 남기고**, ② 커밋 본문에 들어가 나중에 대조 가능하게 한다. 오래된 발췌를 재사용하지 말고 매번 다시 읽는다.
- **diff-driven 모드(URL 2개)**: `get_design_context`를 두 시점 모두 호출해 임시 파일로 저장, Phase 2의 `diff` 입력으로 사용.
- MCP 미연결 시: Figma Desktop 앱 실행을 안내(플러그인 CLAUDE.md "Figma MCP 사전 확인" 절). REST fallback은 `figma-asset-download/scripts/` 헬퍼.

### 1-2. 대상 화면·파일 확정

Code Connect 매핑이 있으면 `get_code_connect_map(nodeId)` 결과를 우선한다.

**목표 화면** = 이 Figma 노드에 대응하는 화면 단위. 컨테이너 타입(Fragment / Composable Screen / Activity)은 가리지 않는다. 이후 Phase 3·3.5의 **scope 기준 단위**가 된다.

매핑 실패한 프레임은 "신규 컴포넌트"로 분류해 Phase 4에 표시한다.

---

## Phase 2: 변경 식별

산출물은 Phase 4와 같은 스키마의 표다 — `분류(Added/Changed/Removed/Moved) | 요소 | Figma 값 | 코드 값 | 위치(file:line)`. **네 칸이 다 채워지지 않는 행은 넣지 않는다** (그래서 "레이아웃 변경" 같은 행은 성립하지 않는다).

`Removed`(코드에 있고 Figma에 없음)는 역방향으로 따로 찾는다 — 특히 **call-site removal**(`design_context`엔 없는데 코드가 여전히 호출·렌더하는 요소). v6.1 실험에서 재현율 100%로 미식별됐던 항목이라 명시한다.

### 국소 경로

메인이 직접 대조한다. 서브에이전트를 쓰지 않는다.

**같은 값이 여러 곳에 있을 때 노드 주석·구조로 대상을 가려낸다** — 실측에서 리스트 간격 12dp와 헤더 간격 12dp가 값이 같아 혼동 위험이 있었다.

프롬프트가 요청하지 않은 차이가 눈에 띄어도 **표에 넣지 않는다.** Phase 4의 "그밖에 눈에 띈 점"에 한 줄로만 남기고 적용하지 않는다.

### 광범위 경로

`design_context`는 변환 원본이라 **메인이 직접 읽는다** — 서브에이전트 요약을 받으면 수치·중첩 구조가 소실된다.

**2a. Diff Agent** — 후보 파일당 1개, 단일 응답에서 병렬 호출. 각 Agent는 담당 파일 하나만 알고(직접 `Read`), 다른 Agent의 산출물과 메인의 의사결정은 받지 않는다. 메인은 **출력 표만** 받는다.

**2b. QA Agent** — 독립 세션 1개, 2a 컨텍스트 미상속(자기평가 편향 차단). 아래를 cross-check해 하나라도 FAIL이면 전체 FAIL.

- **값 검증** — 표의 `Figma 값` 칸이 실제 노드 값과 맞는지 **직접 재조회해서** 확인한다. 2a가 받은 발췌를 그대로 믿지 않는다. (실측 근거: 한 Diff Agent가 stale 발췌를 믿고 `20dp`로 단정한 반면 다른 Agent는 라이브를 재조회해 `12px`로 정반대 결론을 냈다. Case ①에서 유일한 실제 회귀 — 이미 정합했던 화면을 깨뜨리는 행 — 도 이 검증이 좌표 재조회로 잡았다.)
- **Removed 검증** — 사라진 호출이 누락 없이 잡혔는지
- **매핑 커버리지** — Figma 프레임 80%+
- **스코프 위반 행** — 요청 범위 밖을 건드리는 행 표시. 표시된 행은 Phase 3에서 적용하지 않는다

실측에서 QA가 잡아낸 것의 대부분이 **팬아웃이 만든 오탐**이었다(6행 중 유효 2행, 1행은 적용하면 정합했던 화면을 회귀). 그래서 QA는 광범위 경로에만 둔다.

**2c. 재시도 1회** — FAIL이면 2a를 새 서브에이전트로 재spawn(1차 컨텍스트 재사용 금지 — 같은 세션은 같은 누락을 반복한다). 2차도 FAIL이면 경고를 붙여 Phase 3 진행.

### URL 2개 모드 (diff-driven)

두 figma 응답을 unix `diff`로 비교해 변경 라인만 추출 → 키워드로 코드 `Grep` → 적용. 결정적 텍스트 비교라 2a 팬아웃을 대체하며 토큰이 크게 줄고 false signal이 없다. 세부 단계는 메인 재량.

QA(2b)는 **경로 판정을 따른다** — 국소면 생략, 광범위면 위와 동일.

---

## Phase 3: 최소 변경 적용

- **Phase 2 표에 없는 파일·요소는 수정 금지** ("함께 정리" 명목도 안 됨)
- **변경을 목표 화면에 가둔다 (scope 회피 우선)** — 다른 화면도 쓰는 값(공통 토큰·공유 컴포넌트)을 바꿔야 하면 **목표 화면 국소 해법**을 우선한다. 국소 해법이 없거나 본래 전역(디자인 시스템) 변경이면 진행하되 Phase 3.5에 영향 범위를 기록. 단 적용된 디자인을 되돌리는 식의 회피는 금지. (실측 배경: 이전에 sports 작업 중 전역 `AppTypography`를 고쳐 다른 화면까지 영향이 갔다.)
- **색·치수는 `project-design-system`이 있으면 토큰으로** 매핑, 없으면 기존 컨벤션 유지
- **에셋은 `figma-asset-download`에 위임한다** — 신규 정적 에셋, diff-driven에서 새로 나온 `imgXxx` 상수 모두. 동적 비주얼(Canvas·그래프)은 SVG의 실제 파라미터를 보고 적용한다

---

## Phase 3.5: 영향 범위 산출 (scope 안전망)

변경한 값·심볼의 사용처를 `Grep`해 **고유 화면 수**를 센다(파일 위치가 아니라 실제 사용처 기준). **2개 이상 화면이 쓰는 변경**이면 Phase 4 "영향 범위"에 목록을 기록한다 — 커맨드가 커밋 본문에 넣고, 개발자가 `git log`로 확인한다.

완벽한 scope 판정이 아니라 **명백한 누수를 보이게 하는 최소 그물**이다. 차단하거나 자동 되돌림하지 않고, 디자이너에게 scope 판단을 시키지 않는다.

---

## 빌드 에러 수정 (커맨드 재위임 시)

`/figma-apply`가 빌드 게이트에서 실패하면 빌드 에러와 함께 이 스킬을 다시 위임 호출한다. **빌드 실행·재빌드는 커맨드 책임이고 이 스킬은 코드 편집만 한다**(gradle 직접 실행 금지). 커맨드가 최대 3회까지 재위임한다.

- 이미 메인에 있는 `design_context`·Phase 2 표를 재사용한다 — Figma에서 새로 가져오지 않는다
- **적용된 디자인 변경을 되돌려 빌드만 통과시키지 않는다.** 누락 에셋은 `figma-asset-download`로 채우고(참조 삭제 금지), 못 고치면 *수정 불가*로 보고하고 중단한다 (커맨드가 WIP 커밋으로 개발자에게 인계)

---

## Phase 4: 결과 요약

```markdown
## figma-diff-apply 결과

### 경로
국소 (서브에이전트 0) / 광범위 (Diff Agent N개 + QA)

### 변경 적용
| 분류 | 요소 | Figma 값 | 코드(변경 전) | 위치 |
|------|------|---------|-------------|------|

### QA 검증 (광범위 경로에서만)
| 기준 | PASS/FAIL |

### 신규 컴포넌트(매핑 실패)
- (없음)

### 영향 범위 (2개+ 화면에 영향 시에만 기록)
- (없음 — 변경이 목표 화면에 한정됨)

### 디자이너 확인 필요
- 같은 요소가 Figma 프레임마다 다른 값으로 선언된 경우 그 목록
- (없으면 생략)

### 그밖에 눈에 띈 점 (적용 안 함)
- (없으면 생략)
```

---

## 금지사항

- **`design-to-ui` 7-Step 풀 파이프라인 호출 금지** — 이 스킬의 존재 이유 자체가 그 회피
- **국소 경로에서 서브에이전트 팬아웃 금지** — 요청과 무관한 오탐을 만든다
- **광범위 경로에서 QA FAIL을 경고 없이 통과 금지**
- **디자이너가 지정한 값을 Figma 선언값으로 임의 교체 금지** — 둘이 어긋나면 요청대로 적용하고 차이를 Phase 4 "디자이너 확인 필요"에 올린다. 실측에서 Diff Agent가 "5dp 올려줘"(→17dp) 요청을 Figma 선언값 20dp로 덮어쓰라고 결론낸 사례가 있다
- **baseline 파일(`.claude/context/ui-spec/*.md`) 참조 금지** — 후속 PR(`project-ui-spec` 도입)에서 다룸

<!--
Design-To-UI
Copyright (c) 2026-present NAVER Corp.
Apache-2.0
-->

