개발 블로그
회고 4 분 소요

아이디어 사이클 회고 #2 — 토큰만 쓴다는 룰 하나로 디자인이 살아남았다

automation claude-code design-tokens css llm-pipeline

결론부터

매일 다른 LLM 출력으로 만든 데모 30+편이 한 사이트처럼 보이는 유일한 이유public/tokens.css 하나에 색·타이포·간격·radius를 전부 박아두고, npm run lint:design으로 hex literal과 raw px font-size를 커밋 전에 차단하기 때문이다. LLM에게 "예쁘게 만들어줘"는 통하지 않는다. **"이 변수 외에는 쓰지 마"**가 통한다.

룰 하나, 린트 하나. 그 둘이 디자인 시스템 전부다.

토큰 파일 한 장이 디자인 시스템

public/tokens.css는 약 110줄이다. 색 팔레트(brand emerald + 4단 surface ladder + ink 4단), 8px 기반 spacing 스케일(--space-xxs부터 --space-section까지), radius 7단, 그리고 type ramp(--fs-display-xl 60px부터 --fs-mono 13px까지)가 전부 들어있다.

각 사이클 페이지는 <head> 맨 위에서 이걸 한 줄로 <link>한다.

public/cycles/2026-05-08-some-slug/index.html
<link rel="stylesheet" href="/tokens.css">
<style>
  body {
    background: var(--canvas);
    color: var(--ink);
    font-size: var(--fs-body);
    padding: var(--space-lg);
  }
</style>

오케스트레이터 프롬프트(scripts/cycle-prompt.md)의 Stage 4에 못 박혀 있다 — <link rel="stylesheet" href="/tokens.css"> 필수, var(--*) 외 hex literal 금지, raw px font-size 금지. LLM이 색을 새로 발명할 여지를 아예 없앤다.

이름이 추상적인 토큰(

--surface-3, --ink-muted)이 의외로 핵심이었다. "회색"이라고 두면 LLM이 매번 다른 회색을 쓴다. "ladder의 3단"이라고 두면 다른 사이클이 같은 회색을 고른다.

린트가 룰을 지켜낸다

토큰 룰은 README에 적어두는 것만으론 안 지켜진다. scripts/lint-design.mjs 한 파일이 그걸 강제한다 — package.jsonlint:design 스크립트가 이걸 부른다.

scripts/lint-design.mjs
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
const PX_FONT_RE = /font-size\s*:\s*\d+(?:\.\d+)?\s*px/gi;
// 추가로 검사하는 항목:
//  3) cycles.json id/date/slug 일관성
//  4) <html lang="ko"> 누락
//  5) /tokens.css <link> 누락
//  6) HTML+CSS+JS 800줄 예산 초과 경고

스캔 대상은 public/cycles/ 아래 모든 .html / .css다. HTML 안의 <style> 블록도 추출해서 같이 검사한다. 위반이 하나라도 나오면 exit 1 — Stage 4의 Implementer는 커밋 전에 반드시 이걸 통과시켜야 하고, 위반이 나면 룰을 끄는 게 아니라 토큰으로 갈아끼우는 방향으로 고치라고 프롬프트에 못 박혀 있다.

"린트를 suppress하지 말고 토큰으로 바꿔라"는 한 줄이 의외로 중요했다. LLM은 룰을 우회하는 데 창의적이라, 길을 닫지 않으면

// eslint-disable 류의 우회를 만들어낸다. 우회 자체를 옵션에서 제거해야 한다.

부수 효과로, lint는 디자인 외에도 사이클 폴더 구조까지 같이 잡아준다 — <html lang="ko"> 빠뜨리면 fail, tokens.css <link> 빠뜨리면 fail, cycles.jsoniddate가 어긋나면 fail. 800줄 예산 초과는 fail이 아니라 warn(깊이를 위한 예산이지 boilerplate를 위한 게 아니라서). 한 곳에서 전부 본다.

다음 편 예고

다음 편은 두 번째 가드레일 — Stage 5의 QA Tester가 왜 Playwright MCP를 박고 있어야만 매일이 굴러가는지, 그리고 실패한 사이클이 커밋되기 전에 커밋을 abort시키고 텔레그램 3계층 알림이 어디로 떨어지는지에 대한 이야기다.