AI가 사용할 디자인 시스템은 무엇이 달라야 할까

규칙을 문서에 적는 것과 시스템에 심는 것의 차이

본문에는 당시 실제로 적용한 구조와, 운영하면서 확인한 한계를 바탕으로 지금이라면 추가할 검증이 함께 들어 있다. 후자는 해당 문단에서 제안으로 구분했다. 생성 성공률이나 리뷰 시간을 정량적으로 측정하지는 못했으므로, AI의 정확도가 몇 퍼센트 높아졌다거나 개발 시간이 얼마나 줄었다는 말은 이 글에 없다. 관찰할 수 있었던 변화는 사람이 리뷰할 때 반복해서 확인해야 하는 범위였다.

AI에게 일을 맡기기 전에 먼저 걸린 질문이 있었다. 이 일을 굳이 AI에게 맡겨야 하는가. 맡긴다면 어디까지 맡겨도 되는가.

처음에는 세 가지를 살폈다.

  • 처리 과정과 기대하는 결과를 규칙으로 충분히 명세할 수 있는가
  • 생성된 결과를 기계적으로 검증할 수 있는가
  • 잘못됐을 때 사람이 승인해야 할 만큼 영향이 크거나 되돌리기 어려운가

처리 과정과 결과가 이미 정해진 반복 작업이라면 스크립트나 정적 도구로 만드는 편이 낫다. 같은 입력에 기대하는 결과가 분명한 일을 매번 자연어로 설명하고 AI의 판단을 거치게 할 이유는 적다.

여러 조건을 함께 해석해야 하지만 결과를 검증할 수 있는 작업에는 AI를 활용할 여지가 있다. 다만 생성과 검증을 모두 AI의 판단에만 맡기면, 잘못된 결과가 그럴듯한 상태로 남기 쉽다.

프론트엔드에서 Figma를 코드로 옮기는 일은 어디에 가까울까? 지금은 Figma 노드를 AI에게 전달하고 코드를 생성하게 할 수 있다. 이미 존재하는 컴포넌트를 가져오고, JSX를 작성하고, 디자인에 맞춰 배치하는 작업을 사람이 전부 입력해야 할 이유는 줄어들고 있다.

그런데 실제로 맡겨보면 생각보다 자주 틀렸다. 이미 있는 디자인 시스템 컴포넌트를 두고 비슷한 UI를 새로 만들기도 했다. 올바른 컴포넌트를 찾은 뒤 허용되지 않은 prop을 사용하거나, 제품 코드에서 직접 쓰지 않기로 한 스타일을 다시 추가하기도 했다. 화면은 비슷해 보여도 클릭 영역과 스크롤 구조가 달랐다.

이 실패에는 공통점이 있었다. 화면만 보면 대체로 맞아 보였다는 것이다. 빌드도 통과하고 타입 오류도 없으니, 잘못됐다는 사실은 사람이 디자인과 비교하거나 실제로 눌러봐야 드러났다.

Figma를 코드로 옮기는 일은 AI에게 맡길 만해 보이는데, 왜 안정적으로 위임하기 어려웠을까?

Figma에서 코드로 가는 길에는 하나의 정답만 있지 않았다

Figma에는 화면을 구현하는 데 필요한 정보가 많이 들어 있다.

  • 어떤 컴포넌트의 instance인지
  • 어떤 variant와 property가 선택됐는지
  • 어떤 색상과 spacing이 적용됐는지
  • 각 요소의 크기와 배치가 어떤지
  • 어떤 디자인 token을 사용했는지

이런 정보는 비교적 정형화된 방식으로 읽을 수 있다. 하지만 실제 제품 코드를 만들기 위해서는 Figma에 직접 드러나지 않는 결정도 필요하다.

  • 이 행 전체가 링크인지 버튼인지
  • 오른쪽에 놓인 요소가 상태 정보인지 독립적인 액션인지
  • 어느 영역이 페이지의 스크롤을 소유하는지
  • 로딩, 오류, 빈 상태를 어느 계층에서 처리하는지
  • 기존 페이지 패턴 중 무엇을 재사용해야 하는지
  • 현재 애플리케이션 구조의 어느 레이어에 코드를 두어야 하는지
  • 글자를 확대하거나 키보드로 탐색할 때 어떻게 동작해야 하는지
  • 디자인 시스템에 없는 예외를 허용해도 되는지

예를 들어 List 오른쪽에 chevron이 있는 화면과 좋아요 버튼이 있는 화면은 비슷하게 보일 수 있다. 하지만 chevron은 행의 이동 가능성을 보여주는 시각 정보일 수 있고, 좋아요 버튼은 행과 별도로 실행되는 액션이다.

전체 행이 링크라면 chevron은 링크 안에 포함될 수 있다. 반면 독립적으로 동작하는 버튼은 링크 안에 중첩해서는 안 된다. Figma에서 두 요소가 모두 오른쪽 끝에 놓였다는 이유만으로 같은 코드 구조를 사용할 수는 없다.

이런 판단에는 디자이너와 개발자가 제품 안에서 합의한 규칙이 들어간다.

단계 성격
Figma property와 token 읽기 정형화하고 자동화하기 쉬움
연결된 코드 컴포넌트 찾기 명시적인 mapping으로 자동화 가능
정보 구조와 상호작용 해석 제품 맥락에 따른 판단 필요
기존 제품 패턴과 조합 조직의 규칙과 저장소 맥락 필요
타입과 import 규칙 검사 기계적으로 검증 가능
실제 레이아웃과 접근성 확인 실행 환경에서 검증 필요
새로운 예외의 허용 여부 사람의 판단과 승인 필요

Figma-to-Code 전체를 하나의 변환 스크립트처럼 볼 수는 없었다. 대신 자동화할 부분, AI가 해석할 부분, 시스템이 검증할 부분, 사람에게 남길 판단을 나눌 수는 있었다.

Code Connect가 연결한 것과 남겨둔 것

처음에는 Figma MCP와 Code Connect를 활용했다.

Figma의 디자인 시스템 컴포넌트를 실제 코드 컴포넌트와 연결하고, 디자인 시스템 패키지 안에는 Code Connect mapping을 함께 두었다. Figma 노드를 AI에게 전달하면 실제 import 경로와 컴포넌트 사용 예제, Figma property와 코드 prop의 대응 관계를 참고할 수 있는 구조였다.

Figma 공식 문서에 따르면 Code Connect는 MCP 서버 컨텍스트에 디자인 property, import 문, 컴포넌트 사용 snippet, 그리고 코드 생성을 위한 custom instruction을 제공한다.

이때 mapping을 만든 방식에 따라 전달되는 정보의 밀도가 다르다. CLI로 만든 mapping은 코드베이스의 실제 구현 예제와 명시적인 prop mapping, 컴포넌트 소스 경로를 함께 전달한다. Figma UI에서 만든 mapping은 어떤 컴포넌트를 쓸지와 기본 사용 패턴은 알려주지만 전체 snippet을 기본으로 포함하지 않고, “Add instructions for MCP”로 사용 규약을 직접 적어 밀도를 보완하는 구조다. 팀이 어느 방식을 쓰는지에 따라 AI가 받는 정보도 달라진다.

이 연결은 필요했다. AI가 이미 존재하는 컴포넌트를 찾지 못하거나 import 경로를 추측하는 문제를 줄이는 데 도움이 됐고, 디자인만 보고 비슷한 UI를 새로 만드는 대신 실제 코드 컴포넌트를 먼저 확인하게 만들 수 있었다.

하지만 이것만으로는 충분하지 않았다. Code Connect가 MCP에 제공하는 것은 컴포넌트의 구현 정보와 사용 컨텍스트다. 이 정보는 올바른 컴포넌트를 찾는 데 도움을 주지만, 생성된 JSX의 모든 조합을 검사하는 validator로 동작하지는 않는다.

연결된 컴포넌트와 대표적인 사용 예제는 알 수 있었지만, 그다음 질문은 남았다.

  • 이 화면에서 그 컴포넌트를 사용해도 되는가
  • 두 컴포넌트를 함께 사용할 수 있는가
  • 같은 위치에 놓인 요소가 같은 역할을 갖는가
  • type에서 해당 content 조합이 허용되는가
  • 어떤 DOM 구조를 가져야 하는가
  • 어느 레이어에서 import할 수 있는가
  • 페이지 골격과 스크롤은 누가 소유하는가

Code Connect가 컴포넌트 사이의 번역 문제를 줄여줬다면, 남아 있던 것은 선택과 조합의 문제였다. 이 차이를 저장소 안의 규칙으로 보완하기 시작했다.

관련 문서: Code Connect integration with the Figma MCP server

디자인 시스템 규칙을 저장소 안으로 가져오기

먼저 generate-design-system-rules라는 skill을 만들었다. 이 skill은 디자인 시스템 패키지를 기준으로 DESIGN_SYSTEM_RULES.md를 생성했다. 문서에는 컴포넌트별 사용 목적과 주요 prop, 허용되는 조합, 피해야 할 사용 방식을 담았다.

그다음에는 figma-to-code skill을 만들었다. Figma 노드 ID를 전달하면 디자인 정보를 읽고, Code Connect로 실제 컴포넌트를 확인한 뒤, 저장소 안의 디자인 시스템 규칙을 살펴보고 구현을 시작하게 했다.

text
Figma 노드 ID 전달

Figma MCP로 디자인 정보 확인

Code Connect로 실제 컴포넌트와 prop mapping 확인

저장소의 디자인 시스템 규칙 확인

제품 코드 구현

이 구조를 추가한 뒤 AI가 저장소의 디자인 시스템을 먼저 살펴보는 경우가 늘었다. 디자인만 보고 비슷한 UI를 새로 만드는 대신, 실제 컴포넌트와 기존 사용 방식을 찾아보게 할 수 있었다.

그런데 컴포넌트가 늘면서 다른 문제가 생겼다. 모든 규칙을 하나의 Markdown 문서에 모으니 파일이 계속 길어졌다. 특정 컴포넌트 하나를 구현하는 작업에도 다른 컴포넌트의 세부 규칙과 예외가 함께 들어왔다. 규칙을 추가할수록 참고할 정보는 많아졌지만, 매 작업에서 소비하는 컨텍스트도 커졌다.

오래된 규칙이 남을 가능성도 있었다. 컴포넌트는 제거됐는데 중앙 문서의 사용 규칙은 사라지지 않거나, 구현이 바뀌었는데 규칙 문서는 갱신되지 않을 수 있었다. 그래서 규칙의 경계를 바꿨다.

규칙 문서도 컴포넌트와 같은 경계에 두기

모든 규칙을 하나의 Markdown 파일에 넣지 않고, 각 컴포넌트와 가까운 위치에 해당 컴포넌트의 규칙을 두었다.

text
design-system/
├── AGENTS.md
├── CLAUDE.md
└── components/
    ├── list/
    │   ├── AGENTS.md
    │   ├── CLAUDE.md
    │   ├── List.tsx
    │   └── Code Connect mapping
    ├── page-frame/
    │   ├── AGENTS.md
    │   ├── CLAUDE.md
    │   ├── PageFrame.tsx
    │   └── Code Connect mapping
    └── button/
        ├── AGENTS.md
        ├── CLAUDE.md
        ├── Button.tsx
        └── Code Connect mapping

당시에는 AGENTS.md를 규칙의 기준으로 삼고, 같은 위치의 CLAUDE.md가 이를 가리키도록 구성했다. 도구에 따라 symlink나 import 문법을 사용할 수도 있고, 상위 문서에서 하위 규칙 문서로 링크를 제공할 수도 있다.

다만 일반적인 Markdown 링크가 있다는 이유만으로 도구가 해당 문서를 반드시 읽는 것은 아니다. 중요한 것은 파일 이름이나 자동 탐색 방식보다 작업 흐름이었다. figma-to-code skill에는 Code Connect가 알려준 컴포넌트의 실제 파일과 관련 규칙 문서를 함께 확인하도록 지시했다.

text
Figma 노드 ID 전달

Code Connect로 실제 컴포넌트 확인

해당 컴포넌트의 구현과 규칙 문서 확인

제품 코드 구현

List를 구현할 때는 List의 규칙을 읽고, Page Frame을 사용할 때는 Page Frame의 규칙을 읽었다. 모든 규칙을 시작 컨텍스트에 넣는 대신, 필요한 규칙으로 이동할 경로를 작업 절차에 넣은 것이다.

이 배치의 장점은 컨텍스트 크기만이 아니었다. List가 제거되면 List 구현과 규칙, Code Connect mapping을 같은 경계에서 함께 제거할 수 있었다. 규칙이 바뀌었을 때 어떤 구현과 같이 검토해야 하는지도 이전보다 분명했다.

컴포넌트 구현, Figma와 코드의 연결, AI가 읽는 규칙을 같은 변경 단위에 둔 셈이다. 코드에서 모듈의 책임을 나누듯이 AI에게 제공하는 문서도 책임에 따라 나눌 수 있었다.

하지만 문서를 나눈다고 자연어 규칙의 한계까지 사라지는 것은 아니었다.

자연어로 설명할 수 있는 것과 강제할 수 있는 것은 달랐다

List는 시각적으로 단순해 보여도 조합 규칙이 많았다. 행의 형태에 따라 사용할 수 있는 content가 달랐고, 오른쪽 영역에 무엇이 들어가는지에 따라 상호작용 구조도 달라졌다. 같은 위치에 놓인 요소라도 역할은 다를 수 있었다.

Trailing Visual에 해당하는 것은 가격, 상태, badge, chevron처럼 행의 주된 의미를 보조하는 요소다. Secondary Action에 해당하는 것은 좋아요, 더보기, Switch처럼 행과 독립적으로 실행되는 요소다. 화면에서는 둘 다 행의 오른쪽에 놓인다.

문서에는 이 차이를 적을 수 있었다.

navigation 형태에서는 독립적인 secondary action을 사용하지 않는다.
대표 액션 안에 다른 interactive element를 중첩하지 않는다.
selection은 content 종류가 아니라 행의 interaction과 state로 다룬다.

AI가 이 규칙을 읽고 올바르게 구현하는 경우도 있었다. 하지만 문서에 규칙이 있다는 이유만으로 항상 지켜지는 것은 아니었다. 설명이 길어지면 일부 조건을 놓쳤고, 서로 비슷한 예제 중 잘못된 쪽을 고르기도 했다.

타입이 허용하고 화면도 그럴듯하게 보이면 규칙을 어긴 코드가 그대로 남을 수 있었다. 프롬프트와 규칙 문서를 더 자세히 쓰는 것만으로는 이 문제를 안정적으로 해결하기 어려웠다. 자연어 지침은 선택에 필요한 맥락을 제공하지만, 그 자체가 강제 설정은 아니다.

여기서 질문을 바꿨다. AI가 모든 규칙을 빠짐없이 이해하게 만들 수 있을까? 그 질문에 계속 매달리는 대신, 시스템에 표현할 수 있는 규칙을 어겼을 때 실패가 발생하도록 만들 수 있는지를 보기 시작했다.

선택지, 실패, 피드백으로 나누기

문제를 세 개의 축으로 나눴다.

언제 작동하는가 무엇을 하는가
선택지를 줄인다 코드 생성 전 AI가 고를 수 있는 후보를 좁힌다
규칙 위반을 실패로 만든다 코드 생성 후 시스템에 표현한 규칙을 어긴 결과를 거부한다
실패를 다음 시도에 돌려준다 검사 실패 후 무엇을 바꿔야 하는지 판단할 정보를 제공한다

첫 번째 축에는 Code Connect, 컴포넌트별 규칙 문서, 제한된 스타일 API, semantic token, Page Frame이 들어간다. 두 번째 축에는 TypeScript, ESLint, 빌드, 테스트, 시각적 회귀 테스트가 들어간다.

세 번째 축은 특정 도구 하나로 만들어지지 않았다. 오류 메시지에 무엇이 담기는지, 검사를 어떤 순서로 실행하는지, 제약을 우회할 때 어떤 승인 경계를 두는지가 함께 필요했다. 이 축이 가장 자주 빠진다. 실패를 만드는 도구는 대부분 이미 프로젝트에 있고, 그 실패가 다음 시도에 쓸 수 있는 형태인지는 따로 설계해야 하기 때문이다.

역할도 이 축을 따라 나뉘었다.

  • AI는 요구사항을 해석하고, 컴포넌트와 recipe를 선택하고, 기존 패턴을 조합해 코드를 생성한다.
  • 시스템은 prop 조합, import 경계, 금지된 스타일, 빌드와 테스트로 표현할 수 있는 규칙을 검사한다.
  • 사람은 새로운 패턴과 예외의 허용 여부, 제품 의도, 상호작용의 의미, 디자인 시스템 규칙 자체의 변경을 판단한다.

목표는 AI가 한 번에 완벽한 코드를 생성하게 만드는 것이 아니었다. 처음부터 올바른 결과가 나오지 않더라도, 표현해 둔 규칙을 어긴 결과가 조용히 통과하지 않고 실패 원인이 다음 시도에 돌아오면 다시 수정할 수 있었다.

잘못된 조합은 TypeScript에서 실패하게 한다

자연어로만 존재하던 규칙 중 일부는 TypeScript로 옮길 수 있었다. 다음은 당시의 실제 타입을 그대로 옮긴 것이 아니라, List에서 사용했던 설계 방향을 단순화한 예시다.

typescript
type NavigationRow = {
  interaction: 'navigation';
  href: string;
  onAction?: never;
  secondaryAction?: never;
};

type CommandRow = {
  interaction: 'command';
  href?: never;
  onAction: () => void;
  secondaryAction?: SecondaryAction;
};

type StaticRow = {
  interaction: 'static';
  href?: never;
  onAction?: never;
  secondaryAction?: never;
};

type ListRowProps = BaseRowProps &
  (NavigationRow | CommandRow | StaticRow);

interaction이 판별자 역할을 하므로, 이 구조에서는 navigation row에 onAction이나 독립적인 secondaryAction을 넣을 수 없다.

typescript
<ListRow
  interaction="navigation"
  href="/detail"
  secondaryAction={<FavoriteButton />}
  // TypeScript 오류:
  // navigation에서는 secondaryAction을 받을 수 없다.
/>

문서는 어떤 선택이 올바른지 설명한다. 타입은 사용할 수 없는 선택을 코드에 남기지 못하게 한다.

물론 모든 규칙을 TypeScript로 표현할 수는 없다. 서버나 CMS에서 들어오는 데이터에는 런타임 검증이 필요하다. ReactNode를 받는 자유로운 영역의 내부 구조도 TypeScript만으로 완전히 통제하기 어렵다. 타입상 가능하더라도 제품 정책에서는 사용하지 않는 조합이 남을 수 있다.

타입 오류가 올바른 수정 방향까지 알려주는 것도 아니다. 프로젝트의 TypeScript 버전과 설정에 따라 문구는 달라지지만, 위 코드가 만드는 오류의 핵심은 이 한 줄로 좁혀진다.

text
Type 'Element' is not assignable to type 'undefined'.

실제 출력은 이 줄을 props 객체 전체를 나열한 메시지 안에 중첩해서 보여준다. 정작 필요한 정보는 가장 안쪽에 있고, 그마저도 secondaryAction이 허용되지 않는다는 사실까지만 알려준다.

무엇으로 바꿔야 하는지는 알 수 없으므로, 이 오류를 받은 쪽이 선택할 수 있는 수정은 여러 개다.

  • secondaryAction을 제거한다
  • interactioncommand로 바꾸고 href를 제거한다
  • 행 전체의 구조를 다른 recipe로 바꾼다
  • 타입 오류를 억제하거나 단언으로 우회한다

필요한 주변 prop까지 함께 수정하면 앞의 세 방향 모두 타입을 통과할 수 있다. 하지만 첫 번째는 디자인에서 요구한 버튼을 없앨 수 있고, 두 번째는 행 전체가 링크여야 한다는 요구사항을 깨뜨릴 수 있다. 세 번째가 올바른지는 요구사항과 기존 패턴을 다시 확인해야 알 수 있다.

타입은 거부에는 강하지만 안내에는 약했다. 그래도 자연어 지침만 있을 때와는 차이가 있었다. AI가 규칙을 기억하기를 기대하는 대신, 표현 가능한 규칙을 어기면 실패가 발생했다.

import와 아키텍처 규칙은 lint로 옮긴다

잘못된 import도 비슷했다. Feature-Sliced Design처럼 레이어 간 의존 방향이 정해진 구조에서는 특정 레이어가 다른 레이어의 내부 구현을 직접 참조하면 안 된다. 디자인 시스템 역시 공개 진입점이 아닌 내부 파일을 제품 코드가 직접 import하지 않도록 제한할 수 있다.

디자인 시스템의 내부 컴포넌트를 직접 import하지 않는다.

이 문장을 규칙 문서에만 적어두는 것보다, 금지된 import에 lint error를 발생시키는 편이 정확했다.

no-restricted-imports를 사용하면 특정 모듈이나 내부 경로의 import를 제한할 수 있다. 레이어 간 의존 방향을 검사할 때는 import/no-restricted-pathseslint-plugin-boundaries처럼 그 목적에 맞는 규칙을 사용할 수도 있다. raw <a>나 인라인 style처럼 import가 아닌 문법은 no-restricted-syntax나 JSX를 검사하는 별도 lint로 다룰 수 있다.

반복해서 문제가 된 규칙에는 전용 lint가 더 적합할 수 있었다.

text
design-system/no-private-import
design-system/no-raw-navigation
design-system/no-inline-style
architecture/no-invalid-layer-import

여기서 규칙 이름보다 중요한 것은 오류 메시지였다.

text
next/link를 직접 import하지 마세요.
제품의 탐색 정책이 적용된 NavigationLink를 사용하세요.
외부 URL이면 external variant를 선택하세요.

세 줄이 각각 다른 일을 한다. 무엇이 금지됐는지, 대신 무엇을 써야 하는지, 예외 상황에서는 어떻게 갈라지는지다. 사람은 짧은 오류만 보고도 코드베이스를 탐색해 수정 방향을 찾을 수 있지만, AI에게는 가능한 대안까지 포함한 메시지가 훨씬 직접적인 피드백이 됐다.

TypeScript의 오류가 “이 조합은 허용되지 않는다”에서 멈춘다면, lint는 “왜 허용되지 않으며 대신 무엇을 살펴봐야 하는가”까지 알려줄 수 있었다.

모든 타입 규칙을 lint에 다시 구현할 필요는 없다. 자주 실패하고, 타입 오류만으로는 수정 방향을 찾기 어려운 규칙부터 선택적으로 옮기는 편이 낫다. 같은 규칙을 여러 계층에서 관리하는 데도 비용이 들기 때문이다.

Tailwind를 없애지 않고 문자열의 경계를 줄이기

스타일에서도 조용한 실패가 있었다. Tailwind는 빠르게 스타일을 작성하기 좋았고 디자인 시스템 내부 구현에서도 계속 유용했다. 문제는 디자인 시스템의 소비처까지 Tailwind 문자열을 직접 작성할 수 있을 때였다.

typescript
<Stack className="gap-4 px-6 bg-white" />

// className은 TypeScript에서 일반 문자열이다.
<Stack className="gpa-4 px-contianer-md" />

동적으로 class name을 만들면 다른 문제도 생긴다.

typescript
<div className={`bg-${tone}-600`} />

Tailwind는 소스를 프로그램 언어로 파싱하지 않고 평문 텍스트로 훑는다. 공식 문서의 표현으로는 “모든 소스 파일을 평문으로 취급하며 코드로 파싱하려 시도하지 않고”, class name에 쓰일 만한 문자로 이루어진 토큰을 찾아 그중 아는 유틸리티만 남긴다. 그래서 문자열 보간으로 조합한 값은 완성된 class 후보로 인식되지 않고, 완성된 형태가 소스에 없으면 해당 CSS도 생성되지 않는다.

공식 문서가 권장하는 해결책은 prop을 class name의 일부에 끼워 넣는 대신, prop을 완성된 class name에 매핑하는 객체를 두는 것이다. 뒤에 나올 mapping이 정확히 그 형태다.

관련 문서: Detecting classes in source files

당시에는 StyleX를 도입하지 않았다. 스타일을 문자열이 아니라 타입이 붙은 값으로 다룬다는 문제의식만 가져와서, Tailwind는 디자인 시스템 내부에서 사용하되 소비처가 Tailwind token이나 class 문자열을 직접 전달하지 못하게 했다.

typescript
// 소비처가 스타일 문자열을 결정하는 API
<Stack className="gap-4 px-6" />

// 디자인 시스템이 선택지를 소유하는 API
<Stack gap="regular" inset="page" />

내부에서는 제한된 prop을 완성된 Tailwind class와 연결했다.

typescript
const gapClass = {
  compact: 'gap-2',
  regular: 'gap-4',
  spacious: 'gap-6',
} as const;

const insetClass = {
  none: '',
  content: 'px-4',
  page: 'px-6',
} as const;

type Gap = keyof typeof gapClass;
type Inset = keyof typeof insetClass;
typescript
type StackProps = Omit<
  React.ComponentPropsWithoutRef<'div'>,
  'className' | 'style'
> & {
  gap?: Gap;
  inset?: Inset;
};

function Stack({
  gap = 'regular',
  inset = 'none',
  ...props
}: StackProps) {
  return (
    <div
      {...props}
      className={`${gapClass[gap]} ${insetClass[inset]}`}
    />
  );
}

실제 구현을 단순화한 예시지만 경계는 이와 비슷했다. Tailwind 문자열을 없앤 것은 아니다. 문자열을 작성할 수 있는 위치를 디자인 시스템 내부로 제한했다.

제품 코드와 AI는 gap-4gap-6 중 하나를 추측하는 대신, 디자인 시스템이 제공한 compact, regular, spacious 중 하나를 선택했다. 존재하지 않는 값을 쓰면 TypeScript 오류가 발생했다.

typescript
<Stack gap="mediumLarge" />

이 구조에도 한계는 남는다. 내부 mapping에 gpa-4 같은 오타가 들어가면 TypeScript는 Tailwind class의 유효성까지 검사하지 못한다. 다만 오류가 발생할 수 있는 위치는 달라졌다. 이전에는 모든 소비처와 AI 생성 코드가 문자열 정확성을 책임졌지만, 이후에는 디자인 시스템 내부의 제한된 mapping만 책임졌다.

당시에는 이 mapping을 별도의 테스트로 검사하지 않았다. 지금 다시 설계한다면 lint를 먼저 붙일 것 같다. eslint-plugin-tailwindcssno-custom-classname이나 eslint-plugin-better-tailwindcssno-unknown-classes는 Tailwind가 아는 유틸리티가 아닌 class name을 오류로 만든다. gpa-4는 여기서 걸린다.

다만 이 규칙은 class 문자열이 어디에 있는지 알아야 동작한다. 기본 설정이 보는 자리는 className 같은 속성, cva·clsx·cn 같은 알려진 호출, 그리고 className·classes·styles 정도의 변수 이름이다. 위 예시의 gapClass는 그중 어디에도 해당하지 않아서, 검사할 선택자에 직접 등록하지 않으면 그냥 지나간다. 문자열을 한곳에 모으는 것과 그 자리를 도구가 아는 것은 다른 일이었다.

Tailwind 빌드 결과와 대조하는 검사를 함께 둘 수도 있다. mapping에 선언된 class가 실제 CSS에 생성됐는지 확인하는 방식인데, 어떤 CSS 결과물을 검사할지와 variant·escaping을 어떻게 처리할지는 프로젝트 설정에 맞춰 정해야 한다.

문자열을 한 곳에 모았다는 것은 적어도 그 한 곳을 검증 대상으로 삼을 수 있게 됐다는 뜻이었다.

문자열이 놓인 자리를 옮기거나, 문자열을 없애거나

여기까지 오면 mapping을 직접 만드는 대신 cva 같은 라이브러리를 쓰는 선택이 보인다. 다만 이유를 정확히 둘 필요가 있다. cva는 Tailwind class의 유효성을 타입으로 보장하지 않는다. VariantProps가 만들어주는 것은 variant의 키와 값에 대한 타입이고, 전달하는 class는 여전히 문자열이다. gpa-4cva 안에서도 타입 오류가 되지 않는다.

달라지는 것은 그 문자열이 놓인 자리다. 앞의 lint 규칙은 cva 호출을 기본 선택자로 이미 알고 있다. 첫 인자로 넘기는 base 문자열만이 아니라 variants 아래의 값과 compoundVariantsclassName까지 포함한다. 앞에서 직접 만든 gapClass와 같은 mapping을 cvavariants로 옮기면, 검사 지점을 따로 등록하지 않아도 gpa-4가 lint 오류가 된다. 타입이 잡지 못하는 것을 lint가 잡되, 그 lint가 볼 수 있는 자리에 선언을 두는 셈이다.

비용도 같이 있다. 선언한 variant의 CSS는 사용 여부와 무관하게 생성된다. Tailwind가 소스를 평문으로 훑기 때문에, 완성된 class 문자열이 소스에 있으면 그 조합이 화면에서 한 번도 렌더링되지 않아도 CSS는 만들어진다. 이것은 cva만의 문제가 아니라 앞의 gapClass mapping도 똑같이 내는 비용이다. 유틸리티가 원자 단위로 공유되기 때문에 증가폭이 크지는 않지만, 임의 값으로 만든 variant가 늘어날수록 그만큼 남는다.

StyleX는 이 지점을 다르게 푼다. 스타일을 문자열이 아니라 타입이 붙은 값으로 다루고, StyleXStyles<{ ... }>로 컴포넌트가 받을 수 있는 속성과 값의 집합까지 타입에 적을 수 있다. 이 글에서 prop과 mapping으로 만들어낸 경계를 도구가 언어 수준에서 제공하는 셈이다. 정적 분석을 전제로 하기 때문에 공식 문서도 컴포넌트 간 스타일 중복 제거와 미사용 스타일 제거를 함께 이야기한다.

그래도 이미 Tailwind로 굴러가는 코드베이스에 StyleX를 권하기는 어렵다. 스타일 작성 방식을 통째로 바꾸는 마이그레이션 비용이 크고, 참고할 수 있는 코드의 양도 Tailwind 쪽이 훨씬 많다. 뒤쪽은 구현을 AI에게 맡길 때 특히 불리하다. 모델이 덜 접한 도구일수록 첫 생성의 정확도가 낮고, 그만큼 루프가 길어진다. 그래서 결론은 도구 이름이 아니었다. 제약을 타입으로 표현할 수 있는 범위가 넓은 도구일수록 유리하고, 그 범위를 넓힐 수 없다면 소비처가 class 문자열을 쓰는 자리를 줄이는 것이 차선이다. 직접 만든 mapping이든 cva든 목적은 같았다.

관련 문서: no-unknown-classes, StyleXStyles, Introducing StyleX

token의 이름도 선택지를 제한하는 API였다

같은 원칙은 디자인 token 이름에도 적용됐다.

blue.500 같은 primitive token은 값을 알려주지만 사용 목적은 알려주지 않는다. 버튼, 링크, 오류 상태, 차트 중 어디에 사용할지는 다시 추측해야 한다.

primitive token을 없앨 필요는 없다. 디자인 시스템 내부에서 semantic token을 구성하는 데 사용하면 된다. 제품 코드에는 color.content.critical, color.surface.primary, color.action.primary.hover, space.page.inline처럼 역할이 드러나는 이름을 노출할 수 있다. 값이 아니라 사용 목적을 API에 담으면 사람과 AI가 추측해야 하는 범위도 줄어든다.

이 방식에는 유지 비용이 따라온다. semantic 계층이 늘어날수록 디자이너와 개발자가 같은 이름 체계를 함께 관리해야 하고, 역할 이름이 지나치게 많아지면 결국 primitive를 다른 철자로 옮겨 적은 것과 다르지 않게 된다. 이름 하나를 추가할 때 그것이 재사용 가능한 결정인지 확인하는 절차가 같이 필요했다.

그래서 이름에 화면의 구체적인 위치까지 넣는 것이 항상 좋은 것도 아니다. user-profile-card-header-title-margin-top 같은 이름은 재사용 가능한 디자인 결정이라기보다 화면별 스타일 저장소에 가까워진다.

AI가 이해하기 좋은 이름을 따로 만들 필요는 없었다. 사람이 제품 의도를 판단하기 좋은 이름은 AI가 추측해야 하는 범위도 함께 줄여줬다.

컴포넌트만큼 반복되는 것은 페이지의 골격이었다

컴포넌트의 사용을 제한해도 페이지 전체 레이아웃은 쉽게 달라졌다. 성숙한 앱에서는 버튼과 List만 반복되지 않는다. 페이지의 골격도 반복된다.

모바일 WebView라면 다음과 같은 영역이 여러 화면에 계속 나타난다.

  • 상단 header와 뒤로가기
  • 페이지의 주 스크롤 영역
  • 콘텐츠 inset
  • safe area
  • 하단 고정 액션
  • 공지나 오류 banner
  • 로딩과 빈 상태가 들어갈 영역

이 구조를 디자인 시스템에서 제공하지 않으면 각 화면이 다시 레이아웃을 결정한다.

화면을 구현할 때는 안쪽부터 시작하기 쉽다. 먼저 카드와 버튼을 만들고 div로 감싼 뒤 margin과 padding을 추가한다. 실제 화면에 붙였을 때 스크롤이 깨지면 overflow: hidden이나 position: absolute를 덧붙인다. 하나씩 보면 작은 선택이지만, 마지막에는 어느 요소가 높이와 스크롤을 소유하는지 설명하기 어려운 구조가 되기도 한다.

한 프로덕트에서 이 순서를 바깥에서 안쪽으로 바꾸는 실험을 했다. 이 글에서는 이를 프레임 우선, 또는 frame-first라고 부르기로 했다. 일반적인 표준 용어라기보다 페이지 골격을 먼저 정한다는 의미로 사용한 표현이다.

  1. Page Frame: viewport, header, main, bottom 영역을 정한다.
  2. Region Ownership: 높이, 스크롤, safe area, overflow의 소유자를 정한다.
  3. Content Composition: List, Form, Card 같은 내용 컴포넌트를 배치한다.

레이아웃 컴포넌트가 반복되는 골격을 소유하게 했다. 다음 코드는 실제 제품의 이름과 구조를 바꾼 예시다.

typescript
<ScreenFrame
  header={
    <ScreenHeader
      title="화면 제목"
      navigation={{
        type: 'back',
        onAction: handleBack,
      }}
    />
  }
  content={
    <ScrollRegion inset="page">
      <PageContent />
    </ScrollRegion>
  }
  bottom={
    <BottomActionBar
      primaryAction={{
        label: '저장',
        onAction: handleSave,
      }}
    />
  }
/>

이 API는 화면의 모양만 제공하지 않는다. header가 차지하는 영역, 주 스크롤 컨테이너, 콘텐츠 inset, 하단 액션의 위치, safe area 처리까지 함께 소유한다. 각 제품 화면이 이 책임을 반복해서 결정하지 않도록 했다.

제품 코드는 height: 100%, min-height: 0, overflow, 고정 영역과 스크롤 영역의 관계를 화면마다 다시 조합하지 않아도 됐다. AI 역시 같은 레이아웃 문제를 매번 처음부터 풀 필요가 줄었다.

Page Frame을 적용한 뒤 달라진 리뷰 범위

이 레이아웃 구조를 모든 프로덕트에 적용하지는 않았고, 앞서 말한 한 프로덕트에서 실험적으로 운영했다.

Page Frame을 사용하기 전에는 AI가 생성한 화면에서 다음 항목을 함께 살펴야 했다.

  • header가 올바른 위치에 있는가
  • 콘텐츠 padding이 제품 기준에 맞는가
  • 어느 영역이 스크롤되는가
  • 높이와 min-height가 맞는가
  • 하단 액션이 콘텐츠와 함께 움직이는가
  • safe area가 빠지거나 중복되지 않았는가
  • 임의의 margin과 overflow로 골격을 보정하지 않았는가

Page Frame을 적용한 뒤에는 이 책임의 상당 부분이 컴포넌트 안으로 들어갔다. AI가 여전히 잘못된 Frame을 선택하거나 content를 잘못된 영역에 배치할 수는 있었다. 그러나 선택한 Frame이 소유하는 header, scroll, inset, bottom 영역의 기본 규칙을 화면마다 다시 검토할 필요는 줄었다.

리뷰에서는 다음 질문에 더 집중할 수 있었다.

  • 이 화면에 맞는 Frame을 선택했는가
  • 콘텐츠가 적절한 영역에 들어갔는가
  • 기존 Frame으로 표현하기 어려운 예외인가
  • 같은 예외가 반복돼 새로운 정식 패턴이 필요한가

정량적인 근거는 없다. 다만 이전보다 구현을 더 많이 맡겨볼 수 있겠다는 체감은 있었다. AI가 더 똑똑해져서라기보다 AI가 직접 결정해야 하는 레이아웃의 범위가 줄었기 때문이다.

강한 제약이 만드는 비용

레이아웃과 컴포넌트 API를 제한하면 일관성을 얻는 대신 유연성을 잃는다.

기존 화면과 다른 프로모션 페이지나 이벤트 화면은 정해진 Frame으로 표현하기 어려울 수 있다. 최상위 레이아웃 컴포넌트의 버그가 여러 화면에 함께 영향을 줄 수도 있다. 디자인 시스템이 모든 예외의 승인을 담당하면 제품 개발의 병목이 될 수 있다.

한 가지 더 예상해 둘 만한 압력이 있다. Frame이 스크롤을 소유하면, 스크롤과 관련된 요구도 함께 Frame으로 올라온다. 목록에서 돌아왔을 때 스크롤 위치를 복원하거나, 무한 스크롤의 관찰 대상을 지정하거나, 특정 요소를 sticky로 고정하는 일이 그렇다. 이런 요구를 하나씩 prop으로 받다 보면 Frame API가 다시 비대해진다. 스크롤 컨테이너에 접근할 수 있는 좁은 통로를 미리 정해두는 편이 나았을 것이다.

그래서 모든 화면에 같은 수준의 제약을 적용하기보다 여러 단계의 API를 둘 필요가 있었다.

구분 용도 제약 수준
Preset Frame 반복되는 정형 화면 높음
Layout Primitive Stack, Split, ScrollRegion을 조합하는 화면 중간
Custom Frame 기존 구조로 표현하기 어려운 화면 낮음

Preset Frame은 header, scroll, inset, bottom action의 정책을 대부분 소유한다. 앞의 ScreenFrame 예시가 여기에 해당한다.

Layout Primitive는 스크롤과 크기 규칙을 유지하면서 화면 구성을 더 유연하게 만든다. 예시의 ScrollRegion은 Preset Frame 안에서도 쓰이고 이 단계에서도 쓰인다.

Custom Frame은 기존 시스템으로 표현하기 어려운 화면을 위한 탈출구다. 다만 사용 이유를 드러내고, 같은 예외가 반복되면 새로운 정식 패턴으로 승격할지를 검토해야 한다.

모든 예외를 잘못된 사용으로 취급하면 디자인 시스템이 실제 제품과 멀어진다. 반대로 모든 예외를 바로 허용하면 제약을 만든 의미가 없어진다.

AI가 사용하기 좋은 시스템을 만든다는 이유로 제품의 표현 가능성을 지나치게 줄여서는 안 됐다. 제약할 영역과 열어둘 영역을 구분하는 것도 디자인 시스템의 책임이었다.

실패를 만드는 것과 돌아오게 하는 것은 다르다

TypeScript와 lint는 잘못된 결과에 실패를 만들 수 있다. 하지만 앞의 타입 오류에서 봤듯이, 실패가 발생했다는 사실만으로 다음 시도가 올바르게 이어지지는 않는다. 같은 오류에서 출발해 요구사항을 깨는 수정과 올바른 수정이 모두 컴파일을 통과할 수 있다.

그래서 오류 메시지와 함께 실행 절차가 중요했다.

text
Figma와 요구사항

관련 컴포넌트와 규칙 탐색

AI가 코드 생성

TypeScript 검사

ESLint와 아키텍처 검사

빌드와 동작 테스트

시각적 회귀 테스트

통과 → 종료
실패 → 원인을 읽고 코드 생성 단계로 되돌아간다

검사는 실행 비용이 낮고 실패 원인이 직접적인 것부터 배치했다. TypeScript와 lint에서 이미 알 수 있는 오류를 확인하기 위해 매번 브라우저 기반 검증까지 기다릴 필요는 없다. 반대로 타입 검사만 통과했다고 실제 화면까지 맞다고 판단해서도 안 된다.

모든 시도에 전체 검증 계층을 돌리면 루프가 길어진다. 빠른 검사와 최종 검사를 나누는 것도 이 흐름의 일부였다.

우회 경로에도 경계가 필요하다

타입과 lint에는 우회 방법이 있다.

text
@ts-expect-error
eslint-disable-next-line
as any
as unknown as

“타입 오류를 고쳐라”라는 지시를 받은 AI가 가장 적은 변경으로 검사를 통과시키려 할 때 선택할 수 있는 길이기도 하다.

우회 경로는 종류별로 다뤄야 한다. @ts-expect-error 같은 TypeScript 지시 주석은 lint 설정으로 금지하거나 사유를 요구할 수 있다. 규칙 이름 없이 모든 lint를 끄는 주석도 제한할 수 있다.

반면 as anyas unknown as 같은 타입 단언은 주석 억제와 다른 문제다. 외부 라이브러리나 타입이 불완전한 경계에서는 정당한 사용도 생기기 때문에 모든 단언을 일괄적으로 금지하기는 어렵다.

금지 가능한 영역은 lint로 좁히고, 필요한 예외에는 사유와 리뷰 경계를 두는 편이 현실적이었다. 제약을 만드는 것만큼, 제약을 우회할 때 이유가 남도록 하는 것도 필요했다.

브라우저 계층이 실제로 검증한 것

마지막 계층은 스크린샷 픽셀 기반의 시각적 회귀 테스트로 운영했다. 접근성 자동 검사는 붙이지 않았다.

VRT는 baseline 이미지와 현재 결과의 차이를 본다. Tailwind class 누락처럼 타입 검사와 빌드를 통과했지만 화면에서만 드러나는 회귀를 잡는 데 잘 맞았다.

다만 이 계층이 무엇을 검증하는지는 정확히 한정해야 했다. baseline이 없는 새 화면에는 비교 대상이 없다. Figma-to-Code는 새로운 화면을 만드는 경우가 많았기 때문에, 당시의 VRT는 AI가 방금 만든 화면이 Figma의 정답과 일치하는지를 검증한 것이 아니었다. 기존 화면의 렌더링이 의도치 않게 달라지지 않았는지를 확인하는 회귀 검사에 가까웠다.

픽셀만으로는 다음을 알 수 없다.

  • 행 전체와 내부 버튼의 DOM 관계가 올바른가
  • 클릭 영역이 제품 규칙과 일치하는가
  • 키보드 탐색 순서가 올바른가
  • 스크롤을 소유해야 할 요소가 실제로 소유하고 있는가
  • 화면은 같지만 의미가 다른 interactive element가 중첩됐는가

도입부의 실패 사례가 여기에 걸린다. chevron이 링크 안에 포함된 화면과 좋아요 버튼이 링크 안에 중첩된 화면은 스크린샷이 같을 수 있다. 이 차이는 VRT로 닫히지 않는다. 같은 목록의 스크롤 구조 문제는 검증이 아니라 Page Frame이 소유권을 가져가면서 줄었다.

접근성 자동 검사를 추가하면 일부 문제를 더 잡을 수 있다. 하지만 자동 검사 역시 모든 DOM과 상호작용 오류를 증명하지는 않는다. 실제로 문제가 된 구조는 fixture와 회귀 테스트로 고정하고, 키보드 탐색과 상호작용은 별도로 확인해야 한다.

이 글에서 다룬 검증은 대부분 코드가 생성된 뒤에 작동한다. 생성 전에 개입하는 계층도 가능하다. 파일이 쓰인 직후 해당 파일만 타입 검사와 lint에 걸어 즉시 되돌려주거나, 특정 경로의 수정과 위험한 작업 자체를 승인 대상으로 만드는 방식이 그렇다. 다만 그것은 당시 구성한 생성 후 검증 흐름의 바깥에 있어 여기서는 제안으로만 남긴다.

계층 담당하는 질문
Figma MCP·Code Connect 선택지 축소 어떤 디자인 요소가 어떤 코드 컴포넌트와 연결되는가
컴포넌트별 규칙 문서 선택지 축소 어떤 상황에서 무엇을 선택해야 하는가
제한된 스타일 API 선택지 축소 소비처가 임의의 스타일 문자열을 만들고 있는가
semantic token 선택지 축소 값이 아니라 사용 목적을 선택하고 있는가
Page Frame 선택지 축소 페이지 골격과 스크롤의 소유자가 누구인가
TypeScript 규칙 위반 실패 이 prop과 state의 조합이 허용되는가
ESLint·구조 검사 규칙 위반 실패 + 피드백 이 import와 문법이 허용되는가, 대신 무엇을 살펴봐야 하는가
동작 테스트 규칙 위반 실패 주요 상태 전환과 동작이 기대와 일치하는가
시각적 회귀 테스트 규칙 위반 실패 기존 화면의 렌더링이 그대로인가
검사 순서와 오류 메시지 피드백 무엇을 어겼고 다음에 무엇을 바꿔야 하는가
사람 판단과 승인 새로운 예외와 제품 의도를 어떻게 결정할 것인가

무엇이 달라졌고 무엇이 달라지지 않았나

처음의 질문으로 돌아가면, Figma에서 코드로 옮기는 작업은 AI에게 위임할 수 있다고 생각한다. 사람이 모든 component import와 JSX, spacing을 직접 입력할 필요는 줄어들고 있다. 이미 존재하는 디자인 시스템과 페이지 패턴 안에서 구현하는 일이라면 AI가 코드를 생성하고, 검증 결과를 읽고, 다시 수정하는 흐름을 만들 수 있다.

다만 “AI에게 위임할 수 있다”와 “AI가 알아서 완성할 수 있다”는 같은 말이 아니다. 반복 구현은 맡길 수 있지만 다음 결정에는 여전히 사람의 판단이 필요하다.

  • Figma만으로 상호작용의 의미를 결정할 수 없을 때
  • 기존 디자인 시스템에 없는 패턴이 필요할 때
  • 접근성과 제품 요구 사이에 판단이 필요할 때
  • 예외를 허용할지 기존 디자인을 수정할지 결정해야 할 때
  • 반복되는 예외를 디자인 시스템으로 승격할지 검토할 때
  • 현재의 제약 자체가 잘못됐거나 현실을 따라가지 못할 때

위임의 기준도 하나가 아니었다. 앞에서 세운 세 질문에 실제로는 하나가 더 붙었다. 평가 기준 자체를 세울 수 있는지다.

  • 처리 과정과 결과를 충분히 명세할 수 있는 반복 작업은 스크립트와 정적 도구로 처리한다.
  • 해석이 필요하지만 결과를 검증할 수 있는 작업은 AI가 생성하고 검증 루프를 반복한다.
  • 평가 기준 자체가 불분명한 작업은 사람의 판단을 가까이 둔다.
  • 영향이 크거나 되돌리기 어려운 작업에는 자동화 가능 여부와 별도로 승인 경계를 둔다.

Figma-to-Code는 두 번째 영역에 가까웠다. 그래서 조직의 UI 규칙을 AI가 찾을 수 있는 자리에 두고, 표현 가능한 제약을 타입과 lint, 컴포넌트 API로 옮기고, 실패가 다시 AI에게 돌아오는 흐름을 만들어야 했다.

그러면 제목의 질문으로 돌아가서, AI가 사용할 디자인 시스템은 무엇이 달라야 할까.

돌아보면 설계 원칙 자체가 달라지지는 않았다. 선택지를 줄이고, 값 대신 의도를 이름에 담고, 반복되는 결정을 컴포넌트가 소유하게 하는 일은 사람만 쓰던 시절에도 좋은 API의 조건이었다. AI를 위한 별도의 규칙 체계를 만들 필요는 없었다.

달라진 것은 세 가지에 가깝다.

첫째, 규칙이 있어야 할 장소가 달라졌다. 사람은 리뷰와 옆자리 대화와 슬랙 스레드에서 규칙을 배울 수 있지만, AI가 도달할 수 있는 곳은 저장소와 도구가 전달하는 컨텍스트뿐이다. 같은 규칙이라도 어디에 적혀 있는지가 결과를 갈랐다.

둘째, 위반이 드러나는 시점이 달라졌다. 사람이 구현할 때는 잘못된 조합이 리뷰에서 걸려도 늦지 않았다. AI 루프에서는 그 자리에서 신호가 오지 않으면 잘못된 코드 위에 다음 수정이 쌓인다. 리뷰까지 미룰 수 있던 검사를 생성 직후로 당겨야 했다.

셋째, 오류 메시지가 결과물의 일부가 됐다. 사람은 짧은 오류만 보고도 코드베이스를 뒤져 맥락을 복원한다. 다음 행동을 오류 메시지에서 읽는 쪽이 작업한다면, 무엇이 금지됐는지만 적힌 메시지는 절반만 일한 셈이다.

돌아보면 새로운 도구를 많이 만든 것은 아니었다. Figma 컴포넌트를 실제 코드와 연결했다. 사람들이 합의한 규칙을 문서로 적고 컴포넌트 경계로 나눴다. 그중 표현 가능한 규칙은 타입과 lint로 옮겼다. 스타일 문자열을 작성할 수 있는 위치를 줄였고, 반복되는 페이지 골격은 Page Frame이 소유하게 했다. 마지막에는 정적 검사와 브라우저 검증이 담당하는 범위를 나눴다.

자연어 문서는 어떤 선택을 해야 하는지 설명했다. 타입과 lint, 테스트는 시스템에 표현한 규칙의 위반을 거부했다. 제품 의도와 새로운 예외는 사람에게 남겼다.

AI가 사용하기 좋은 디자인 시스템은 AI를 위한 설명서가 많은 시스템이 아니었다. 표현해 둔 규칙을 어기면 실패하고, 무엇을 어겼는지가 다음 시도에 쓸 수 있는 형태로 돌아오는 시스템이었다.