본문으로 건너뛰기

AI 퍼블리싱의 일관성 문제, 디자인 시스템 맵으로 해결하기

·14 min read

문제: 같은 디자인인데 AI가 매번 다른 코드를 뱉는다

Figma 시안을 AI(Claude, GPT 등)에게 "이거 코드로 만들어줘"라고 하면 겪는 문제가 있다.

같은 UI를 줬는데 매번 다른 코드가 나온다.

// 첫 번째 시도 — AI가 프로젝트 컴포넌트를 모름
<Badge colorScheme="blue">소진</Badge>
 
// 두 번째 시도 — 컴포넌트는 찾았는데 색상을 추측
<StatusBadge color="green" label="소진" />
 
// 세 번째 시도 — 드디어 맞음
<StatusBadge color="blue" label="소진" />

왜 이런 일이 생길까?

AI는 매 요청마다 처음부터 판단한다. "이 Figma 노드에 어떤 컴포넌트를 쓸까?", "배경색 #e9f2ff는 blue야 green이야?", "import는 어디서 하지?" — 이 질문들을 매번 새로 답한다.

사람이라면 한번 정하면 기억하지만, AI는 기억이 없다. 그래서 같은 디자인을 줘도 결과가 들쭉날쭉하다.

해결 아이디어: 판단 자체를 없앤다

"AI야, 잘 판단해"가 아니라 판단할 필요가 없는 구조를 만들면 된다.

핵심 발상은 이것이다:

사람이 읽는 마크다운 규칙 문서 대신, 스크립트가 읽는 JSON 매핑 파일을 만들자.

이전: Figma 데이터 → AI가 규칙 문서를 읽고 판단 → 코드
이후: Figma 데이터 → 스크립트가 JSON으로 자동 결정 → AI는 조립만 → 코드

AI가 "이 색이 뭘까?" 고민하는 대신, 스크립트가 미리 답을 정해놓는 것이다.

핵심: design-system-map.json

모든 매핑 규칙을 하나의 JSON 파일에 모았다. 프로젝트 루트의 .claude/design-system-map.json이다.

프로젝트/
  .claude/
    design-system-map.json   ← 이 파일이 핵심
  public/
    token.json               ← Figma에서 export한 디자인 토큰
  src/
    components/              ← 프로젝트 컴포넌트들

이 맵 안에는 여러 종류의 매핑이 들어있다. 그중 핵심 5가지를 살펴보면 아래와 같다. (이 외에도 buttonVariantMap, fillsToStatusBadgeByText, semanticComponentRules 등이 자동 생성된다)

1. instanceMap — "이 Figma 컴포넌트 → 이 코드 컴포넌트"

Figma에서는 재사용 가능한 UI 조각을 "컴포넌트 인스턴스(INSTANCE)"라고 부른다. React의 컴포넌트와 같은 개념이다.

문제는 AI가 Figma의 "Status Badge"라는 이름만 보고 어떤 React 컴포넌트를 써야 할지 모른다는 것이다. instanceMap이 이걸 직접 알려준다:

{
  "instanceMap": {
    "Status Badge": {
      "component": "StatusBadge",
      "import": "@/components/status-badge",
      "defaultProps": {},
      "colorResolve": "fillsToStatusBadge"
    },
    "Single Select": {
      "component": "Select",
      "import": "@/components/select",
      "defaultProps": {}
    }
  }
}

Figma에서 "Status Badge"를 만나면 → StatusBadge 컴포넌트를 쓰고, import 경로는 @/components/status-badge다. 고민 끝.

2. fillsToStatusBadge — "이 배경색 → 이 color prop"

StatusBadge 같은 컴포넌트는 color prop으로 색상을 정한다. 그런데 Figma에서 넘어오는 건 #eff7ff 같은 hex 값이다.

AI가 #eff7ff를 보고 "이게 blue일까 green일까?" 추측하는 대신, 미리 정해놓는다:

{
  "fillsToStatusBadge": {
    "#eff7ff": "blue",
    "#e0ffeb": "green",
    "#fef2e3": "yellow",
    "#fff5f5": "red",
    "#f3f4f6": "grey",
    "#fcf1f3": "pink"
  }
}

이 매핑은 어디서 올까? Figma에서 export한 token.json에 답이 있다:

token.json에 accent-blue1: "#eff7ff" 가 있으면
                ↓ 자동으로
design-system-map에 "#eff7ff""blue" 가 생긴다

수작업 없이 토큰 파일에서 자동 추출된다.

3. hexContextMap — "같은 색인데 쓰이는 곳이 다르면?"

같은 #e5e7eb라도 배경에 쓰이면 grey.3이고, 테두리에 쓰이면 border.basic.2다. 실무에서 정말 자주 겪는 문제인데, hexContextMap이 이걸 구분한다:

{
  "hexContextMap": {
    "background": { "#e5e7eb": "background.basic.4" },
    "border": { "#e5e7eb": "border.basic.2" },
    "text": { "#111827": "grey.10", "#6b7280": "grey.6" }
  }
}

"이 hex 값이 어디에 쓰이는가"까지 매핑해놓으면 AI가 맥락을 추측할 필요가 없다.

4. importPaths — "import 경로 추측 금지"

AI가 가장 자주 틀리는 것 중 하나가 import 경로다. @/components/StatusBadge인지, @/components/status-badge인지, @/components/ui/status-badge인지.

전체 목록을 그냥 줘버린다:

{
  "importPaths": {
    "StatusBadge": "@/components/status-badge",
    "Select": "@/components/select",
    "Table": "@/components/table",
    "Box": "@chakra-ui/react/box",
    "Flex": "@chakra-ui/react/flex"
  }
}

5. pageTemplates — "이 페이지는 어떤 유형?"

Figma 화면을 보고 "이건 테이블 페이지다", "이건 대시보드다"를 판별하는 것도 자동화했다. Figma 노드의 children 이름에 특정 키워드가 있으면 페이지 유형이 결정된다:

{
  "pageTemplates": {
    "table": {
      "structure": "Header → Layout → Controls + Table",
      "requiredImports": [
        { "component": "PageHeader", "from": "@/components/page-header" },
        { "component": "Table", "from": "@/components/table" }
      ],
      "indicators": ["Table", "Header Row", "Row", "Pagination"]
    }
  }
}

children에 "Table", "Row", "Pagination"이 보이면 → 테이블 페이지로 확정. 필요한 import 목록까지 한 번에 결정된다.

그 외 자동 생성되는 매핑들

위 5가지 외에도 생성기가 자동으로 만드는 매핑이 3개 더 있다.

fillsToStatusBadgeByText — 텍스트 색상으로 뱃지 색상 결정

앞서 본 fillsToStatusBadge가 배경색(accent-blue1)으로 color를 결정한다면, 이건 텍스트 색(accent-blue2)으로 결정한다. 같은 token.json의 accent 패턴에서 추출되지만 레벨이 다르다:

{ "#0066cc": "blue", "#16a34a": "green", "#dc2626": "red" }

배경이 투명하고 텍스트만 있는 뱃지에서 유용하다.

buttonVariantMap — 버튼 배경색으로 variant/colorPalette 결정

Figma에서 버튼의 배경색이 #3366ff면 primary 버튼인지, #ef4444면 삭제 버튼인지를 자동 판별한다. token.jsonprimary-4, accent-red2, grey-2 등의 시맨틱 토큰에서 추출된다:

{
  "#3366ff": { "variant": "solid", "colorPalette": "primary" },
  "#ef4444": { "variant": "solid", "colorPalette": "red" },
  "#e5e7eb": { "variant": "solid", "colorPalette": "grey" }
}

semanticComponentRules — 프레임워크별 금지 패턴

이건 token.json이 아니라 생성기 스크립트 안에 하드코딩된 상수다. 프레임워크 커뮤니티의 베스트 프랙티스를 강제하는 룰이다:

{
  "forbidden": [
    { "pattern": "<Box display=\"flex\"", "replacement": "<Flex>" },
    { "pattern": "style={{", "replacement": "className=\"...\"" }
  ]
}

package.json에서 감지된 프레임워크에 따라 Chakra/Tailwind/MUI 각각 다른 금지 패턴이 들어간다.

맵 자동 생성

이 맵을 직접 작성할 필요는 없다. token.jsonsrc/components/를 스캔하면 전부 자동 생성된다.

python3 ~/.claude/scripts/init_design_system_map.py --project-root /path/to/project

이 스크립트가 하는 일을 단계별로 보면 이렇다.

Step 1: token.json에서 색상 패턴 추출

디자인 토큰 파일에는 accent-blue1, accent-green2 같은 이름 규칙이 있다. 생성기가 사용하는 숫자의 의미:

  • 1 → 배경색 (연한 색) → fillsToStatusBadge에 저장
  • 2 → 텍스트색 (진한 색) → fillsToStatusBadgeByText에 저장
accent_re = re.compile(r'^accent-(\w+?)(\d)')
for name, hex_val in name_to_hex.items():
    m = accent_re.match(name)
    if m:
        color_name = m.group(1)  # blue, green, yellow...
        level = int(m.group(2))
        if level == 1:
            status_badge_bg[hex_val] = color_name
        elif level == 2:
            status_badge_text[hex_val] = color_name

accent-blue1: #eff7ff"#eff7ff": "blue" 매핑이 자동으로 만들어진다.

Step 2: 시맨틱 토큰 이름으로 용도 분류

토큰 이름 자체에 용도 힌트가 있다. background-basic-1은 배경, border-basic-1은 테두리:

for name, hex_val in name_to_hex.items():
    if name.startswith('background-basic-'):
        bg_map[hex_val] = dot_name
    elif name.startswith('border-basic-'):
        border_map[hex_val] = dot_name
    elif re.match(r'^grey-(\d+)', name):
        num = int(...)
        if num <= 4:   # 밝은 grey → 배경
            bg_map[hex_val] = dot_name
        else:          # 어두운 grey → 텍스트
            text_map[hex_val] = dot_name

Step 3: 컴포넌트 파일 스캔

src/components/ 아래 모든 .tsx 파일에서 export된 컴포넌트 이름과 경로를 수집한다:

NAMED_EXPORT_RE = re.compile(
  r'export\s+(?:default\s+)?(?:function|const)\s+(\w+)'
)

export default StatusBadge{ "StatusBadge": "@/components/status-badge" } 이런 식으로.

Step 4: Figma 이름 ↔ 코드 컴포넌트 매칭

미리 정의된 패턴 테이블을 기반으로, 프로젝트에 실제로 존재하는 컴포넌트만 instanceMap에 추가한다:

FIGMA_INSTANCE_PATTERNS = {
    'Status Badge': 'StatusBadge',
    'Single Select': 'Select',
    'Search Input': 'SearchInput',
}
 
for figma_name, comp_name in FIGMA_INSTANCE_PATTERNS.items():
    if comp_name in import_paths:  # 프로젝트에 있을 때만
        instance_map[figma_name] = { ... }

분석 스크립트에서 어떻게 쓰이나

생성된 맵은 Figma 분석 스크립트(figma_analyze.py)에서 자동으로 사용된다.

분석 스크립트 흐름:
 
1. Figma API로 노드 트리 가져오기
2. token.json으로 색상/텍스트 토큰 역색인 빌드
3. design-system-map.json 로드
4. INSTANCE → 코드 컴포넌트 자동 결정     ← 맵이 결정
5. hex → 컨텍스트별 시맨틱 토큰 자동 결정  ← 맵이 결정
6. 페이지 유형 자동 판별                   ← 맵이 결정
7. 결과 JSON 출력

4~6단계가 맵으로 추가된 부분이다. 전부 AI 토큰 0개로 처리된다. 스크립트가 결정적(deterministic)으로 처리하니까.

resolve 전후 비교

맵 적용 전 — AI가 직접 판단해야 한다:

{
  "name": "Status Badge",
  "type": "INSTANCE",
  "fills": [{ "hex": "#eff7ff" }]
}

맵 적용 후 — 답이 이미 나와 있다:

{
  "name": "Status Badge",
  "type": "INSTANCE",
  "fills": [{ "hex": "#eff7ff" }],
  "resolvedComponent": {
    "component": "StatusBadge",
    "import": "@/components/status-badge",
    "props": { "color": "blue", "label": "소진" },
    "resolvedBy": "instanceMap"
  }
}

AI는 resolvedComponent를 보고 그대로 코드에 넣기만 하면 된다. 판단이 아니라 조립이다.

프레임워크별로 달라지는 것

생성기는 package.json의 dependencies를 보고 UI 프레임워크를 자동 감지한다. Chakra UI, Tailwind CSS, MUI, Ant Design 4종을 지원하며, 프레임워크에 따라 달라지는 설정이 있다:

Chakra UITailwind CSSMUI
기본 import@chakra-ui/react/*@/components/ui/* (shadcn)@mui/material/*
금지 패턴<Box display="flex"><Flex> 사용style={{}}className 사용<div style={{display:"flex"}}<Stack> 사용

다만 색상 매핑은 프레임워크와 무관하다. token.json 구조가 같으면 Chakra든 Tailwind든 동일한 hexContextMap을 쓴다.

전체 아키텍처

글로벌 (모든 프로젝트에서 공유)         프로젝트별 (각 프로젝트 고유)
~/.claude/scripts/                    프로젝트/.claude/
  init_design_system_map.py  ← 생성기  design-system-map.json  ← 결과물
  figma_analyze.py           ← 분석기
  figma_publish.py           ← 오케스트레이터
 
                  프로젝트/public/
                    token.json  ← Figma 디자인 토큰 (입력)
  • 생성기: token.json + 컴포넌트 스캔 → design-system-map.json 생성
  • 분석기: Figma API + token.json + map → 컴포넌트/색상/페이지유형 결정 완료된 JSON 출력
  • 오케스트레이터: 분석 → 자산 추출 → 코드 생성 → 검증까지 전체 파이프라인 실행

정리

원칙설명
AI의 판단 영역 최소화결정할 수 있는 건 스크립트가 결정
token.json이 유일한 입력수작업 매핑 없음
맵은 프로젝트별, 스크립트는 글로벌재사용 극대화
LLM 토큰 0개로 해결 가능한 건 스크립트로비용 절감 + 일관성

결과: 누가, 언제, 어떤 AI 에이전트가 실행해도 같은 디자인 → 같은 코드.