다크 모드
디자인 시스템에 MCP 한 스푼
작성자: 에디(최종선) | 작성일: 2026.08.12
들어가며
안녕하세요. 카카오모빌리티 웹FE개발팀에서 프론트엔드 개발을 맡고 있는 에디입니다. 카카오모빌리티에서는 새로운 디자인 시스템 MODI를 만들어 가고 있습니다. 시스템의 모습이 갖춰지면서 Figma 시안을 MODI 컴포넌트와 토큰으로 구현하는 과정에도 AI 에이전트를 활용해 보기로 했습니다. Figma가 이미 Dev Mode MCP 서버를 제공하고 있었기 때문에, 에이전트가 디자인 콘텍스트와 MODI 문서를 함께 읽으면 가능할 것처럼 보였습니다. 하지만 범용적인 React·Tailwind 형태의 응답을 MODI 코드로 옮기는 과정에는 별도의 판단이 필요했습니다. 에이전트가 토큰 이름을 잘못 변환하거나, export가 된 속성을 그대로 사용했지만, 캔버스와 다른 결과가 만들어지는 경우도 있었습니다. 문서를 더 많이 제공하는 것만으로는 이 문제를 안정적으로 해결하기 어려웠습니다. 저희는 공식 Figma MCP를 대체하는 대신, MODI의 규칙을 더하는 변환 계층을 만들었습니다. 이 글에서는 프록시 아키텍처, 결정형 변환, 도구 스키마, 검증 절차를 어떻게 설계했는지 살펴보고, 마지막으로 결정형 로직과 에이전트의 행동을 각각 어떻게 테스트했는지 소개합니다.
1. 공식 Figma MCP에 디자인 시스템 콘텍스트를 더하다
간극 1 — 응답이 Tailwind로 온다
Figma Dev Mode MCP의 get_design_context를 호출하면, 선택한 노드가 이런 모습으로 돌아옵니다.
html
<div
className="flex flex-col gap-[var(--spacing/24,24px)]"
data-name="Main Pages Section"
data-node-id="646:28242"
>
...
</div>Figma Dev Mode MCP는 선택한 노드를 React와 Tailwind 형태로 반환합니다. 범용 도구가 특정 팀의 컴포넌트 체계를 알 수 없다는 점을 고려하면 자연스러운 형식입니다. 다만 MODI 컴포넌트와 토큰으로 구현하는 저희 환경에서는 이 결과를 그대로 사용할 수 없었습니다. 저희에게 필요했던 것은 클래스 문자열 자체보다 노드의 id·이름·계층과 연결된 디자인 변수였습니다. 처음에는 Figma REST API를 이용해 이 정보만 남긴 JSON을 직접 만들려고 했습니다. Tailwind 문법을 제거하고 노드 구조와 스타일 참조만 반환하는 방식이었습니다. 그러나 이 접근만으로는 사용자가 선택한 노드의 맥락을 안정적으로 복원하기 어려웠습니다.
간극 2 — 클릭하는 위치에 따라 node-id 가 달라진다
에이전트에게 “이 영역을 구현해 주세요”라고 요청했지만, 지정한 영역(아래 빨간색 박스)이 아닌 엉뚱한 영역(보라색 박스)의 코드가 생성되는 일이 반복되었습니다.
원인을 추적해 보니, 저희가 사용한 흐름에서는 URL의 node-id만으로 사용자가 기대한 선택 맥락을 복원하기 어려운 경우가 있었습니다. 특히 외부 라이브러리의 컴포넌트 인스턴스로 구성된 프레임에서는 캔버스에서 보이는 논리적인 묶음과 파일 내부의 노드 트리가 다르게 해석될 수 있었습니다.
Figma REST API를 이용한 접근과 node-id를 복사하는 플러그인도 검토했습니다. 하지만 REST API의 응답만으로는 캔버스의 선택 맥락을 충분히 복원하기 어려웠고, 플러그인 방식은 사용자가 패널을 열어 직접 복사해야 했습니다. 저희가 원한 백그라운드 자동화 흐름에는 적합하지 않았습니다. 반면 Figma Desktop MCP는 사용자가 보고 있는 캔버스의 맥락과 연결되어 있었습니다. 따라서 이 부분은 공식 MCP에 맡기고, 응답을 MODI에 맞게 변환하는 계층만 추가하기로 했습니다.
공식 MCP를 유지하고 변환 계층을 추가하다
방향을 바꿨습니다. Figma Desktop MCP(로컬 127.0.0.1:3845)를 버리지도 않고, 그대로 노출하지도 않고, 감싸서 보강했습니다.
이 미들웨어에서 업스트림 도구를 다루는 방식은 세 가지입니다.
| 방식 | 대상 | 하는 일 |
|---|---|---|
| 순수 프록시 | get_screenshot | 그대로 전달. 우리가 보탤 게 없는 도구는 건드리지 않는다 |
| 보강 래퍼 | get_code_connect_map → get_code_connect_map_with_modi_docs | Figma의 컴포넌트 매핑 결과에 각 React 컴포넌트의 MODI 공식 문서를 doc 필드로 병렬 조회해 붙인다 |
| 조립 도구 | get_design_context + get_variable_defs → get_figma_modi_*_context | 두 업스트림 응답을 병렬로 받아, 토큰 매핑(2장)과 구현 가이드·검수(4장)를 얹어 하나의 응답으로 조립한다 |
2. 토큰 변환은 왜 프롬프트가 아니라 코드인가 — 결정형과 비결정형의 경계
Figma variables를 그대로 쓸 수 없는 이유
Figma는 변수마다 Web·Android·iOS 플랫폼별 code syntax를 하나씩 등록할 수 있습니다. 하나의 토큰 체계만 사용하는 환경이라면 이 기능만으로 충분할 수 있습니다. 하지만 MODI에서는 하나의 Figma 변수가 스택과 토큰 시스템에 따라 여러 코드 표현으로 바뀌어야 했습니다.
| 예시 | 예시 | 필요한 처리 |
|---|---|---|
| 같은 Web 플랫폼에서도 스택에 따라 표현이 다름 | Text/primary → React의 colors.text.primary, Web의 var(--modi-color-text-primary) | framework에 따른 변환 |
| MODI와 레거시 토큰 패키지가 공존함 | Text/primary와 Neutral1의 import 경로가 다름 | 토큰 분류와 패키지 분기 |
| 변수명에 전처리와 제외 규칙이 필요함 | ✶ 접두사, 역할 주석, 매핑하지 않을 서브 브랜드 변수 | 정규화와 필터링 |
React와 Web은 모두 Figma code syntax의 Web 항목을 사용하므로 어느 한쪽을 등록해도 다른 스택에는 별도의 변환이 필요합니다. 또한 변수의 코드 이름만으로는 사용할 패키지와 import 경로, 제외 대상까지 표현할 수 없었습니다. 결국 저희에게 필요한 것은 변수 하나를 코드 이름 하나로 바꾸는 단순한 1:1 매핑이 아니었습니다. 이 변환을 에이전트와 MCP 서버 중 어디에서 담당할지 결정해야 했습니다.
가장 쉬운 방법은 프롬프트에 쓰는 것이었지만
가장 빠른 방법은 변환 규칙을 프롬프트에 적는 것이었습니다. 하지만 LLM은 동일한 입력도 brandPrimaryFixedMiddle1이나 brand.primaryFixedMiddle_1처럼 다르게 변환할 수 있고, 존재하지 않는 토큰을 만들기도 합니다. 규칙 준수 여부를 일반적인 유닛 테스트로 보장하기 어렵고, 호출할 때마다 변환 규칙을 콘텍스트에 포함해야 한다는 비용도 있었습니다. 토큰처럼 정답이 하나인 영역을 프롬프트에 남겨둘 이유가 없었습니다.
그래서 변환을 전부 코드로 내렸다
토큰 변환은 MCP 코드가 담당합니다. 매핑 도구는 구현할 노드의 nodeId와 대상 스택을 나타내는 framework(react 또는 web)를 입력으로 받습니다. MCP는 해당 노드에 사용된 Figma 변수를 가져와 간격·색상·타이포그래피 등의 유형으로 분류한 뒤, 선택한 스택에 맞는 토큰 경로와 import 문을 반환합니다. 에이전트가 변환 규칙을 직접 적용하는 것이 아니라 완성된 결과를 받는 구조입니다. 예를 들어 framework: "react"로 호출하면 다음과 같은 결과가 반환됩니다.
json
{
"figmaVariable": "Text/primary",
"token": "colors.text.primary",
"value": "#1A1A1A",
"import": "@kakaomobility/modi-react"
}같은 변수를 framework: "web"으로 호출하면 토큰은 var(--modi-color-text-primary), 패키지는 @kakaomobility/modi-web으로 바뀝니다. 어느 표현을 사용할지 에이전트가 판단하는 대신, framework 인자에 따라 MCP가 결정적으로 변환합니다. 변환한 토큰이 실제로 존재하는지도 확인합니다. MODI 토큰은 Figma variables에 등록되어 있으므로 get_variable_defs로 확인할 수 있습니다. 반면 Neutral1, Body1_16B 같은 레거시 토큰은 Figma variable이 아니므로, MCP를 빌드할 때 실제 토큰 패키지가 export 하는 이름을 추출해 검증 목록을 만듭니다. 매핑 도구는 이 목록에서 확인된 토큰만 반환합니다. 대응하는 토큰을 찾지 못한 변수는 unmapped 항목으로 분리합니다. 에이전트가 존재하지 않는 토큰을 추측하는 대신, 원래 값이나 fallback 값을 사용하도록 안내하기 위해서입니다. 토큰 패키지가 업데이트되면 검증 목록도 다시 생성할 수 있습니다. 레거시 토큰의 실제 색상 값도 같은 패키지를 기준으로 확인합니다. d Neutral1과 같은 다크 모드 변수는 Figma가 반환한 값과 코드 토큰의 값이 다른 경우가 있어, 빌드 시점에 패키지에서 라이트·다크 값을 추출합니다. 두 값이 다르면 에이전트에는 실제 코드에서 사용하는 패키지의 값을 반환합니다. 정리하면 Figma에서는 어떤 변수가 사용되었는지 확인하고, MCP는 해당 변수를 대상 스택의 토큰으로 변환한 뒤 실제 사용 가능한 이름과 값인지 검증합니다. 정답이 정해진 변환과 검증은 LLM의 판단에 맡기지 않고 코드가 담당하도록 한 것입니다.
아이콘은 결정형으로, 이미지는 판단 보류로
토큰 외에도 화면을 구성하는 리소스가 두 부류 더 있습니다. 아이콘과 이미지이며, 저희는 이 둘을 정반대로 처리합니다. 같은 경계 원칙에서 비롯된 결정입니다. 아이콘은 정답이 하나인 결정형입니다. Figma에서 아이콘은 data-name="24 / ic_24_x"와 같은 슬롯 프레임 패턴으로 전달되며, 이 이름은 아이콘 패키지의 <I24X /> 컴포넌트와 규칙에 따라 1:1로 대응합니다. 따라서 이름 변환은 코드 규칙으로 확정하되, 토큰과 마찬가지로 존재 여부를 검증합니다. 규칙에 따라 생성한 컴포넌트 이름이 실제 패키지에는 존재하지 않을 수 있기 때문입니다(신규 아이콘이 아직 패키지에 포함되지 않은 경우 등). 검증 기준은 문서가 아니라 실제로 설치된 패키지의 export 목록입니다. 목록에 없으면 비슷한 이름의 다른 아이콘으로 대체하는 것을 금지하고 TODO 주석 + 플레이스홀더로 남깁니다. 반면 일러스트·3D·심볼과 같은 이미지 에셋(img_*, symbol_*, illust_* 등)은 자동 매핑을 아예 금지했습니다. 아이콘과 달리 규칙으로 도출할 수 있는 정답이 없기 때문입니다. 해당 에셋을 어떤 CDN 경로에 올릴지, 어떤 이미지 시스템을 사용할지는 사람이 내려야 하는 결정입니다. 정답이 없는 영역에서 매핑을 허용하면 가장 그럴듯한 오답이 나옵니다. 따라서 기존 프로젝트에 이미지 유틸이나 CDN 매핑이 이미 있더라도 예외 없이 TODO 주석 + 플레이스홀더 div로 남깁니다. 4장의 시각 비교에서도 이 영역은 "불일치가 정상"인 영역으로 별도 취급합니다. 아이콘은 정답이 있으므로 코드가 확정하고, 이미지는 정답이 없으므로 추측 자체를 차단한 것입니다.
정답이 있는 일은 코드로, 판단이 필요한 일만 LLM에게
이 결정을 일반화해 보면, 이 프로젝트 전체를 관통하는 원칙이 하나 나옵니다.
| 작업의 성격 | 담당 | 프로젝트 예시 |
|---|---|---|
| 결정형 | 코드 | 변수→토큰 매핑, 아이콘 이름 매핑(24 / ic_24_x → <I24X />), import 경로 |
| 비결정형 | LLM + 가이드 | 컴포넌트 조합, 레이아웃 해석, 기존 코드와의 통합 |
| 비결정형이되 검증 가능 | LLM + 검수 도구 | 구현 후 자기 검수 (4장) |
저희는 MCP 서버의 역할을 많은 도구를 노출하는 것이 아니라, LLM의 자유도를 판단이 반드시 필요한 영역에만 남기고 나머지는 결정형으로 회수하는 것이라고 정의했습니다. 이후의 장들도 결국 이 경계의 한쪽을 다룹니다. 3장은 비결정형 영역의 입구를 좁히는 방법, 4장은 출구를 검증하는 방법, 5장은 두 영역을 각각 테스트하는 방법을 설명합니다.
3. 도구 스키마가 곧 프롬프트다 — framework 인자 하나로 가이드를 반으로 줄인 이야기
가이드를 둘 다 실었더니 생긴 일
초기 버전에서는 구현 콘텍스트 응답에 앞서 언급한 두 스택의 가이드를 모두 이어 붙였습니다.
(응답)
가이드 modi-web ← CSS 변수, 유틸 클래스 기준
가이드 modi-react ← JS 객체 토큰 기준결과는 혼선이었습니다. React 프로젝트를 구현하는 에이전트가 웹(vanilla) 쪽 규칙을 혼용하면서 토큰 표기(CSS 변수 vs JS 객체)가 뒤섞였습니다. 에이전트로서는 하나의 응답 안에 서로 모순되는 두 권위가 들어 있으므로, 무엇을 따를지 매번 새로 "판단"해야 했던 것입니다. 결정형이어야 할 것, 즉 "어느 스택인가"라는 문제가 비결정형 영역으로 새어 들어간 사례였습니다.
판단을 인자 한 칸으로 옮기다
고치는 방법은 단순했습니다. 스택 선택을 응답에서 빼서 도구 호출 인자로 옮겼습니다.
json
{
"name": "get_figma_modi_implementation_context",
"inputSchema": {
"properties": {
"nodeId": { "type": "string" },
"framework": {
"enum": ["react", "web"],
"description": "react: React/TypeScript + modi-react. web: Vanilla/Vue/Svelte + modi-web."
}
}
}
}에이전트는 대화 맥락(레포의 스택)을 보고 framework를 한 번 고르고, 서버는 그 값으로 분기해서 해당 스택의 가이드와 검수만 붙입니다. 응답 길이는 절반이 됐고, 서로 다른 스택의 가이드가 한 응답에 섞일 가능성을 제거했습니다.
스키마의 세 요소는 각각 프롬프트다
이 경험을 통해 한 가지 관점을 정리했습니다. 에이전트가 도구를 사용할 때 실제로 확인하는 것은 서버 구현이 아니라 도구 설명과 입력 JSON 스키마뿐입니다. 따라서 스키마의 각 요소가 사실상 프롬프트의 역할을 나누어 맡고 있는 셈입니다.
- 필드 목록은 호출 템플릿입니다.
nodeId와framework만 받는다는 것 자체가 호출 JSON의 모양을 고정합니다. enum은 허용값의 전부입니다.["react", "web"]이면 그 밖의 값은 스키마 단계에서 끝납니다. "뭘 넣을 수 있지?"라는 판단이 사라집니다.description은 값을 고르는 규칙입니다. "react는 React/TypeScript + modi-react"라는 한 줄이, 가이드 원문 수십 줄이 하던 스택 판별을 대신합니다.
프롬프트 엔지니어링이라고 하면 지시문을 길고 정교하게 작성하는 방식을 떠올리기 쉽지만, MCP 도구에서는 스키마를 좁히는 것이 지시문을 늘리는 것보다 강력했습니다. 지시문은 어길 수 있지만, 스키마는 어길 수 없기 때문입니다. 물론 스키마가 통제하는 범위는 "호출된 도구의 입력"까지입니다. 에이전트가 처음부터 엉뚱한 도구를 선택하는 문제는 스키마 바깥에 있으며, 해당 영역은 5장의 행동 평가에서 별도로 다룹니다. 같은 원리를 문서 검색에도 적용했습니다. 문서 조회 도구 get_modi_docs는 query(주제)와 framework를 받아 해당 스택의 공식 문서만 선별해 반환합니다. 사용자가 "modi-react 쪽 Text 문서"처럼 패키지명을 일일이 명시할 필요가 없습니다. 검색 자체에는 사람의 표현과 문서 목록의 표현이 다르다는 또 다른 문제가 있었습니다. 사용자는 "디자인 토큰"으로 질문하지만, 문서 제목은 "토큰"일 수 있으며, buton과 같은 오타나 btn과 같은 축약어가 입력되기도 합니다. 따라서 정확히 일치하는 결과가 없으면 편집 거리 기반의 퍼지 매칭으로 검색 범위를 한 번 더 넓히는 2단계 검색 방식을 적용했습니다. 이 역시 "비슷한 말도 찾아봐"라는 프롬프트가 아니라 코드로 해결한 2장 원칙의 연장입니다. 여기에 문서를 누가 소유하는지에 대한 결정이 하나 더해집니다. 이 도구가 반환하는 문서의 운영 주체는 MCP가 아니라 디자인 시스템입니다. MODI 문서 사이트가 llms.txt 인덱스와 마크다운 원문을 배포하며, MCP는 호출 시점에 해당 인덱스를 가져와 검색하고 원문을 조회할 뿐 문서 사본을 보유하지 않습니다. 문서가 갱신되면 MCP를 재배포하지 않아도 다음 호출부터 새로운 내용이 반환되며, 1장의 보강 래퍼가 컴포넌트 매핑에 첨부하는 문서도 같은 경로에서 가져옵니다. 2장에서 토큰 목록은 빌드할 때 코드에 박았는데, 문서는 반대로 런타임에 조회합니다. 얼핏 상반된 두 선택이 공존하는 건 둘의 역할이 다르기 때문입니다. 토큰 목록은 환각을 차단하는 검증 기준이므로 배포된 패키지와의 일치가 우선이며, 문서는 LLM이 참고할 판단 자료이므로 최신성이 우선입니다. "이 정보의 권위를 어디에 두는가"라는 동일한 질문에 대한 두 가지 답인 셈입니다. 이 구조는 향후 디자인 시스템의 사내 지식베이스로도 확장할 수 있습니다. 컴포넌트 문서뿐 아니라 마이그레이션 가이드, 사용 정책과 실제 적용 사례를 같은 인터페이스로 조회하게 만드는 것입니다. 각 지식은 기존 운영 주체가 관리하고, MCP는 작업에 필요한 내용만 가져온다는 원칙을 유지하면 문서의 중복 관리 없이 에이전트가 참고할 수 있는 지식의 범위를 넓힐 수 있습니다.
4. text-align: right의 미스터리 — 생성이 아니라 검증을 시스템화하다
Figma에서는 분명 맞아 보였는데…
토큰 매핑과 가이드를 다 갖추고 나서도 이런 일이 계속 생겼습니다. Figma 시안은 왼쪽 정렬로 보이는데,
구현 결과는 자꾸 오른쪽으로 치우쳤습니다.
CSS를 열어 보니 원인이 있었습니다.
css
color: var(--Text-tertiary, var(--Text-tertiary, #666));
text-align: right; /* Figma 체감과 어긋남 */
font-size: var(--Typo-body_small, 14px);이 사례에서는 Figma의 export 코드에 text-align: right가 포함되어 있었고, 에이전트가 해당 값을 구현에 반영했습니다. 직접적인 원인은 에이전트의 환각이 아니라 export 코드와 캔버스에서 보이는 결과 사이의 차이였습니다.
처음부터 완벽한 생성은 불가능하다는 걸 인정하기
이 사건에서 얻은 교훈은 "더 좋은 프롬프트를 쓰자"가 아니었습니다. 생성은 비결정형 영역이며, 비결정형 영역의 품질은 생성 시점에 보장할 수 없다는 것이었습니다. 사람도 바쁘면 눈으로만 훑고 넘어가기 마련입니다. "잘 확인해"라는 말만으로는 같은 실수가 반복됩니다. 필요한 것은 반복할 수 있는 검증 절차였으며, 저희는 이를 MCP 도구로 만들었습니다.
검수 체크리스트를 도구로 만들다
get_modi_implementation_inspection은 Figma를 호출하지 않고, 프레임워크별 검수 체크리스트를 돌려주는 도구입니다. 체크리스트는 5단계로 구성되어 있습니다.
- 금지 항목 — 근거 없는 시각·모션 임의 추가,
data-node-id등 Figma 메타 속성 잔존 여부 - 토큰 변환 — 매핑 응답의 토큰이 실제로 쓰였는지, 원시 값(
font-size: 14px)이 남지 않았는지 - Figma 일치성 — 수치·텍스트가 export 코드와 일치하는지
- 구조 — 레이아웃과 아이콘 슬롯 크기 규칙
- 시각적 비교 — 에이전트가 브라우저 MCP로 구현 화면을 직접 캡처하고 Figma 스크린샷과 비교 이 도구가 검수를 직접 실행하는 것은 아닙니다. 검수 절차와 보고 형식을 반환하고 에이전트가 이를 수행합니다. 1~4단계에서 실패하면 해당 부분을 수정한 뒤 전체 항목을 다시 확인하며, 이 과정은 최대 3회 반복합니다. 5단계는 앞선 검수 결과를 보고한 뒤 사용자의 동의를 받아 진행합니다. 동의받으면 에이전트가 브라우저 MCP로 구현된 화면에 접근해 스크린샷을 자동으로 캡처하고, Figma의
get_screenshot으로 가져온 캔버스 이미지와 시각적으로 비교합니다. 사용자가 구현 화면을 직접 캡처해 전달할 필요는 없습니다. 검수에서는 캔버스 캡처를 최종적인 시각적 기준으로 두었습니다. export 코드와 구현이 일치하더라도 둘이 같은 왜곡을 포함할 수 있기 때문입니다.
| 권위 | 원천 | 역할 |
|---|---|---|
| 1순위 | 캔버스 캡처 | 디자이너가 실제로 본 픽셀 |
| 2순위 | export 코드 | Figma의 코드 해석 |
| - | 구현 코드 | 검증할 결과물 |
보고에는 단순히 “전반적으로 유사함”이라고 쓰는 대신 행·열 기준선과 텍스트 베이스라인 등 실제로 대조한 항목을 기록하도록 했습니다. 이미지와 일러스트의 TODO 영역은 픽셀 비교에서 제외하지만, 주변 레이아웃과 텍스트 정렬은 계속 검사합니다. 또한 구현 진입점인 get_figma_modi_implementation_context에 디자인 콘텍스트, 토큰 매핑, 가이드와 검수 체크리스트를 함께 포함했습니다. 검수를 별도 선택지가 아니라 구현 흐름의 기본 콘텍스트로 만든 것입니다. 에이전트가 이 절차를 따르는지는 다음 장의 행동 평가로 점검합니다.
5. 이 설계를 어떻게 회귀 테스트하는가
여기까지 네 가지 제어 장치를 살펴보았습니다. 이제 남은 질문은 하나입니다. 이 장치들이 현재뿐만 아니라 코드가 변경된 이후에도 제대로 작동한다는 사실을 어떻게 확인할 수 있을까요? MCP 서버 테스트에는 일반 라이브러리와 다른 어려움이 하나 있습니다. 최종 사용자가 사람이 아니라 에이전트이기 때문에 "서버가 올바른 응답을 제공하는가"와 별개로 "에이전트가 올바른 도구를 올바른 인자로 호출하는가"라는 영역이 존재합니다. 저희는 이 두 영역을 2장의 경계 원칙에 따라 서로 다른 방식으로 테스트합니다.
결정형 영역: fixture 기반 유닛 테스트
토큰 매핑은 순수 함수이므로 일반적인 방식으로 테스트합니다. 실제 Figma 노드에서 get_variable_defs가 반환한 스냅샷을 fixture로 저장해 매퍼에 직접 주입하고, 기대하는 토큰과 명시적으로 대조합니다. 색상·간격·라디우스·타이포·섀도·unmapped 규칙별로 테스트 파일을 나누었으며, 각 fixture에는 출처인 실제 노드 URL을 주석으로 남겼습니다. Figma Desktop 없이도 CI에서 실행되며, 매핑 규칙의 회귀는 이 단계에서 모두 발견됩니다. 빌드 시점에 생성한 토큰 목록(2장)과 조합하면 보장 범위가 명확해집니다. "존재하는 토큰만, 규칙대로"라는 조건은 테스트와 타입으로 증명되는 영역입니다.
비결정형 영역: mcp-tool-call-assert로 도구 선택을 검증한다
반면 "에이전트가 어떤 도구를 고르는가"는 유닛 테스트로 잡을 수 없습니다. 그래서 저희는 사용자 프롬프트와 기대 도구 호출을 짝지은 시나리오 JSON을 만들고, 이를 검증하는 mcp-tool-call-assert 스킬로 평가합니다.
흐름은 이렇습니다. 시나리오마다 user_input(사용자가 실제로 던질 프롬프트)과 expected_tools(불려야 할 도구 이름 + 인자 검증 규칙)를 적어 둡니다. 평가할 때는 시나리오마다 분리된 서브 에이전트에게 그 프롬프트를 넘기고, 에이전트가 실제로 MCP 도구를 호출하게 합니다. 코드를 끝까지 작성하는 게 아니라 "이 요청이면 어떤 도구를, 어떤 인자로 부를 것인가"를 실행해 보는 겁니다. 서브 에이전트가 돌려준 호출 기록을 시나리오의 기대값과 대조해서 시나리오별로 PASS / FAIL을 판정하고, 여러 시나리오는 병렬로 돌립니다.
json
{
"id": "figma-url-design-inquiry",
"user_input": "택시 아이콘 이름이 뭐야?\n@https://www.figma.com/design/...?node-id=37-8588",
"expected_tools": [
{
"name": "get_figma_modi_design_context",
"param_assertions": [
{ "field": "nodeId", "rule": "one_of", "expected": ["37:8588", "37-8588"] },
{ "field": "framework", "rule": "one_of", "expected": ["react", "web"] }
]
}
]
}param_assertions가 단순히 "도구 이름이 맞는가"를 확인하는 수준을 넘어서는 이유가 있습니다. Figma URL의 node-id가 37:8588과 37-8588처럼 다르게 표기되더라도 통과하도록 one_of로 허용 범위를 정의할 수 있으며, query에 "설치"가 포함되었는지 확인하는 것처럼 필드별로 exists, contains 등의 규칙을 적용할 수 있습니다. 도구 설계의 의도가 시나리오에 그대로 반영되는 셈입니다. 핵심 시나리오는 위 예시와 같은 대비 쌍입니다. Figma URL과 함께 "구현해줘"라고 요청하면 합성 진입점(implementation_context)과 get_screenshot이 호출되어야 하며, 같은 URL이라도 "아이콘 이름이 뭐야?"라는 조회성 질문에는 가벼운 design_context만 호출되어야 합니다. 4장에서 우려했던 위험, 즉 에이전트가 가벼운 도구만 호출하고 검수를 건너뛰는 상황을 측정 가능한 시나리오로 전환한 것이 이 스킬입니다. 이 영역의 평가는 유닛 테스트처럼 결정적이지 않습니다. 모델이 바뀌면 결과가 달라질 수 있으므로 통과율로 관리해야 합니다. 그러나 바로 그 점이 핵심입니다. 결정형 영역은 코드와 유닛 테스트로 확실하게 보장하고, 비결정형 영역은 mcp-tool-call-assert로 통계적으로 감시합니다. 2장에서 설명한 결정형/비결정형 경계가 테스트 전략에도 그대로 적용됩니다.
마치며
정리하면 저희가 만든 것은 더 긴 프롬프트가 아니라, LLM이 판단해야 하는 범위를 줄이는 시스템입니다. 공식 Figma MCP의 콘텍스트는 유지하면서 MODI 정보를 보강하고, 토큰과 아이콘처럼 정답이 있는 변환은 코드가 확정했습니다. 도구 스키마로 입력을 좁히고, 구현 이후에는 검수 절차를 기본 흐름에 포함했습니다. 이 구조가 모든 오류를 없애는 것은 아닙니다. 대신 문제가 발생했을 때 디자인 콘텍스트, 변환 규칙, 도구 호출과 구현 결과 중 어느 단계에서 어긋났는지 추적할 수 있게 되었습니다. 공식 MCP를 그대로 사용할지 직접 만들지 고민하는 팀이라면, 기존 MCP에 조직의 콘텍스트를 보강하는 변환 계층도 하나의 선택지가 될 수 있습니다. 아래 영상은 Figma URL과 “구현해 줘”라는 요청 한 문장으로 시작해, 에이전트가 MODI 컴포넌트와 토큰을 사용해 화면을 구현하는 과정을 담았습니다. 읽어주셔서 감사합니다.