Work
Project2026

AI 에이전트에게 피그마 라이브러리를 맡긴다는 것 — Astryx 디자인 시스템 미러링기

Figma에서 보기
AI 에이전트에게 피그마 라이브러리를 맡긴다는 것 — Astryx 디자인 시스템 미러링기

들어가며

Astryx는 149개 컴포넌트를 제공하는 오픈소스 React 디자인 시스템이다. 우리 팀은 이 코드 기반 디자인 시스템을 그대로 피그마 라이브러리로 옮기는 작업, 이른바 "미러링(mirroring)"을 진행해왔다. 목표는 단순했다 — 코드에 있는 컴포넌트, 프롭, 토큰, 예제를 피그마에서도 1:1로 볼 수 있게 만들어서 디자이너가 실제로 존재하지 않는 컴포넌트를 그리거나, 실제와 다른 스펙으로 디자인하는 일이 없도록 하는 것.
그런데 이 작업을 실제로 해보니 "코드를 보고 피그마에 옮겨 그린다"는 문장 뒤에 생각보다 훨씬 많은 함정이 숨어 있었다. 이 글은 2주 남짓한 기간 동안 AI 에이전트로 이 작업을 진행하면서 우리가 겪은 문제와, 그 문제들을 풀기 위해 결국 별도의 자동화 제어 계층(control plane)까지 만들게 된 과정을 기록한 것이다.

Context — 왜 "코드 → 피그마 미러링"이 어려운 일인가

Astryx는 살아있는 프로젝트다. CLI(npx astryx ...)와 npm 패키지(@astryxdesign/core, @astryxdesign/cli)가 버전업될 때마다 컴포넌트가 추가되고, 프롭이 바뀌고, 공식 예제가 늘어난다. 우리가 미러링해야 하는 대상은 다음과 같았다.
  • 공식 컴포넌트 149개, 페이지 템플릿 43개, 블록 템플릿 584개
  • 각 컴포넌트의 정확한 variant, prop, 기본값, 공식 예제(Examples) 코드
  • 색상 · 간격 · 반경 · 그림자 · 타이포그래피 등 디자인 토큰과 그 실제 값
  • 이 모든 것을 담아낼 피그마 파일 하나(astryx_design_system, 최종 81개 페이지)
문제는 이 정보의 "진짜 소스(source of truth)"가 어디에 있느냐였다. 공식 문서 사이트(astryx.atmeta.com)는 클라이언트 렌더링되는 SPA라서, 일반적인 웹 스크래핑이나 MCP의 얕은 get() 호출로는 실제 렌더링된 Examples 코드를 놓치기 일쑤였다. 실제로 초기 감사에서 Badge의 Badge — Counts, Badge — Status 예제나 Navigation 관련 예제들이 이런 방식으로 통째로 누락된 적이 있다. 그래서 우리는 결국 "문서 사이트가 아니라 로컬 CLI가 유일한 진짜 소스"라는 원칙을 세우게 됐다 (npx astryx component <Name>, npx astryx template <Name>으로 실제 렌더링되는 소스를 직접 확인).
즉 이 프로젝트의 본질은 "그림을 예쁘게 그리는 것"이 아니라, 끊임없이 변하는 코드 진실을 피그마라는 별도 시스템에 지속적으로, 정확하게, 안전하게 동기화하는 것이었다.

Problem — AI에게 그냥 맡기면 벌어지는 일들

프로젝트 초기(2026년 7월 초)에는 AI 에이전트가 피그마 MCP를 통해 비교적 자유롭게 파일을 편집했다. 그 결과 반복적으로 나타난 문제 유형은 대략 이랬다.
1. 없는 걸 있는 것처럼 그린다. 공식 컴포넌트가 아닌데도 임의로 Icon/check, Icon/search 같은 개별 아이콘 컴포넌트나 MobileNavToggle 같은 걸 "공식 컴포넌트"처럼 문서화한 사례가 있었다. 반대로 실제로는 존재하는 BaseTypeahead, Field, LayoutHeader/Content/Footer/Panel, ToggleButtonGroup 등 15개 공식 컴포넌트가 통째로 빠져 있기도 했다. 페이지 구조도 실제 공식 문서 내비게이션과 무관하게 Navigation (Tabs · Breadcrumbs · Pagination)처럼 임의로 묶여 있었다.
2. 하드코딩된 값이 토큰을 대체한다. 한 번의 컴포넌트/스타일 감사에서만 메인 컴포넌트에 걸린 바인딩되지 않은 SOLID 색상 1,640개, 바인딩되지 않은 코너 반경 772개가 발견됐다. 예컨대 AvatarStatusDot의 점 색상은 실제 토큰 --color-success #0D8626이 아니라 임의의 #14a05a 같은 값이 박혀 있었다. 이런 값들은 라이트 모드에서는 눈에 잘 안 띄지만, 다크 모드 전환이나 브랜드 테마 교체 시 바로 깨진다.
3. 넓은 범위의 쓰기가 이전 작업을 조용히 지운다. 피그마 MCP로 여러 페이지를 한 번에 건드리는 작업을 하다가, Button·Badge·TextInput 페이지를 이미 동기화해둔 상태에서 Pagination·Collapsible·Dialog를 작업했더니 이전에 만들어둔 프레임들이 사라진 사고가 있었다. AI가 "지금 맡은 일"만 보고 판단하면, 이미 완료된 다른 작업의 존재를 놓치기 쉽다.
4. 검증이 스스로를 속인다. 버튼 하나, 예제 하나가 "그럴듯하게" 보이는 것과 "공식 소스와 실제로 일치하는 것"은 다르다. 에이전트가 자기 작업 결과를 스스로 "완료"라고 판단하게 두면, 대표 예제 하나만 맞으면 나머지 variant도 맞다고 착각하는 식의 확증 편향이 반복됐다.
이 네 가지는 서로 다른 문제처럼 보이지만 사실 하나의 원인으로 수렴한다 — "이번 작업 하나만 보는 에이전트"에게 프로젝트 전체의 진실성 검증까지 맡길 수는 없다는 것.

Approach — 자동화 제어 계층 설계

그래서 우리는 "에이전트가 알아서 잘 하겠지"에서 "승인되지 않은 변경은 물리적으로 실행될 수 없다"로 설계 철학을 바꿨다. automation/ 아래에 만든 제어 계층의 핵심 아이디어는 세 가지다.

1. 역할 분리 (Reader / Editor / Verifier)

한 에이전트가 읽고, 고치고, 검증까지 다 하면 자기 실수를 자기가 놓치기 쉽다. 그래서 역할별 계약(automation/prompts/)을 완전히 분리했다.
  • Coordinator — 체크포인트와 라우팅 문서를 읽고, 불변의 실행 디렉터리(automation/runs/<run-id>/)를 만들고 diff/plan을 생성한 뒤 승인 대기 상태로 멈춘다.
  • Figma Reader — 읽기만 한다. "불필요하게 생성·편집·삭제·이름 변경을 하지 않는다"가 계약의 첫 줄이다.
  • Figma Editor승인된 operation ID만 실행할 수 있다. 작업 중 우연히 발견한 다른 문제를 "김에 같이" 고치는 것(free-form cleanup)은 계약상 명시적으로 금지된다. 게시(publish)도, 성공 선언도 스스로 할 수 없다.
  • Verifier — Editor의 판단을 재사용하지 않고 피그마를 처음부터 다시 읽어 독립적으로 검증한다. 계약에는 이렇게 적혀 있다: "검증자는 실패를 스스로 복구할 수 없다. 실패는 새로운 diff/승인 사이클을 시작시킨다."

2. 승인은 "지금 이 상태"에만 유효한 해시다

승인(approval.json)은 다음 값들을 하나로 묶어 바인딩한다 — automation/schemas/approval.schema.json 기준:
  • planHash — 지금 승인하는 계획 자체의 해시
  • figmaBeforeHash — 승인 시점 피그마 상태의 해시
  • sourceVersion — Astryx 버전
  • operationIds — 실행이 허용된 정확한 작업 목록
  • acknowledgedHighRiskIds, expiresAt — 고위험 작업 승인 여부와 24시간 만료
핵심은 이 다섯 가지 중 하나라도 바뀌면 승인 자체가 무효화된다는 점이다. validate-approval.mjs가 실행 직전에 이 값을 현재 상태와 다시 대조하고, 조금이라도 어긋나면 실행을 막는다. 계획이 바뀌었거나, 그 사이 누군가 피그마를 손댔거나, 패키지 버전이 올라갔거나, 승인이 만료됐다면 — 새로 diff/plan/approval 사이클을 처음부터 다시 밟아야 한다.
작업 위험도도 등급화했다 (automation/config/risk-rules.yaml).
json
{
"UPDATE_DOCUMENTATION": "low",
"REPLACE_ASSET": "medium",
"ADD_VARIANT": "medium",
"ADD_COMPONENT": "high",
"REMOVE_COMPONENT": "high",
"RENAME_COMPONENT": "high"
}
그리고 아예 자동화 대상에서 제외한 작업도 명시했다: PUBLISH_LIBRARY(라이브러리 게시), INVENT_PUBLIC_API(없는 공식 API 발명), RESOLVE_SOURCE_CONFLICT(소스 충돌의 임의 해석). 이 세 가지는 사람이 직접 판단해야 하는 영역이라고 선을 그었다.

3. 검증은 3단계, 그리고 실패해도 괜찮게 설계한다

automation/config/library.yaml에 정의된 검증 순서는 structural → semantic → screenshot이다. 구조적으로 깨진 인스턴스가 있는지, 프롭/variant가 공식 스펙과 의미적으로 일치하는지 먼저 걸러내고, 마지막에야 스크린샷 비교로 넘어간다 — 스크린샷 렌더링과 비교는 비용이 크기 때문에 앞 단계에서 이미 실패한 작업까지 굳이 찍지 않는다.

Solution — 실제로 부딪힌 사고와 그 처리 과정: Chat 페이지 이야기

이론은 늘 깔끔하다. 실제로 이 시스템이 진가를 발휘한 건 Chat 페이지 복구 작업에서였다. 2026-07-19부터 07-20까지 이틀에 걸쳐 약 19개의 실행 기록(automation/runs/2026-07-19-chat-*, 2026-07-20-chat-*, v1~v12, -recovery, -retry2 등)이 쌓인, 이 프로젝트에서 가장 굴곡이 많았던 작업이다.
1차 감사에서 FAIL 판정. 별도의 읽기 전용 감사(2026-07-19-chat-page-border-asset-audit.md)가 Chat 컴포넌트들을 다시 살펴보니:
  • 공식 ChatComposer--shadow-low/--shadow-med 그림자만 쓰고 테두리가 없는데, 피그마에는 존재하지 않는 두꺼운 회색 테두리(ring)가 그려져 있었다.
  • 공식 첨부파일 썸네일 5장(illustrative-vertical-1..5.png)이 실제 이미지 대신 단색 블록으로 대체돼 있었다.
  • 시스템 메시지 아이콘 자리에 진짜 아이콘 대신 빈 원(ellipse)만 있었다.
공식 Chat 첨부파일 드로어 — 실제 이미지 자산이 복구된 상태
ChatComposerDrawer — Attachments: 색 블록 placeholder였던 자리에 공식 이미지 5장이 810×1440 원본 그대로 들어가고, 44×44로 클리핑된 상태.
복구 시도 중 실제 인프라 장애를 만났다. 여기서 흥미로운 지점이 나온다. 5장의 이미지를 업로드하는 작업(chat-restore-attachment-images)을 승인까지 받아 실행하던 중, 피그마 커넥터의 OAuth 토큰이 갱신 도중 만료되면서 업로드 요청 5건이 전부 HTTP 401 token_revoked로 실패했다. 이때 실패 기록(automation/runs/2026-07-20-chat-border-assets-repair-recovery/failure.json)은 이렇게 남아 있다.
json
{
"reason": "Figma connector OAuth token was revoked while requesting node-targeted upload URLs; all five requests returned HTTP 401 token_revoked.",
"planHash": "cf20c0dcf896159286cc64e4b4aaaffc7701518db9026c21b9c4fffc7eb7183d",
"figmaMutationsApplied": 0,
"uploadUrlsIssued": 0,
"notStartedOperations": [
"chat-restore-system-message-icons",
"chat-add-dictation-in-composer-example"
],
"requiredResolution": "Reconnect or reauthorize the Figma connector..."
}
중요한 건 여기서 에이전트가 "업로드가 안 되니 figma.createImage()로 우회해서라도 채워 넣자"는 식으로 타협하지 않았다는 점이다. 프로토콜상 금지된 우회로였고, 실제로 figmaMutationsApplied: 0으로 아무 것도 건드리지 않은 채 실패를 있는 그대로 기록하고 멈췄다. 커넥터를 재인증한 뒤에야 (-retry2 실행) 실제로 이미지가 올라갔다.
시스템 메시지 아이콘이 실제 Heroicons로 교체된 상태
ChatSystemMessage — Icon: 빈 원이었던 자리가 실제 Heroicons outline 벡터(UserPlusIcon, LockClosedIcon, SparklesIcon, ShieldCheckIcon)로 교체됐다.
검증 단계 자체가 두 번 더 작업을 되돌렸다. 이후 진행된 메시지 레이아웃/간격 복구 작업(2026-07-20-chat-message-layout-spacing-repair.md, v8~v11)에서는 v9가 "예제 4개의 콘텐츠가 컨테이너를 벗어난다"는 이유로, v10이 "Full Featured 예제가 12px 부족하다"는 이유로 독립 검증 단계에서 자체적으로 반려됐다. v11에 이르러서야 시각 점수 94/100으로 통과했다. 검증자가 편집자의 "다 됐다"는 주장을 그대로 믿지 않고 매번 처음부터 다시 확인했기 때문에 가능한 일이었다.
Chat 공식 예제 페이지 최종 상태
여러 차례의 반복 끝에 도달한 Chat 공식 예제 페이지 — Composer, Dictation, Layout, ScrollButton 등 모든 예제가 실제 소스와 일치하는 최종본.
최종 폴리시된 ChatComposer — Attachments
최종적으로 도달한 ChatComposer — Attachments 상태: 가짜 테두리 없이 Shadow/Low 하나로만 표현되는 공식 elevation.
이 사이클은 총 12번의 버전(v1~v12)과 3번의 recovery/retry를 거쳐 automation/runs/2026-07-20-chat-composer-layout-asset-rebuild-v12에서 최종 검증(817개 인스턴스, 0개 broken instance, 시각 점수 96/100)으로 마무리됐다. 느리게 보일 수 있지만, 우리가 원한 건 "빠르게 끝내는 것"이 아니라 "끝났다고 말할 수 있는 상태가 실제로 맞는 것"이었다.

Impact — 결과

규모. 최종적으로 피그마 파일은 81개 페이지, 2,885개 컴포넌트(공식 Astryx 미러 763개 + 사용자 제공 Material 아이콘 2,122개), 91개 컴포넌트 세트, 292개 변수(6개 컬렉션), 텍스트 스타일 14개, 이펙트 스타일 8개를 담게 됐다. 공식 페이지 템플릿 43개는 43/43 정확히 매칭되며, broken instance는 0개다.
구체적으로 고쳐진 것들.
  • Table 컴포넌트 72개 variant의 행 높이가 실제 값(compact 28 / balanced 36 / spacious 44)과 다르게 32/44/56으로 하드코딩돼 있던 것을 교정.
  • MultiSelector 트리거 54개 variant의 높이가 32/40/48이었던 것을 실제 --size-element 값인 28/32/36으로 교정.
  • 색상 테마 시스템을 새로 구축 — Theme 변수 컬렉션에 Core 포함 8개 팔레트(Neutral, Butter, Chocolate, Gothic, Matcha, Stone, Y2K), 158개 프리미티브 × 8모드 = 1,264개 색상값을 공식 값과 100% 일치시켰고, 기존 79개 Color 변수는 ID/스코프를 그대로 보존한 채 별칭(alias)으로 라우팅.
  • Material 아이콘 2,122개를 원격 참조에서 로컬 컴포넌트로 전환하고 전부 color/icon/primary 토큰에 바인딩.
Avatar Group 공식 예제 — 프롭 그대로, 실제 사진 자산 그대로
공식 문서의 Usage 설명, variant, 실제 예시 사진까지 그대로 옮겨진 Avatar — Group 예제. "그럴듯한 목업"이 아니라 소스와 대조 가능한 사본이라는 게 이 프로젝트 전체의 기준선이다.
자동화 자체의 효율. 처음에는 파일 하나 바꾸는 데도 81페이지 전체를 매번 다시 읽는 식이었고, 실행 기록만 73MB, 그중 44MB가 완전히 동일한 official.json 13벌의 중복이었다. 이후 해시 기반 콘텐츠 캐시, 검증된 베이스라인에 병합하는 스코프드 스냅샷(Merkle root 비교), 스크린샷 해시 재사용을 도입해 불필요한 재수집을 없앴다. 전체 라이브러리 재읽기는 버전 변경·파운데이션 변경·게시 전 점검 같은 명확한 트리거가 있을 때만 실행되도록 library.yaml에 못박았다. 자동화 코드 자체의 단위/통합 테스트는 19개가 통과 상태로 유지되고 있다.
여전히 사람이 하는 일. 라이브러리 게시(publish)는 의도적으로 자동화하지 않았다. 팀 라이브러리에 실제로 반영되어 다운스트림 소비자에게 전파되는 순간은 늘 사람이 마지막으로 확인하고 누른다.

마무리

이 프로젝트에서 가장 크게 배운 건 "AI 에이전트가 실수한다"는 사실 자체가 아니라, 그 실수를 전제로 시스템을 설계해야 한다는 것이었다. 승인은 지금 이 순간의 상태에만 유효한 해시로 묶고, 읽기·쓰기·검증의 권한을 분리하고, 검증자가 편집자의 주장을 믿지 않고 매번 처음부터 다시 확인하게 만드는 것. 이 세 가지 원칙만 지키면, 실패는 "사고"가 아니라 "다음 승인 사이클로 넘어가는 정상적인 신호"가 된다.
Chat 페이지 하나를 완전히 소스와 일치시키는 데 19번의 실행과 이틀이 걸렸다. 느리다고 느낄 수도 있지만, 그 19번 각각이 "이번엔 진짜 맞는지" 스스로 의심하고 검증한 결과라는 점에서, 우리는 이 속도를 기꺼이 받아들이기로 했다.
← 전체 프로젝트로 돌아가기
AI 에이전트에게 피그마 라이브러리를 맡긴다는 것 — Astryx 디자인 시스템 미러링기
AI 에이전트에게 피그마 라이브러리를 맡긴다는 것 — Astryx 디자인 시스템 미러링기