네, 영어로 테마를 설명하고 작동하는 디자인 시스템을 얻을 수 있다. 다만 영어 설명은 프롬프트가 아니다. 소스 파일이다. 그리고 모든 소스 파일처럼 컴파일러, 타입 시스템, 테스트가 필요하다.
챗봇 쿼리처럼 다루면 화요일에는 월요일과 다른 색상 팔레트를 얻게 된다. 한정된 컨텍스트와 엄격한 스키마를 가진 DSL로 다루면, 디자인 시스템이 소비할 수 있는 재현 가능한 토큰을 얻는다.
디자인 토큰은 창작 문제가 아니라 동기화 문제다
대부분의 팀은 색을 고르는 데 어려움을 겪지 않는다. Figma, CSS 변수, 컴포넌트 라이브러리 간에 색을 일관되게 유지하는 데 어려움을 겪는다. 전형적인 워크플로우는 이렇다. 디자이너가 스타일 가이드에서 hex 코드를 업데이트하고, 개발자가 이를 JSON 파일에 복사하고, 다른 개발자가 React 컴포넌트에서 잘못된 키를 참조하고, 3개월 후 한 곳에서는 #1a1a2e가 되고 다른 곳에서는 #1b1b2f가 된다.
진정한 고통은 인수인계다. Figma는 코드가 아니다. JSON은 디자인 도구가 아니다. 영어는 그 중간에 있다. 영어는 디자이너와 개발자 모두 교육 없이 읽을 수 있는 유일한 형식이다.
문제는 LLM이 영어를 hex 코드로 바꿀 수 있는지가 아니다. 그 과정을 CI에서 실행할 만큼 신뢰할 수 있는 것으로 만들 수 있는지가 문제다.
theme-to-code는 실제로 어떻게 작동하는가
아키텍처는 간단명료하다. 평이한 영어로 짧은 테마 매니페스트를 작성한다. 스크립트가 제한된 출력 형식으로 LLM에 전달한다. LLM이 구조화된 토큰을 반환한다. 해당 토큰을 디스크에 쓰고, 타입 검사를 수행하며, 커밋한다.
파이프라인은 다음과 같다.
- 매니페스트: 산문으로 테마를 설명하는
.txt또는.md파일. - 스키마: LLM이 반환해야 하는 Zod(또는 JSON Schema) 정의.
- 컴파일러: 매니페스트를 읽고, 구조화된 출력으로 LLM을 호출하고,
tokens.json을 쓰는 스크립트. - 게이트: 생성된 토큰이 스냅샷에서 벗어나거나 제약을 위반할 경우 실패하는 테스트.
핵심은 세 번째 단계다. 모델에게 “다크 테마를 생성하라”고 요청하는 것이 아니다. 스키마를 채우라고 요청하는 것이다. 이것은 창작 글쓰기 작업을 구조화된 데이터 추출 작업으로 바꾸며, LLM은 이 분야에서 훨씬 뛰어나다.
TypeScript로 작동하는 컴파일러
다음은 최소한이지만 완전한 구현이다. 테마 설명을 읽고, Zod 스키마로 OpenAI를 호출하며, 결과를 JSON 파일에 쓴다.
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import OpenAI from "openai";
// 1. Define the bounded context: only these tokens exist
const TokenSchema = z.object({
name: z.string(),
colors: z.object({
background: z.string().regex(/^#[0-9a-f]{6}$/i),
surface: z.string().regex(/^#[0-9a-f]{6}$/i),
primary: z.string().regex(/^#[0-9a-f]{6}$/i),
text: z.string().regex(/^#[0-9a-f]{6}$/i),
muted: z.string().regex(/^#[0-9a-f]{6}$/i),
}),
spacing: z.object({
unit: z.number().min(4).max(16),
scale: z.array(z.number()).length(4),
}),
radii: z.object({
sm: z.number(),
md: z.number(),
lg: z.number(),
}),
});
type TokenSet = z.infer<typeof TokenSchema>;
// 2. The compiler
async function compileTheme(
description: string,
apiKey: string
): Promise<TokenSet> {
const client = new OpenAI({ apiKey });
const completion = await client.chat.completions.create({
model: "gpt-4o",
messages: [
{
role: "system",
content:
"You are a design-token compiler. " +
"Convert the user's theme description into the exact JSON schema provided. " +
"All colors must be valid 6-digit hex. " +
"The spacing unit must be a multiple of 4. " +
"The scale array must have exactly 4 values.",
},
{
role: "user",
content: description,
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "theme_tokens",
strict: true,
schema: zodToJsonSchema(TokenSchema),
},
},
});
const raw = JSON.parse(completion.choices[0].message.content!);
return TokenSchema.parse(raw);
}
// 3. CLI entrypoint
async function main() {
const description = await Bun.file("theme.manifest.txt").text();
const tokens = await compileTheme(description, process.env.OPENAI_API_KEY!);
await Bun.write("tokens.json", JSON.stringify(tokens, null, 2));
console.log("Compiled theme:", tokens.name);
}
main();
의존성으로 zod, openai, zod-to-json-schema가 필요하다. 스크립트는 Bun을 사용한다고 가정하지만, Bun.file과 Bun.write를 fs.promises로 교체하는 것은 trivial하다.
무시할 수 없는 트레이드오프
이것은 작동하지만 공짜가 아니다.
결정론. temperature를 0으로 설정하고 구조화된 출력을 활성화하더라도, 동일한 설명이 모델 버전 간에 약간 다른 hex 코드를 생성할 수 있다. OpenAI 업데이트 후 컴파일러를 다시 실행하면 스냅샷 테스트가 실패할 수 있다. 생성된 tokens.json을 커밋하고 매니페스트가 변경될 때만 다시 컴파일해야 한다.
지연 시간. 빌드 단계에서 LLM을 호출하면 수 초, 때로는 수십 초가 추가된다. 매 핫 리로드마다 테마를 컴파일하지 마라. 매니페스트 파일이 변경될 때 pre-commit hook이나 CI에서 컴파일러를 실행하라.
스키마 경직성. LLM은 새로운 토큰 카테고리를 발명할 수 없다. 나중에 디자인 시스템에 elevation.shadows.xl이 필요하면, 모델이 출력하기 전에 Zod 스키마, 시스템 프롬프트, 컴파일러를 업데이트해야 한다. 이것은 버그가 아니라 기능이다. DSL을 한정된 상태로 유지한다.
접근성. LLM은 프라이머리 색상이 배경에 대해 WCAG 대비 비율을 충족하는지 알지 못한다. 생성된 토큰은 별도의 검사로 검증해야 한다. TokenSchema.parse 호출 후에 다음을 추가하라.
function contrastRatio(hex1: string, hex2: string): number {
// WCAG relative luminance calculation
const lum = (hex: string) => {
const rgb = parseInt(hex.slice(1), 16);
const [r, g, b] = [(rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff];
const f = (c: number) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
};
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
};
const l1 = lum(hex1) + 0.05;
const l2 = lum(hex2) + 0.05;
return l1 > l2 ? l1 / l2 : l2 / l1;
}
// After parsing tokens
const ratio = contrastRatio(tokens.colors.primary, tokens.colors.background);
if (ratio < 4.5) {
throw new Error(`Contrast ratio ${ratio.toFixed(2)} fails WCAG AA`);
}
붕괴하는 경우: 상호작용 문제
이 접근 방식은 색상 팔레트, 스페이싱 스케일, 서체 패밀리에 대해 훌륭하게 작동한다. 하지만 토큰이 산문으로 설명하기 어려운 방식으로 상호작용할 때 붕괴한다.
시맨틱 토큰은 전형적인 함정이다. “링크와 버튼에 프라이머리 색상을 사용하라”고 썼다고 하자. 그런데 비활성화된 버튼은 어떻게 하는가. 이미 프라이머리 색상을 사용하는 경고 배너 내부의 링크는 어떻게 하는가. 이것들은 테마 수준의 결정이 아니다. 컴포넌트 수준의 규칙이며, 매니페스트 파일이 아닌 컴포넌트 라이브러리에 속한다.
매니페스트를 프리미티브 토큰으로 한정하라. 시맨틱은 컴포넌트가 처리하게 하라.
자주 묻는 질문
한정된 컨텍스트 DSL이란 무엇인가. 범위가 좁은 작은 언어이다. 이 경우 “언어”란 엄격한 스키마에 의해 지배되는 디자인 토큰만을 설명하는 영어 산문이다.
동일한 설명이 항상 동일한 토큰을 생성하는가. 항상 그렇지는 않다. LLM 출력은 모델 버전과 제공업체에 따라 변동된다. 생성된 토큰을 커밋하고 스냅샷 테스트로 드리프트를 감지하라.
컴포넌트 수준 스타일에 이것을 사용할 수 있는가. 아니다. 매니페스트는 색상과 스페이싱과 같은 프리미티브 토큰만 정의해야 한다. 컴포넌트 시맨틱은 컴포넌트 라이브러리에 속한다.
OpenAI가 필요한가, 아니면 다른 모델도 작동하는가. 구조화된 JSON 출력을 지원하고 시스템 프롬프트를 따르는 모든 모델이 작동한다. Llama 3.3과 같은 로컬 모델도 빌드 파이프라인에서 충분히 빠르게 실행할 하드웨어가 있다면 실행 가능하다.
열 줄짜리 매니페스트와 엄격한 스키마로 시작하라
마케팅 랜딩 페이지나 관리자 대시보드와 같이 하나의 한정된 컨텍스트를 선택하라. 열 줄짜리 매니페스트를 작성하고, 선택적 필드가 없는 Zod 스키마를 정의하고, 첫 번째 토큰 파일을 생성하라. 출력을 커밋하라. 매니페스트를 다시 컴파일하고 커밋된 스냅샷과 비교하는 테스트를 작성하라.
매니페스트가 변경되지 않았는데 스냅샷이 드리프트하면 컴파일러가 결정론적이지 않은 것이다. 시스템 프롬프트를 수정하거나 모델 버전을 고정하여 결정론적이 될 때까지 대처하라.
당신의 디자인 시스템에는 또 다른 Figma 플러그인이 필요하지 않다. 디자이너와 개발자가 모두 읽을 수 있는 진실의 원천이 필요하다. 평이한 영어는 읽을 수 있다. 스키마가 그것을 신뢰할 수 있게 만든다.