Claude Code로 디자인 시스템 구축하기: Design Token, Storybook, CI 실전
디자인 토큰부터 Storybook, 접근성, CI까지 Claude Code로 안전하게 디자인 시스템을 구축하는 방법을 설명합니다.
버튼부터 만들면 실패한다: 디자인 시스템은 변경을 운영하는 방식이다
팀에서 디자인 시스템을 시작하면 버튼, 카드, 입력 폼부터 보기 좋게 정리하기 쉽습니다. 몇 주 뒤 브랜드 색을 바꾸거나 disabled 상태와 focus 스타일을 통일하려고 하면 문제가 드러납니다. 같은 색이 수십 개 파일에 흩어져 있고, Storybook에는 실제 업무에서 쓰는 상태가 없으며, 어떤 시각적 변경을 승인해야 하는지도 분명하지 않습니다.
디자인 시스템은 컴포넌트 전시장이 아닙니다. 색상, 간격, 타이포그래피, 상태, 리뷰와 테스트를 제품 화면을 깨뜨리지 않고 반복해서 바꾸는 운영 모델입니다. Claude Code는 기존 저장소를 읽고, 여러 파일을 수정하고, Storybook과 테스트를 실행한 뒤 diff를 보고할 수 있어 이 작업에 잘 맞습니다. 하지만 브랜드 의미, 공개 API, 최종 접근성 경험과 시각적 차이의 승인까지 대신 판단하지는 않습니다.
이 글은 tokens.json 하나에서 시작해 React/TypeScript 컴포넌트, Storybook, 접근성 검사, 시각적 회귀 테스트와 CI까지 연결합니다. 전체 프런트엔드를 한 번에 다시 만들 필요는 없습니다. 버튼 하나와 작은 토큰 집합으로 검증 가능한 첫 번째 경로를 만드는 것이 목표입니다.
이 글에서 가져갈 핵심 네 가지
- primitive, semantic, component의 세 계층으로 토큰을 나눠 raw color가 컴포넌트에 퍼지는 일을 막습니다.
- Claude Code가 수정할 디렉터리와 실행할 검증 명령을 먼저 고정합니다.
- 2026년 권장 방식인
@storybook/addon-vitest와vitest --project=storybook을 사용하고, 수동 접근성 검토도 남깁니다. - Figma는 검토 입력으로 사용하며, 처음부터 위험한 양방향 자동 동기화를 만들지 않습니다.
함께 읽을 글로는 Claude Code 디자인 토큰 관리, Claude Code Storybook 개발, Claude Code 접근성 개선이 있습니다.
목표 아키텍처: tokens.json을 코드의 계약으로 삼기
이 흐름에서 코드 쪽 기준 원본은 tokens.json입니다. Figma는 디자인 작업에 꼭 필요하지만, 코드와 CI에 들어오는 결정은 사람이 검토하고 되돌릴 수 있는 계약으로 표현되어야 합니다.
flowchart LR
Figma["Figma Variables"]
Tokens["tokens.json"]
Build["token build script"]
CSS["CSS variables"]
TS["TypeScript token map"]
Components["React components"]
Storybook["Storybook stories"]
CI["Visual and a11y CI"]
Figma -->|review input| Tokens
Tokens --> Build
Build --> CSS
Build --> TS
CSS --> Components
TS --> Components
Components --> Storybook
Storybook --> CI
Design Token은 색상, 간격, 모서리 반경, 타이포그래피와 컴포넌트 상태 같은 디자인 결정을 이름이 있는 데이터로 저장한 것입니다. 컴포넌트에 #2563eb을 직접 쓰면 단지 “파란색”이라는 정보만 남습니다. action.background.primary 같은 semantic token을 사용하면 “주요 행동의 배경색”이라는 목적이 남아 브랜드가 바뀌어도 컴포넌트를 검색해서 일일이 고칠 필요가 없습니다.
최신 기준은 공식 자료에서 확인할 수 있습니다. Design Tokens Community Group 형식, Claude Code 문서, Claude Code 보안 가이드, Storybook Vitest addon, Storybook 접근성 테스트, Storybook 시각적 테스트, Figma REST API를 참고하세요.
Claude Code가 맡을 범위와 사람이 판단할 범위
“디자인 시스템을 만들어 줘”라는 요청에는 파일 범위도, 합격 기준도, 금지 사항도 없습니다. 결과는 크고 검토하기 어려운 diff가 되기 쉽습니다. “Button만 마이그레이션하고 공개 API는 유지하며, Storybook 상태를 추가하고 접근성 테스트를 실행하라”처럼 경계를 적어야 안전합니다.
| 작업 영역 | Claude Code에 맡기기 좋은 범위 | 사람이 결정할 사항 |
|---|---|---|
| 토큰 | CSS의 반복 색상과 간격을 찾아 후보 목록 생성 | 브랜드 의미와 토큰 이름 |
| 컴포넌트 | 타입이 있는 Button, Input, Alert 구현 | 공개 API와 제품 의미 |
| Storybook | 변형, 상태, interaction story 추가 | 실제 업무에서 중요한 상태 |
| 접근성 | 누락된 label, focus 문제, axe 위반 탐지 | 최종 키보드, 스크린 리더, UX 판단 |
| CI | Pull Request에 시각적·접근성 검사 추가 | 실패 정책과 예외 승인 절차 |
파일을 수정하기 전에 다음 프로젝트 규칙을 Claude Code에 전달합니다.
디자인 시스템 작업 규칙:
- src/components, src/styles, .storybook, tests, scripts, tokens.json만 수정한다.
- 브랜드 색상을 바꾸기 전에 이전 토큰 이름과 새 토큰 이름을 모두 나열한다.
- 새 컴포넌트마다 TypeScript props, 키보드 동작, Storybook stories, 접근성 메모를 추가한다.
- 완료를 보고하기 전에 npm run tokens:build, npm run test:storybook -- --run, npm run build-storybook, npm run test:visual을 실행한다.
- focus 동작이 바뀌면 수동 검토 절차를 포함한다.
보안도 디자인 시스템 작업 범위에 포함됩니다. Figma token, npm token, CI secret, 고객의 비공개 스크린샷을 프롬프트에 붙이지 마세요. Claude Code 권한은 필요한 디렉터리와 명령으로 제한하고 실행 전에 명령을 검토합니다. 대규모 snapshot 갱신은 사람이 diff를 본 뒤 승인해야 합니다.
최소 설치: 2026년에는 Storybook Vitest addon을 우선한다
아래 예시는 utility class를 쓰는 React와 TypeScript 프로젝트를 가정합니다. pnpm이나 yarn을 쓴다면 패키지 관리자에 맞게 명령을 바꾸세요.
npm install class-variance-authority clsx tailwind-merge
npx storybook@latest init
npx storybook add @storybook/addon-a11y
npx storybook add @storybook/addon-vitest
npm install -D @playwright/test concurrently http-server wait-on
npx playwright install chromium
Vitest addon은 Vite 기반 Storybook framework 또는 지원되는 Next.js Vite 통합이 필요합니다. 오래된 프로젝트가 이 조건을 충족하지 못할 때만 Storybook 공식 마이그레이션 문서를 따라 기존 @storybook/test-runner를 호환용 fallback으로 검토하세요. 새 프로젝트의 권장 설정으로 사용해서는 안 됩니다.
로컬과 CI에서 같은 흐름을 재현하도록 package.json script를 추가합니다. 핵심은 test:storybook이 vitest --project=storybook을 실행하는 것입니다.
{
"scripts": {
"tokens:build": "node scripts/build-tokens.mjs",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"test:storybook": "vitest --project=storybook",
"test:visual": "playwright test tests/button.visual.spec.ts"
}
}
세 계층의 Design Token으로 변경 계약 만들기
토큰은 primitive, semantic, component 세 계층으로 나눕니다. Primitive는 원시 값을 저장하고, semantic은 그 값의 목적을 설명하며, component는 특정 UI 상태의 결정을 담습니다. 이렇게 해야 색상 팔레트는 유지하면서 컴포넌트가 숫자로 된 색상 이름에 직접 의존하지 않습니다.
{
"$schema": "https://www.designtokens.org/schemas/2025.10/format.json",
"primitive": {
"color": {
"blue": {
"50": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.9373, 0.9647, 1], "hex": "#eff6ff" }
},
"600": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.1451, 0.3882, 0.9216], "hex": "#2563eb" }
},
"700": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.1137, 0.3059, 0.8471], "hex": "#1d4ed8" }
}
},
"gray": {
"50": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.9765, 0.9804, 0.9843], "hex": "#f9fafb" }
},
"200": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.898, 0.9059, 0.9216], "hex": "#e5e7eb" }
},
"900": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.0667, 0.0941, 0.1529], "hex": "#111827" }
}
},
"red": {
"600": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.8627, 0.149, 0.149], "hex": "#dc2626" }
},
"700": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [0.7255, 0.1098, 0.1098], "hex": "#b91c1c" }
}
},
"white": {
"$type": "color",
"$value": { "colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff" }
}
},
"space": {
"2": { "$type": "dimension", "$value": { "value": 0.5, "unit": "rem" } },
"3": { "$type": "dimension", "$value": { "value": 0.75, "unit": "rem" } },
"4": { "$type": "dimension", "$value": { "value": 1, "unit": "rem" } },
"6": { "$type": "dimension", "$value": { "value": 1.5, "unit": "rem" } }
},
"radius": {
"md": { "$type": "dimension", "$value": { "value": 0.375, "unit": "rem" } },
"lg": { "$type": "dimension", "$value": { "value": 0.5, "unit": "rem" } }
}
},
"semantic": {
"color": {
"surface": { "$type": "color", "$value": "{primitive.color.white}" },
"text": { "$type": "color", "$value": "{primitive.color.gray.900}" },
"border": { "$type": "color", "$value": "{primitive.color.gray.200}" },
"focus": { "$type": "color", "$value": "{primitive.color.blue.600}" }
}
},
"component": {
"button": {
"primary": {
"background": { "$type": "color", "$value": "{primitive.color.blue.600}" },
"backgroundHover": { "$type": "color", "$value": "{primitive.color.blue.700}" },
"text": { "$type": "color", "$value": "{primitive.color.white}" }
},
"danger": {
"background": { "$type": "color", "$value": "{primitive.color.red.600}" },
"backgroundHover": { "$type": "color", "$value": "{primitive.color.red.700}" },
"text": { "$type": "color", "$value": "{primitive.color.white}" }
}
}
}
}
이 파일 하나에서 CSS 변수와 TypeScript token map을 생성해 두 결과물을 사람이 따로 관리하지 않게 합니다.
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
const source = JSON.parse(readFileSync("tokens.json", "utf8"));
function getToken(path) {
const node = path.split(".").reduce((current, key) => current?.[key], source);
if (!node || typeof node.$value === "undefined") {
throw new Error(`Unknown token reference: ${path}`);
}
return node.$value;
}
function resolveValue(value, stack = []) {
if (typeof value === "string" && value.startsWith("{") && value.endsWith("}")) {
const path = value.slice(1, -1);
if (stack.includes(path)) {
throw new Error(`Circular token reference: ${[...stack, path].join(" -> ")}`);
}
return resolveValue(getToken(path), [...stack, path]);
}
return value;
}
function toCssValue(value) {
if (value && typeof value === "object") {
if (typeof value.hex === "string") return value.hex;
if (typeof value.value === "number" && typeof value.unit === "string") {
return `${value.value}${value.unit}`;
}
throw new Error(`Unsupported token value: ${JSON.stringify(value)}`);
}
return String(value);
}
function walk(node, pathParts = [], result = {}) {
if (!node || typeof node !== "object") return result;
if (node && typeof node === "object" && typeof node.$value !== "undefined") {
result[pathParts.join("-")] = toCssValue(resolveValue(node.$value));
return result;
}
for (const [key, value] of Object.entries(node)) {
if (key.startsWith("$")) continue;
walk(value, [...pathParts, key], result);
}
return result;
}
const flat = walk(source);
const css = [
":root {",
...Object.entries(flat).map(([name, value]) => ` --${name}: ${value};`),
"}",
""
].join("\n");
mkdirSync(dirname("src/styles/tokens.css"), { recursive: true });
mkdirSync(dirname("src/tokens.ts"), { recursive: true });
writeFileSync("src/styles/tokens.css", css);
writeFileSync("src/tokens.ts", `export const tokens = ${JSON.stringify(flat, null, 2)} as const;\n`);
console.log(`Generated ${Object.keys(flat).length} tokens.`);
첫 요청부터 모든 UI를 바꾸게 하지 마세요. “반복되는 raw color와 spacing을 찾아 후보 토큰으로 매핑하고, 파일을 수정하지 말고 보고서만 반환하라”고 먼저 요청합니다. 사람이 이름과 범위를 승인한 다음 컴포넌트 하나를 옮깁니다.
타입이 안정적인 React 컴포넌트 만들기
컴포넌트 계층은 단순하고 예측 가능해야 합니다. 아래 Button은 variant, size, loading, disabled와 눈에 보이는 focus 스타일을 포함하면서 기본 button 속성을 그대로 받을 수 있습니다.
import { forwardRef, type ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const buttonVariants = cva(
[
"inline-flex items-center justify-center gap-2 rounded-md font-medium",
"transition-colors focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-[var(--semantic-color-focus)] focus-visible:ring-offset-2",
"disabled:pointer-events-none disabled:opacity-50"
],
{
variants: {
variant: {
primary: [
"bg-[var(--component-button-primary-background)]",
"text-[var(--component-button-primary-text)]",
"hover:bg-[var(--component-button-primary-backgroundHover)]"
],
secondary: "border border-[var(--semantic-color-border)] bg-[var(--semantic-color-surface)] text-[var(--semantic-color-text)] hover:bg-gray-50",
danger: [
"bg-[var(--component-button-danger-background)]",
"text-[var(--component-button-danger-text)]",
"hover:bg-[var(--component-button-danger-backgroundHover)]"
]
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base"
}
},
defaultVariants: {
variant: "primary",
size: "md"
}
}
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{ className, variant, size, loading = false, disabled, children, ...props },
ref
) {
return (
<button
ref={ref}
className={cn(buttonVariants({ variant, size }), className)}
disabled={disabled || loading}
aria-busy={loading || undefined}
{...props}
>
{loading ? (
<span
aria-hidden="true"
className="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent"
/>
) : null}
<span>{children}</span>
</button>
);
});
리뷰 질문은 “버튼이 예쁜가?”에서 끝나지 않습니다. 여러 제품 팀이 오래 사용할 만큼 API가 안정적인지, loading 중 중복 제출을 막는지, 새 variant가 기존 호출을 깨뜨리지 않는지 확인해야 합니다. 이 판단은 실제 제품 흐름을 아는 사람이 맡습니다.
Storybook을 실행 가능한 명세로 바꾸기
중요한 상태는 모두 story로 존재해야 합니다. Storybook에 없는 상태는 디자이너가 검토하기 어렵고, 테스트가 잡아내기 어려우며, 팀이 같은 화면을 가리키며 논의하기도 어렵습니다.
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./Button";
const meta = {
title: "Design System/Button",
component: Button,
parameters: {
layout: "centered",
a11y: {
test: "error"
}
},
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "danger"]
},
size: {
control: "select",
options: ["sm", "md", "lg"]
},
loading: { control: "boolean" },
disabled: { control: "boolean" }
}
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: "Save changes",
variant: "primary"
}
};
export const Danger: Story = {
args: {
children: "Delete",
variant: "danger"
}
};
export const Loading: Story = {
args: {
children: "Saving",
loading: true
}
};
export const AllStates: Story = {
render: () => (
<div className="flex flex-wrap items-center gap-3">
<Button variant="primary" size="sm">Small</Button>
<Button variant="primary" size="md">Medium</Button>
<Button variant="primary" size="lg">Large</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="danger">Danger</Button>
<Button disabled>Disabled</Button>
<Button loading>Loading</Button>
</div>
)
};
Claude Code에는 기존 stories를 유지하고 빠진 상태만 추가하며 story ID가 바뀌면 이유를 설명하라고 요청하세요. 그래야 시각적 snapshot과 접근성 보고서가 계속 명확한 검토 대상에 연결됩니다.
CI에서 컴포넌트·접근성·시각적 검사를 실행하기
자동 접근성 검사는 키보드와 스크린 리더를 이용한 수동 검토를 대체하지 않지만 많은 구조적 위반을 일찍 발견합니다. Vite 기반 Storybook의 2026년 권장 경로는 @storybook/addon-vitest입니다. 이 addon은 stories를 브라우저 테스트로 변환하고, parameters.a11y.test = "error"가 있는 story에서 접근성 위반이 나오면 컴포넌트 테스트를 실패시킵니다.
CI에서는 npm run test:storybook -- --run을 실행합니다. 예전 @storybook/test-runner 흐름과 달리 Vitest addon은 컴포넌트와 접근성 테스트를 위해 별도의 Storybook 서버를 띄울 필요가 없습니다. 아래의 맞춤 Playwright screenshot에는 빌드된 Storybook 서버가 계속 필요합니다.
시각적 테스트는 고가치 상태부터 작게 시작합니다. 모든 story에 snapshot을 한꺼번에 추가하지 마세요.
CI를 켜기 전에 대상 앱에서 npx playwright test tests/button.visual.spec.ts --update-snapshots를 한 번 실행하고, 사람이 기준 이미지를 검토한 뒤 저장소에 커밋합니다. 기준 이미지가 없으면 Playwright가 비교할 대상이 없어 첫 CI가 실패합니다.
import { expect, test } from "@playwright/test";
test("button all states visual snapshot", async ({ page }) => {
await page.goto("http://127.0.0.1:6006/iframe.html?id=design-system-button--all-states");
await expect(page).toHaveScreenshot("button-all-states.png", {
fullPage: true,
animations: "disabled"
});
});
토큰, 컴포넌트, Storybook과 screenshot 검사를 GitHub Actions에 연결합니다.
name: design-system-quality
on:
pull_request:
paths:
- "tokens.json"
- "scripts/build-tokens.mjs"
- "src/components/**"
- "src/styles/**"
- ".storybook/**"
- "tests/**"
- "package.json"
- "package-lock.json"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run tokens:build
- run: npx playwright install --with-deps chromium
- run: npm run test:storybook -- --run
- run: npm run build-storybook
- run: >
npx concurrently -k -s first -n server,tests
"npx http-server storybook-static -p 6006"
"npx wait-on http://127.0.0.1:6006 && npm run test:visual"
CI가 실패하면 실패한 story ID, axe 위반, 변경 파일과 시각적 diff를 Claude Code에 제공합니다. secret이나 비공개 정보가 섞일 수 있는 전체 로그를 그대로 프롬프트에 넣지 마세요.
Figma 통합의 현실적인 경계
Figma Variables는 토큰 작업에 유용한 입력이지만 초기부터 자동 양방향 동기화를 만들면 위험합니다. 승인되지 않은 실험, 오래된 컴포넌트 이름과 비공개 디자인 메모가 생산 토큰에 섞일 수 있습니다.
| 대상 | 자동화하기 좋은 작업 | 피해야 할 작업 |
|---|---|---|
| Figma Variables | export 후 tokens.json과 비교 | 생산 토큰을 바로 덮어쓰기 |
| Figma Components | 상태와 prop 후보 수집 | React API를 자동 결정 |
| Figma comments | 해결되지 않은 질문 요약 | 최종 디자인 의도 추측 |
| Storybook links | 디자인 리뷰에 story URL 첨부 | Storybook을 디자인 승인으로 간주 |
Claude Code에는 먼저 읽기 전용 차이 보고서를 만들게 합니다.
figma-tokens-export.json과 tokens.json을 읽는다.
다음 내용을 포함한 Markdown 보고서를 만든다.
1. Figma에는 있지만 코드에는 없는 토큰
2. 코드에는 있지만 Figma에는 없는 토큰
3. 이름이 같은 semantic token의 값 차이
tokens.json을 수정하거나 토큰 이름을 바꾸지 않는다. focus, danger, text color의 위험한 차이를 표시한다.
목표는 동기화 자체가 아니라 사람이 안전하게 검토할 수 있는 diff입니다. 팀이 기준 원본에 합의하기 전에는 Figma 연동을 report-only 모드로 유지합니다.
Use case 1: SaaS 관리자 화면을 한 화면씩 옮기기
SaaS 관리자 화면의 버튼, 폼, 표와 모달에는 loading, disabled, error, 권한 상태가 많습니다. Claude Code에 기존 Button 사용처를 조사하고 호환되지 않는 props를 분류하게 한 뒤, 호환 계층을 만들고 한 화면씩 마이그레이션합니다. 삭제, 결제, 권한 변경처럼 위험한 작업의 문구와 흐름은 사람이 확인합니다.
합격 기준에는 기존 화면에 변화가 없을 것, 새 화면의 모든 상태가 Storybook에 있을 것, 키보드만으로 조작할 수 있을 것, token build와 CI가 통과할 것이 포함되어야 합니다. 수십 개 화면을 하나의 Pull Request에 넣지 마세요.
Use case 2: 화이트 라벨 제품의 브랜드 전환
화이트 라벨 제품은 고객마다 primitive brand color가 달라도 “주요 행동”, “위험 행동”, “본문 텍스트”라는 semantic token은 안정적으로 유지할 수 있습니다. Claude Code는 브랜드별 CSS variables를 만들고 Storybook에 theme switch를 추가할 수 있습니다.
사람은 색상 대비와 브랜드 사용 허가를 확인해야 합니다. 고객이 준 파란색이라고 해서 버튼 텍스트나 focus ring에 접근성 문제 없이 쓸 수 있다고 자동 판단하면 안 됩니다.
Use case 3: 레거시 CSS를 점진적으로 정리하기
오래된 프로젝트에는 비슷한 파란색, 제각각인 간격과 반복되는 radius가 많습니다. Claude Code가 raw value를 스캔하고 사용 횟수와 문맥으로 묶어 “원시 값 → 후보 토큰 → 사용 파일” 마이그레이션 표를 만들게 합니다.
컴포넌트 하나를 먼저 바꾸고 시각적 snapshot을 비교하세요. 사이트 전체를 한 commit에서 바꾸면 토큰 오류인지 컴포넌트 예외인지 구분하기 어렵습니다. 작은 배치마다 되돌릴 수 있는 지점을 남깁니다.
Use case 4: 마케팅 페이지와 상담 퍼널
상담 사이트의 CTA 버튼, 가격 카드와 폼 상태가 서로 다르면 방문자 신뢰가 낮아지고 A/B 테스트 결과도 설명하기 어려워집니다. Claude Code는 컴포넌트를 통일하고 제출 중, 실패, 성공, 중복 클릭 상태를 Storybook에 추가할 수 있습니다.
전환 전략은 사람이 결정합니다. 디자인 시스템은 같은 컴포넌트와 추적 가능한 상태로 실험하도록 도울 뿐, 특정 문구나 색상이 상담을 늘렸다고 자동으로 증명하지 않습니다.
자주 생기는 함정과 수정 방법
함정 1: 컴포넌트가 primitive token을 직접 사용한다
원인: blue-600은 편리해 보이지만 브랜드 외형을 컴포넌트 의존성에 넣습니다. 브랜드가 바뀌면 저장소 전체 검색과 치환이 필요합니다.
수정: 컴포넌트는 semantic 또는 component token만 사용하고 primitive는 상위 토큰의 참조 대상으로 둡니다.
함정 2: Storybook이 로컬에서 열리면 완료라고 본다
원인: CI에 없는 컴포넌트 카탈로그는 의존성 업데이트 뒤 조용히 깨질 수 있습니다. 문서일 뿐 안전망은 아닙니다.
수정: vitest --project=storybook, Storybook build와 중요한 시각적 snapshot을 Pull Request 검사에 넣습니다.
함정 3: 시각적 snapshot을 너무 빨리 늘린다
원인: 애니메이션, 날짜, 외부 폰트와 임의 ID가 잡음을 만들고 리뷰어가 차이를 보지 않고 갱신을 승인하게 됩니다.
수정: 동적 콘텐츠를 고정하고 중요한 컴포넌트 상태부터 시작하며, snapshot 차이는 사람이 직접 확인합니다.
함정 4: axe 통과를 접근성 완료로 해석한다
원인: 자동 도구는 문구의 의미, 키보드 흐름의 품질과 스크린 리더 이해도를 모두 판단하지 못합니다.
수정: 키보드와 스크린 리더 수동 검토를 남기고 실행 절차와 결과를 기록합니다.
함정 5: Claude Code에 전체 마이그레이션을 한 번에 맡긴다
원인: 파일 범위, 시각적 차이와 API 변경이 동시에 커져 실패 원인을 찾기 어렵습니다.
수정: 컴포넌트 단위로 나누고 먼저 proof command를 정한 뒤 현재 배치를 승인하고 다음 범위를 엽니다.
병합 전 체크리스트
- 토큰 이름이 외형만이 아니라 의미를 표현한다
- 컴포넌트 props가 최소이며 안정적이다
- Storybook에 disabled, loading, error, focus, hover 상태가 있다
- 키보드만으로 조작할 수 있다
- 필요한 곳에 ARIA가 있고 native HTML로 충분한 곳에는 중복 추가하지 않았다
- 사람이 시각적 snapshot 차이를 검토했다
- Figma 차이가 검토 가능한 산출물로 저장되었다
- Claude Code가 요청한 파일 영역만 수정했다
- 프롬프트, 로그, story와 screenshot에 secret이나 고객 비공개 데이터가 없다
이 체크리스트를 프로젝트 지침에 넣으면 다음 Claude Code 세션에서도 같은 합격 기준을 재사용할 수 있습니다.
다음 단계는 하나만 선택한다
대상 프로젝트에 작은 tokens.json을 만들고 생성 script를 실행한 뒤 기존 Button의 모든 상태를 Storybook에 추가하세요. CSS variables와 TypeScript 상수가 반복 생성되고, npm run test:storybook -- --run, Storybook build, 시각적 테스트가 CI에서 재현되는 것을 확인한 다음 두 번째 컴포넌트로 넘어갑니다. Figma 연동은 당분간 읽기 전용 보고서로 둡니다.
팀에서 디자인 시스템 마이그레이션 범위, Storybook 도입, 접근성 검토 또는 UI 리팩터링을 위한 Claude Code 권한 경계를 함께 정해야 한다면 교육 및 상담 페이지를 확인하세요.
실제로 테스트한 결과
2026년 7월 22일, 이 글의 build-tokens.mjs 코드를 추출해 전체 tokens.json 예제로 실행했습니다. 정상 fixture는 종료 코드 0으로 끝났고 CSS custom properties 25개와 TypeScript token map을 생성했으며 {primitive.color.blue.600} 참조를 #2563eb으로 해석했습니다. 알 수 없는 참조를 넣은 negative fixture는 0이 아닌 종료 코드와 Unknown token reference 메시지로 실패했습니다. JSON snippet, code fence와 내부 링크도 검사했고 Storybook 명령은 현재 공식 Vitest addon 및 마이그레이션 문서와 대조했습니다. 이 사이트 저장소 자체에는 Storybook을 설치하지 않았으므로 실제 도입 전 대상 애플리케이션에서 컴포넌트와 시각적 테스트를 실행해야 합니다.
관련 글
Claude Code로 Design Tokens 구현하기: Figma 핸드오프부터 CSS 변수와 Tailwind까지
Claude Code로 Design Tokens를 설계하고 Style Dictionary, CSS 변수, Tailwind, React에 연결합니다.
Claude Code로 CSS variables와 theme token 설계하기
Claude Code로 CSS custom properties, var() fallback, theme token, dark mode를 설계합니다.
Claude Code로 실무 API 설계하기: OpenAPI, 테스트, Breaking Change 검사
Claude Code로 REST API를 설계하는 실전 흐름. OpenAPI, Mock, 테스트, 버전, 보안, 함정을 다룹니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.