JINAH YEOM

BLOG

디자이너가 만든 프로토타이핑 도구: 정적 시안을 버리고 진짜 컴포넌트로 프로토타입

Figma 시안을 코드 프로토타입으로 옮기는 반복 작업을, 디자인 시스템의 실제 컴포넌트와 토큰으로 자동화한 이야기

매번 똑같은 반복 노동

우리 팀은 11c-design이라는 디자인 시스템을 운영한다. Next.js 16 + shadcn/ui + Tailwind v4 위에 얹은 컴포넌트 라이브러리이고, 색·간격·타이포·반경 같은 값은 전부 토큰으로 관리한다. Token Studio에서 정의한 값을 Style Dictionary로 빌드해 styles/tokens.css로 떨어뜨리고, 코드에서는 하드코딩 없이 var(--token-name) 형태로만 참조한다는 규칙을 지킨다.

/* styles/tokens.css — Style Dictionary가 빌드한 결과물 */
:root {
  --color-text-primary: #111827;
  --color-bg-brand: #3b82f6;
  --space-2: 8px;
  --space-4: 16px;
  --radius-md: 6px;
  --font-body-md: 500 14px/20px "Pretendard";
}

시스템은 잘 굴러갔다. 문제는 그 시스템으로 무언가를 새로 검토할 때였다.

디자인 시안이 나오면, 그게 실제로 동작할 때 어떤 느낌인지 확인하고 싶어진다. 필터를 누르면 목록이 어떻게 줄어드는지, 상태가 바뀌면 화면이 어떻게 반응하는지, 좁은 화면에서 레이아웃이 무너지지는 않는지. Figma의 인터랙션만으로는 부족해서 결국 코드로 프로토타입을 만들게 되는데, 이 과정이 매번 똑같았다.

시안을 열고, 색상값을 토큰으로 하나하나 바꾸고, 버튼과 인풋을 11c 컴포넌트로 갈아 끼우고, 간격을 토큰에 맞추고, 상태 로직을 붙이고… 만들고 나면 그럴듯한데, 며칠 뒤 다시 보면 어딘가 하드코딩된 #3B82F6이나 padding: 16px가 숨어 있었다. 정합성을 지키려고 만든 프로토타입이 정작 시스템과 어긋나 있는 것이다.

이 작업, 판단은 사람이 하지만 손은 기계가 해도 되는 일이었다.

정적 시안 대신 '동작하는 컴포넌트'

그래서 11c-figma-proto라는 스킬을 만들었다. 한 줄로 요약하면 이렇다.

Figma 디자인 스펙을 읽어, 11c-design의 실제 컴포넌트와 토큰으로 동작하는 프로토타입을 자동 생성한다.

핵심은 '실제 컴포넌트'라는 부분이다. 시중의 디자인-투-코드 도구 대부분은 시안을 픽셀 단위로 뜯어 그럴듯한 정적 HTML을 만든다. 보기엔 똑같지만 우리 시스템 입장에서는 남의 코드다. 토큰을 안 쓰고, 버튼도 <div>에 스타일을 입힌 가짜다. 이런 결과물은 "이렇게 생겼다"는 건 보여줘도, "이렇게 동작한다"거나 "이대로 개발하면 된다"는 건 보장하지 못한다.

내가 원한 건 정반대였다. 시안이 아니라 자산을 만들고 싶었다. 실제 <Button>, 실제 <Chip>, 실제 토큰으로 조립돼서, 그대로 두면 디자인 QA 도구가 되고 개발자에게 넘기면 참고 구현이 되는 것.

스킬 자체는 생각보다 단순한 명세로 시작한다. 무엇을 입력받고, 어떤 규칙으로 변환하고, 무엇을 절대 하지 말아야 하는지를 적어 둔 문서다.

<!-- SKILL.md (발췌) -->

---

## 입력
- Figma 프레임 URL 또는 노드 ID
- 대상 화면의 레이아웃 · 컴포넌트 · 색/간격/타이포 스펙

---

## 변환 규칙
1. 색·간격·타이포·반경 값은 반드시 11c 토큰으로 매핑한다.
2. UI 요소는 11c-design 실제 컴포넌트로 치환한다 (div 흉내 금지).
3. 인터랙션(필터/상태변경/화면전환)을 실제로 동작시킨다.

---

## 금지 사항 (하드 룰)
- 색상 리터럴(#RRGGBB, rgb(), hsl()) 직접 사용 금지
- 매직 넘버 간격/반경(px 직값) 금지 → var(--space-*), var(--radius-*)
- 토큰 매핑을 못 찾으면 임의로 박지 말고 경고를 남긴다

스킬이 일하는 4단계

스킬은 프로토타입 하나를 만들 때 네 단계를 밟는다.

① Figma 스펙 읽기

먼저 Figma에서 해당 화면의 레이아웃, 컴포넌트 구성, 색·간격·타이포 값을 읽어 온다. 여기서 나오는 값은 아직 '날것의 숫자'다.

// Figma에서 읽어 온 원시 스펙 (예시)
{
  "node": "FilterChip",
  "fills": [{ "color": "#3B82F6" }],
  "cornerRadius": 6,
  "padding": { "top": 8, "right": 16, "bottom": 8, "left": 16 },
  "text": { "font": "Pretendard", "size": 14, "lineHeight": 20, "weight": 500 }
}

#3B82F6, 12px, Pretendard 14/20 같은, 아직 아무 의미도 없는 숫자들이다.

② 11c-design 실컴포넌트 + 토큰으로 전환

날것의 숫자를 시스템의 언어로 번역하는 단계다. 이게 스킬의 핵심이다.

색이 나오면 그냥 그 색을 박지 않고, 토큰 테이블에서 가장 가까운 의미 토큰을 찾는다. 값(value)이 아니라 역할(role)로 매핑하는 게 포인트다.

// 원시 값 → 의미 토큰 매핑 테이블 (발췌)
const TOKEN_MAP = {
  color: {
    "#111827": "--color-text-primary",
    "#6B7280": "--color-text-secondary",
    "#3B82F6": "--color-bg-brand",
  },
  space:  { 8: "--space-2", 12: "--space-3", 16: "--space-4" },
  radius: { 6: "--radius-md", 8: "--radius-lg" },
} as const;

function toToken(kind: keyof typeof TOKEN_MAP, raw: string | number) {
  const hit = (TOKEN_MAP[kind] as Record<string, string>)[raw];
  if (!hit) warn(`매핑 실패: ${kind}=${raw} → 토큰 없음`); // 임의로 박지 않는다
  return hit ? `var(${hit})` : null;
}

그리고 버튼처럼 생긴 요소를 만나면 <div>로 흉내 내지 않고 실제 컴포넌트로 치환한다. 결과적으로 코드에는 이런 식의 변환이 쌓인다.

// ❌ 시안을 그대로 옮긴 코드 (남의 코드)
<div style={{ background: "#3B82F6", padding: "8px 16px", borderRadius: "6px" }}>
  저장
</div>

// ✅ 11c 시스템의 언어로 번역된 코드 (우리 자산)
<Button variant="primary" size="md">저장</Button>

화면에 보이는 결과는 같지만, 코드의 의미가 완전히 달라진다. 위쪽 코드는 브랜드 색이 바뀌면 손으로 고쳐야 하지만, 아래쪽 코드는 토큰만 바꾸면 프로토타입까지 자동으로 따라온다.

③ 동작 구현

정적인 껍데기에 움직임을 넣는다. 필터를 누르면 목록이 실제로 걸러지고, 상태 값을 바꾸면 뱃지와 버튼이 반응하고, 탭을 누르면 화면이 전환된다.

// realtime 목록의 필터 동작 — 시안이 아니라 실제로 걸러진다
const [filters, setFilters] = useState<FilterState>({ status: "all", assignee: null });

const visible = useMemo(
  () => issues.filter((i) =>
    (filters.status === "all" || i.status === filters.status) &&
    (!filters.assignee || i.assignee === filters.assignee)
  ),
  [issues, filters]
);

return (
  <>
    <ChipGroup>
      {STATUS_OPTIONS.map((opt) => (
        <Chip
          key={opt.value}
          selected={filters.status === opt.value}
          onClick={() => setFilters((f) => ({ ...f, status: opt.value }))}
        >
          {opt.label}
        </Chip>
      ))}
    </ChipGroup>
    <IssueTable rows={visible} />
  </>
);

여기서부터는 '시안'이 아니라 '앱'에 가까워진다. 클릭해 보면서 흐름이 자연스러운지, 엣지 케이스에서 뭐가 깨지는지 직접 확인할 수 있다.

④ 정합성 자동 검증

마지막이 내가 가장 아끼는 단계다. 다 만든 코드를 스킬이 다시 훑으면서 하드코딩된 스타일 값이 남아 있는지 검사한다. 색상 리터럴, 매직 넘버 간격, 토큰을 거치지 않은 폰트·반경 값을 찾아내 토큰으로 대체하거나 경고를 띄운다.

// 정합성 검사기 — 하드코딩된 스타일 값을 잡아낸다
const RULES = [
  { name: "color-literal", re: /#[0-9a-f]{3,6}\b|rgb\(|hsl\(/gi },
  { name: "magic-space",   re: /\b(margin|padding|gap):\s*\d+px/gi },
  { name: "magic-radius",  re: /border-radius:\s*\d+px/gi },
];

function lintTokens(code: string) {
  const violations = RULES.flatMap((rule) =>
    [...code.matchAll(rule.re)].map((m) => ({
      rule: rule.name,
      value: m[0],
      hint: "var(--token-name) 형태로 교체 필요",
    }))
  );
  return violations; // 비어 있으면 통과, 아니면 리포트
}
$ 11c-figma-proto lint ./proto/realtime.tsx

✖ 2 violations
  color-literal   #E5E7EB      → var(--color-border-subtle) 제안
  magic-space     padding:12px → var(--space-3) 제안

프로토타입이 시스템과 어긋나 있습니다. 위 2건을 교체하세요.

이 단계 덕분에 "정합성을 지키려고 만든 프로토타입이 정작 어긋나 있는" 그 아이러니가 사라졌다. 사람이 눈으로 잡던 걸 규칙이 잡으니, 며칠 뒤에 다시 봐도 프로토타입이 시스템과 어긋나 있지 않다.

실전 투입 — Keeper Admin 모바일 전환

도구는 실제로 굴려봐야 진짜인지 안다. 마침 Keeper Admin을 모바일로 옮기는 작업이 있었다. 기존 화면은 정적 HTML로 짜여 있었고, 이걸 11c 컴포넌트 기반 React로 마이그레이션하면서 모바일 반응형까지 새로 잡아야 했다. 프로토타이핑 도구를 검증하기에 딱 맞는 무대였다.

두 화면을 집중적으로 다뤘다.

realtime — 일감/이슈 현황 목록. 관리자가 실시간으로 처리 현황을 훑는 화면이다. 필터 칩을 분리해 상단에 노출하고, 세부 필터는 바텀시트로 내렸다. 검색과 정렬을 붙이고, 좁은 화면에서도 읽히도록 테이블 뷰를 재구성했으며, 빈 목록·로딩·에러 같은 엣지 케이스까지 실제로 동작시켰다.

ticket — 일감 상세. 하나의 일감이 여러 상태를 오가는 화면이라 로직이 더 무거웠다. 상태머신을 그대로 옮겨 스텝퍼로 시각화했다.

// ticket 상태머신 — 시안엔 없던 '규칙'이 코드로 살아난다
const TRANSITIONS: Record<TicketStatus, TicketStatus[]> = {
  open:        ["in_progress", "canceled"],
  in_progress: ["resolved", "on_hold"],
  on_hold:     ["in_progress", "canceled"],
  resolved:    ["closed", "in_progress"], // 재오픈 허용
  closed:      [],
  canceled:    [],
};

const canMove = (from: TicketStatus, to: TicketStatus) =>
  TRANSITIONS[from].includes(to);

// 스텝퍼는 이 상태머신을 그대로 그린다
<Stepper
  steps={STEP_ORDER}
  current={ticket.status}
  isReachable={(s) => canMove(ticket.status, s)}
/>

정보는 5개 탭으로 나눴고, 화면 폭에 따라 레이아웃이 달라지는 반응형을 적용했다.

작업하면서 몇 가지 구조적 결정도 내렸다. 정적 HTML을 React로 옮기는 김에 두 화면이 공유하는 부분을 공통 셸(shell)로 추출했고, 뷰포트를 4단계로 나눈 반응형 기준을 세웠다.

// 공통 셸 — realtime/ticket이 함께 쓰는 레이아웃 골격
export function AdminMobileShell({ header, children }: ShellProps) {
  return (
    <div className="admin-shell">
      <AppBar>{header}</AppBar>
      <main>{children}</main>
      <BottomNav />
    </div>
  );
}
/* 뷰포트 4단계 — 값은 전부 토큰 (하드코딩 없음) */
@media (min-width: var(--bp-sm)) { /* ≥360  모바일  */ }
@media (min-width: var(--bp-md)) { /* ≥600  태블릿 세로 */ }
@media (min-width: var(--bp-lg)) { /* ≥905  태블릿 가로 */ }
@media (min-width: var(--bp-xl)) { /* ≥1240 데스크톱 */ }

프로토타입이라고 대충 만든 게 아니라, 그대로 실제 구현의 골격이 되도록 잡은 것이다.

결과물은 정적 시안이었다면 절대 못 했을 걸 해냈다. 필터를 실제로 눌러 목록이 줄어드는 걸 보고, 상태를 바꿔 스텝퍼가 진행되는 걸 보고, 브라우저 폭을 줄여 레이아웃이 4단계로 접히는 걸 봤다. "이렇게 생겼다"가 아니라 "이렇게 동작한다"를 손으로 만질 수 있었다.

예상 못 한 수확 — 도구가 시스템의 빈틈을 찾아냈다

여기까지는 계획된 이야기다. 그런데 진짜 재밌는 건 그다음에 일어났다.

프로토타입을 만들다 보니, 11c-design 자체의 한계가 드러나기 시작했다.

realtime 화면에서 필터 바텀시트를 구현하려는데, 우리 BottomSheet 컴포넌트가 여러 필터를 한 번에 적용하는 batch 패턴을 지원하지 않았다. 시트 안에서 조건을 여러 개 고르고 "적용"을 눌러 한꺼번에 반영하는, 흔하디흔한 패턴인데도 그랬다. 우리 컴포넌트는 선택이 일어나는 즉시 바깥에 반영해 버리는 구조였다.

// 우리가 필요했던 것 — 본체 탭과 삭제 탭이 분리
<Chip onClick={toggle} onRemove={remove} removable />

// 실제 컴포넌트 — onClick 하나뿐. X를 눌러도 본체 onClick이 같이 터진다
<Chip onClick={handle} />   // 삭제하려다 필터가 토글된다 😵

둘 다 시안만 그렸다면 절대 안 나왔을 발견이다. 정적 시안에서 칩은 그냥 그림이니까 X가 눌리든 말든 상관이 없다. 실제로 동작시켜 보니까 비로소 "어? 이거 우리 컴포넌트로는 안 되는데?"가 나온 것이다.

여기서 선순환이 생겼다.

프로토타입을 만들려고 디자인 시스템을 쓴다 → 시스템의 빈틈이 드러난다 → 시스템을 개선한다 → 다음 프로토타입은 더 매끄럽게 나온다.

프로토타이핑 도구가 단순히 시간을 아껴 주는 데서 그치지 않고, 디자인 시스템을 성장시키는 압력으로 작동한 것이다. 도구가 시스템을 검증하고, 시스템이 도구를 더 좋게 만든다. 애초에 정적 시안이 아니라 '진짜 컴포넌트'로 프로토타입을 뽑기로 한 결정이, 여기서 뜻밖의 배당을 돌려줬다.

정리하며

이 프로젝트를 지나면서 확실해진 게 하나 있다. 디자이너가 개발을 조금 할 줄 알면, 자기 일의 반복을 스스로 걷어낼 수 있다. 시안을 프로토타입으로 옮기는 그 지루한 반복이 그랬다.

그런데 이 도구는 거기서 한 걸음 더 나갔다. 반복을 없앤 것에 더해, 그 과정에서 우리가 매일 쓰는 시스템의 약한 고리까지 찾아냈다. 자동화가 노동을 줄이는 걸 넘어, 시스템의 품질을 끌어올리는 계기가 될 수 있다는 걸 배웠다.

정적 시안은 예쁘다. 하지만 그건 사진이다. 나는 이제 사진 대신, 눌러 볼 수 있고 깨져 볼 수 있고 그래서 시스템을 시험할 수 있는 프로토타입을 만든다. 그리고 그게 시스템을 다시 더 낫게 만든다.

다음 반복 노동이 눈에 띄면, 아마 또 도구를 만들고 있을 것 같다.