2026년 08월 24일

8

같은 Figma를 보고도 매번 다른 코드가 나오는 문제 줄이기

AI Agent

86a81755-86d6-4f87-add9-98465f0d0394 1

figma-design-to-code 스킬에 검증 절차를 붙인 과정

1. 도입 배경

Figma MCP로 화면을 읽고 AI에게 구현을 맡기면 제법 그럴듯한 코드가 나옵니다. 화면 하나를 빠르게 만들어 볼 때는 이 정도면 충분하다고 생각했습니다. 같은 작업을 다시 해보기 전까지는요.

문제는 같은 작업을 다시 실행했을 때였습니다. 한 번은 공통 디자인시스템의 Button을 사용하더니, 다음에는 앱 안에 있는 Button을 선택했습니다. 기존 Figma cache를 두고 MCP를 다시 호출하거나 screenshot만 보고 비슷한 화면을 만드는 일도 있었습니다. 그때마다 Figma와 코드를 처음부터 대조해야 했습니다.

처음에는 prompt를 더 자세히 쓰면 해결될 거라고 생각했습니다. 그런데 규칙을 길게 적는 것만으로는 부족했습니다. AI가 어떤 Figma 정보를 읽었는지, 왜 그 컴포넌트를 골랐는지가 다음 실행에 남지 않았기 때문입니다.

그래서 목표를 바꿨습니다. AI가 항상 같은 코드를 만들게 하려는 대신, 같은 Figma 정보와 이미 검증한 판단을 다시 사용하게 만들기로 했습니다.

2. 먼저 Figma 입력을 고정했습니다

작업은 Figma 링크의 file key와 node ID를 확인하는 데서 시작합니다. 현재 검증 범위는 settlement 신규 작업으로 제한했습니다. 검증하지 않은 앱까지 같은 규칙을 적용하면 잘못된 확신을 줄 수 있다고 봤습니다.

node ID를 확인하면 바로 MCP를 호출하지 않습니다. feature cache와 Git 이력부터 뒤져 같은 node가 있는지 확인합니다. 이미 있다면 오래됐다는 이유만으로 새로 가져오지 않습니다. 찾지 못했을 때만 get_design_context를 호출하고 Framelink, variables, screenshot을 차례로 수집합니다.

확인 순서를 명령으로 줄이면 다음과 같습니다.

1rg "<node-id>" docs/spec-driven/<app>/<feature>/figma-cache 2git log --all -- docs/spec-driven/<app>/<feature>/figma-cache

각 도구에서 확인하는 정보도 다릅니다. 구현 참고 코드와 Code Connect, annotation은 get_design_context에서 봅니다. 화면 안의 node 구조와 component ID, token 연결 정보는 Framelink가 맡습니다. variables는 token 값을 확인할 때 쓰고 screenshot은 마지막 화면 비교를 위해 남겨둡니다.

수집한 결과는 context, Framelink, variables, screenshot, nodes, preflight로 나누어 bundle에 저장했습니다. MCP 호출은 동시에 끝나지 않습니다. 그래서 수집 시각과 SHA-256도 파일마다 따로 기록했습니다. 나중에 검증할 때 지금 보고 있는 정보가 당시 구현에 사용한 정보와 같은지 확인하려는 목적입니다.

manifest에는 이런 형태로 각 파일의 출처와 수집 시각, hash가 들어갑니다.

1{ 2 "artifacts": [ 3 { 4 "kind": "context", 5 "source": "mcp__figma__get_design_context", 6 "capturedAt": "2026-08-21T04:51:24.665Z", 7 "sha256": "7b463b60831e8f1aae60a4f32df9c6d9ea27e22ead3d0786da767109c422790e" 8 }, 9 { 10 "kind": "framelink", 11 "source": "mcp__figma_framelink__get_figma_data", 12 "capturedAt": "2026-08-21T04:51:28.713Z", 13 "sha256": "fdfd1114546226ec70459a2e1ab9d883e015764435c526bd70a661581c439e05" 14 } 15 ] 16}

3. 컴포넌트 선택 순서를 규칙으로 만들었습니다

Figma 정보를 저장하는 것만으로는 충분하지 않았습니다. 같은 component를 보고도 어떤 코드를 사용할지는 다시 판단해야 했습니다.

컴포넌트는 다음 순서로 확인하도록 정했습니다.

1Code Connect 2→ 공통 디자인시스템 3→ settlement 컴포넌트 4→ native 또는 신규 구현

무엇을 골랐고 왜 골랐는지는 receipt에 남겼습니다. Figma 화면 안의 INSTANCE가 하나라도 빠지면 검증은 실패합니다. 공통 디자인시스템의 Button을 쓸 수 있는데 다른 Button을 고르거나 새로 만들 때도 근거가 필요합니다. 기록만 그럴듯하게 채우는 일도 막았습니다. 선택한 import와 export가 고정된 commit에 실제로 존재하는지 검사했습니다.

예를 들어 공통 디자인시스템의 Button을 선택한 판단은 다음처럼 남습니다.

1{ 2 "id": "component:<figma-node-id>", 3 "kind": "component", 4 "status": "resolved", 5 "selected": { 6 "import": "@fe-service/design-system/components", 7 "export": "Button", 8 "source": "root-ds" 9 }, 10 "selectionBasis": "root-candidate", 11 "evidence": { 12 "resolvedPath": "packages/design-system/src/components/button/button.tsx" 13 } 14}

이 과정이 이번 작업에서 가장 중요한 가드레일이었습니다. AI에게 공통 컴포넌트를 우선 사용하라고 요청하는 데서 끝내지 않고, 근거 없이 우회하면 다음 단계로 넘어가지 못하게 만들었습니다.

4. 구현 전후를 세 단계로 나누어 검사했습니다

validator는 D4, D6, D7 세 단계로 나누었습니다.

1pnpm --silent figma:validate bundle <bundle-path> --gate d4 2pnpm --silent figma:validate bundle <bundle-path> --gate d6 3pnpm --silent figma:validate bundle <bundle-path> --gate d7 --seal-revision <commit>

D4가 확인하는 것은 component 선택의 완전성입니다. Framelink에서 찾은 INSTANCE가 receipt에 전부 들어 있는지, component ID와 variant 정보가 맞는지 검사합니다. 선택한 import와 export가 실제 코드에 있는지도 이 단계에서 확인합니다.

D6는 구현에 들어가기 전 마지막 확인입니다. 선택하지 못했거나 여러 후보 사이에서 결론을 내리지 못한 component가 하나라도 있으면 실패합니다. 검사를 통과하면 receipt의 SHA-256이 출력되므로 이후 검토에서도 같은 receipt를 사용했는지 알 수 있습니다.

구현 중 새로운 정보를 발견해도 코드를 먼저 바꾸지 않습니다. receipt를 수정하고 component mapping부터 다시 검사합니다. 코드 모양은 AI가 프로젝트 문맥에 맞게 바꿀 수 있지만, 이미 검증한 component 선택을 조용히 바꾸지는 못하게 했습니다.

5. 마지막은 browser render로 확인했습니다

정적 검사만 통과했다고 Figma와 같은 화면이라고 말할 수는 없습니다. 구현이 끝나면 receipt를 commit으로 고정하고, repository에 설치된 Playwright로 같은 route, state, viewport의 화면을 캡처했습니다.

화면만 저장하는 것으로 끝내지 않고 재현에 필요한 조건도 함께 기록했습니다.

1{ 2 "render": { 3 "screenshot": "render.png", 4 "route": "/settlements/monthly?month=2026-07", 5 "state": "WORK 단계, 특정 직원 필터 read-only 화면", 6 "viewport": { 7 "width": 375, 8 "height": 916, 9 "deviceScaleFactor": 1 10 } 11 } 12}

reviewer는 receipt를 보지 못합니다. 전달받는 것은 Figma screenshot과 실제 browser render, nodes.json, 고정 commit의 구현 코드뿐입니다. 미리 허용한 차이에 끌려가지 않고 눈에 보이는 차이부터 찾도록 했습니다.

D7에서는 reviewer가 찾은 차이와 receipt에 미리 기록한 차이를 비교합니다. 양쪽에 기록된 node와 속성이 정확히 같아야 통과합니다. receipt에만 있는 예외도 실패하고, reviewer만 발견한 차이도 실패합니다. browser render가 없으면 D7을 통과할 수 없습니다.

validator의 마지막 비교는 단순합니다. 미리 기록한 차이와 review에서 발견한 차이를 같은 locator로 정렬한 뒤 두 집합이 다르면 실패시킵니다.

1const declared = receipt.declaredDeviations.map(locatorKey).sort(); 2const observed = report.deviations.map(locatorKey).sort(); 3 4if (JSON.stringify(declared) !== JSON.stringify(observed)) { 5 throw new Error('declared and observed deviations differ'); 6}

Figma 입력을 가져올 때는 Playwright나 CDP를 사용하지 않았습니다. Playwright는 구현 화면을 캡처하는 마지막 단계에서만 사용했습니다. Figma 입력과 구현 결과를 얻는 경로를 섞지 않으려는 선택이었습니다.

6. settlement PoC에서 확인한 것

처음에는 확인 dialog 한 화면으로 전체 흐름을 검증했습니다. 이후 FRAME을 root로 가진 모바일 화면도 처리할 수 있도록 validator를 보완하고, 세 화면의 bundle을 추가했습니다. 고정된 실제 bundle을 다시 검사해 보니 세 화면 모두 D4, D6, D7을 통과했습니다.

blind review에서는 Figma의 템플릿 표기와 실제 문구의 차이, backdrop 투명도처럼 screenshot에서 확인되는 차이가 잡혔습니다. 이를 막연히 비슷하다고 넘기지 않고 node와 속성 단위로 receipt에 남겼습니다. 같은 화면을 다시 작업할 때 무엇이 의도된 차이인지 처음부터 추측하지 않아도 됩니다.

7. 회고

이번 작업을 하며 좋은 prompt만으로는 풀리지 않는 문제가 있다는 걸 다시 느꼈습니다. 판단이 필요한 구현은 AI에게 맡기고, 입력과 선택 기준, 통과 조건은 코드로 검사하는 편이 오히려 단순했습니다.

AI는 코드 구조를 프로젝트에 맞게 조정하고, validator는 반복해서 지켜야 하는 조건을 검사합니다. 한쪽으로 전부 해결하려 하지 않고 역할을 나누는 방식입니다.

물론 이 스킬이 Figma MCP 호출과 최종 코드를 결정론적으로 만든 것은 아닙니다. 지금 고정할 수 있는 범위는 Figma cache, Git과 skill revision, component 선택 내역, 구조 검증까지입니다. token 선택을 receipt에 포함하는 작업도 남아 있고, settlement 외 앱은 각 앱의 component와 token 구조를 확인한 뒤에야 적용할 수 있습니다.

이전과 달라진 점은 분명합니다. 결과가 마음에 들지 않을 때 AI에게 다시 만들어 달라고 요청하는 대신, 어느 입력과 판단에서 차이가 생겼는지 확인할 수 있습니다. AI의 불확실성을 없애지는 못했지만 불확실성이 퍼지는 범위는 줄였습니다.

HS
@Hyungseok Kwon
Journey To Solution