에이전트에게 UI 구현을 맡기기 전에 정해야 할 디자인 값

X Facebook

Written by

in

화면 설계 문서를 넘기자 구현을 맡은 에이전트가 카드 사이 여백과 빈 목록에 표시할 내용을 물었다. 버튼을 눌렀을 때의 색도 정해져 있지 않았다. 문서에는 “여백은 적당히”, “로딩 처리”라고만 적혀 있었다.

구현은 에이전트에게 맡겼다. 하나한테 다 시키면 오래 걸려서 화면을 나눠 여러 개를 띄우는데, 담당들은 서로 대화하지 못한다. 다음에 부르면 앞에서 뭘 했는지 모르는 새 에이전트가 오니 넘겨주는 문서에 따라 결과물이 달라진다.

문장을 읽는 데는 문제가 없었지만, 구현에 필요한 값은 빠져 있었다. 같은 문서로 서로 다른 화면을 만들 수 있었다. 에이전트가 추측하거나 되묻지 않도록 디자인 값을 어디까지 정해야 하는지 살펴봤다.

지금 이 사이트의 스펙에 그 검사를 그대로 돌려 봤다. 화면 스펙 다섯 편에 state:empty·state:loading·state:error·state:default와 focus-order가 다 있는지, 컴포넌트 스펙 세 편에 접근성 다섯 키가 다 있는지를 grep으로 셌다.

검사 대상 항목 결과
화면 스펙 5편 상태 4종 + focus-order 빠진 것 0건
컴포넌트 스펙 3편 role·aria·focus·keyboard·contrast 3편 모두 keyboard 없음
스펙 전체 적당히·알아서·필요하면 본문 0건

표 1. 이 사이트의 화면·컴포넌트 스펙에 검사를 돌린 결과

화면 쪽은 다 찼는데 컴포넌트 쪽에서 같은 항목이 셋 다 빠졌다. 한 편만 빠졌으면 실수지만 셋이 같은 자리에서 빠지면 그 항목을 쓰는 습관이 없는 것이다. 값으로 적어 두라고 정해 놓고도 이렇게 된다.

토큰 쪽은 packages/scopulus-ui/design/tokens.md 한 파일이다. 478줄에 표가 162줄이고, 맨 뒤 커버리지 표에 16개 항목이 각각 몇 건 쓰였는지까지 적혀 있다. 상태 disabled는 0건이라 “미사용. 조합만 고정”이라고 적혀 있고, 고도도 0건이라 “쓰지 않는다”와 그 이유가 적혀 있다.

curvez에서 이 질문을 맡는 건 wireframe-spec 스킬이다. 화면 어디에 무엇이 놓이는지만 정한 밑그림이 와이어프레임이다. 버튼이나 카드 한 개가 어떻게 생기고 어떤 상태를 갖는지 적은 것이 컴포넌트 스펙이다. 이 스킬이 둘을 함께 맡는다.

1. “적당히”가 한 군데 남으면 무슨 일이 나나

스킬 문서의 첫 문장이 목표를 이렇게 적어 둔다.

디자인 스펙의 목표는 그림이 아니라 판정 가능한 값이다. 구현 에이전트가 이 문서만 읽고 같은 화면을 만들면 성공이고, “적당히” 가 한 군데라도 남으면 같은 스펙에서 매번 다른 화면이 나온다.

그래도 되물어 주면 다행이다. 그냥 값을 만들어 넣으면 결정이 코드에만 남고 다음 화면을 맡은 담당은 그 코드를 읽지 않는다. 이쪽 목록은 여백 12px로 가고 저쪽 목록은 16으로 간다. 둘 다 화면은 잘 나오니 값이 어긋났다는 사실을 아무도 모른다.

그래서 이 스킬은 스펙만 쓰고 코드는 쓰지 않는다. 실제 .tsx·.css를 쓰는 것은 nextjs-implementation의 몫이다. 여기서는 멈춰 있는 상태의 값만 확정한다. 움직일 때의 값, 그러니까 속도와 가감속은 motion-standards로 넘긴다.

2. 값을 무슨 근거로 고르는가

시안이 다 있으면 쉽다. 이미지는 읽고 공개 링크는 받아 와서 색·간격·타이포를 값으로 뽑는다. 눈대중으로 반올림하지 않는 것이 조건이다. 시안이 일부만 있으면 그 화면에서 토큰을 먼저 전부 확정하고 나머지 화면도 새 값 없이 그 토큰만으로 조립한다.

시안이 하나도 없을 때가 문제다. 여기에는 순서가 정해져 있어서 위에서부터 차례로 적용하다 값이 정해지는 단계에서 멈춘다.

  1. 요구사항의 사용자 흐름 — 수용 기준에서 화면 수, 각 화면의 목표, 필수 입력·출력을 뽑는다. “이 화면에서 사용자가 끝내야 하는 일”이 정해지면 영역 분할과 우선순위가 따라온다
  2. 레포에 이미 있는 값 — 코드 저장소에서 tailwind.config.*, theme.*, tokens.*, *.css의 -- 변수를 찾아 재사용한다
  3. 웹의 관례 — 호버, 포커스 링, 브레이크포인트
  4. 기본 스케일 — 간격 4pt 그리드(4/8/12/16/24/32/48), 타이포 4단계(12/14/16/20/24/32), radius 3단계(4/8/999)

4번까지 내려왔으면 그 선택을 decisions에 남긴다. 무엇을 왜 그렇게 정했는지 적는 자리다. 뒤집을 때 고칠 위치는 reversible_at에 같이 쓴다. 근거를 따져 고른 값과 기본 스케일에서 집어 온 값이 문서에서 똑같이 생겼으면, 무엇부터 뒤집어야 할지 알 수 없다.

시안이 있어도 접근성 기준을 어기면 고쳐야 한다. 확인할 기준은 두 가지다. 글자색과 배경색의 밝기 차이가 4.5:1이상일 것, 눌러야 하는 자리가 24×24 px이상일 것. 앞의 것이 모자라면 저시력 사용자나 밝은 야외에서 글자가 안 읽히고 뒤의 것이 모자라면 누를 때 옆 버튼이 눌린다. 둘 중 하나를 어기면 기준을 지키는 쪽으로 고치되, 원본 값과 수정 값과 이유를 셋 다 decisions에 적는다. 조용히 바꾸면 디자이너가 되돌린다.

3. 토큰은 라이트와 다크를 같은 행에서 정한다

토큰이 왜 필요한지는 디자인 토큰은 왜 한 방향으로만 흘러야 하나 에 적었다. 원시 토큰과 의미 토큰을 왜 나누는지도 거기 있다. 여기서는 그 토큰을 처음에 몇 개, 어떤 이름으로 확정하느냐만 본다.

tokens.md의 표는 한 행에 | 토큰 | 라이트 | 다크 | 용도 |를 모두 채운다. 한 칸도 비우지 않는다. 색은 배경과의 쌍으로만 의미가 있다. 라이트만 정한 토큰은 아직 검증되지 않은 값으로 본다. 다크에서 대비를 맞추려고 텍스트 색을 밝힌다고 하자. 라이트에서 통과했던 조합이 깨진다. 그때는 이미 컴포넌트 코드에 라이트 값이 들어가 있다.

이름은 --<category>-<role>-<variant>로 쓴다. --color-bg-canvas 같은 형태다. 반대로 --color-blue-500처럼 값을 이름에 넣는 것은 금지다. 다크에서 그 토큰이 밝은 색이 되면 이름과 실제 값이 어긋난다. 구현자는 이름을 믿고 잘못 쓰게 된다.

새 값이 필요하다고 매번 토큰을 더 만들지는 않는다. 토큰을 몇 개까지 늘릴지는 의미가 몇 개인가를 보고 정한다.

상황 판단
기존 토큰과 눈으로는 구분되지 않는 차이 (색 명도차 5% 미만, 간격 그리드 1스텝 미만, radius·폰트 2px 미만) 기존 것을 쓴다
같은 새 값이 서로 다른 컴포넌트 3곳 이상에 필요 토큰으로 승격한다
한 곳에서만 쓰인다 토큰을 만들지 않는다. 컴포넌트 스펙에 값을 직접 적고 출처를 남긴다
값은 같은데 의미가 다르다 토큰을 나눈다
값은 다른데 의미가 같다 나누지 않는다. 하나로 통일하고 통일한 이유를 남긴다

표 1. 새 토큰을 만들지 말지 가르는 기준

마지막 두 줄이 토큰을 늘리는 기준을 뒤집는다. 값이 얼마나 다른지만 보고 판단하면 #111827과 #111828이 토큰 두 개가 된다. 본문 글자색과 제목 글자색은 토큰 하나로 합쳐진다.

그리고 tokens.md 끝에 ## 대비 검증 섹션을 둔다. 줄 형식이 고정돼 있다.

- fg=#RRGGBB bg=#RRGGBB mode=light|dark min=4.5

라이트와 다크 각각 최소 3쌍(본문·보조·CTA)을 넣는다. 이 줄을 검증 단계에서 그대로 읽어 대비를 계산한다. 형식이 어긋나면 그 쌍은 검사에서 빠져 버린다.

4. 상태 네 줄은 빈 채로라도 남긴다

화면마다 screens/<screen-id>.md 하나를 쓴다. ## layout → ## states → ## responsive → ## a11y 순으로 채운다. 각각 배치, 화면이 처할 수 있는 상황, 화면 폭에 따른 변화, 접근성이다. 이 중 ## states에는 네 줄이 반드시 있다.

  • state:default
  • state:loading
  • state:empty
  • state:error

논리적으로 불가능한 상태라도 줄을 지우지 않는다. 대신 그 자리에 사유를 적는다.

- state:empty — 단건 조회라 빈 상태가 없다. 없는 주문 ID 는 state:error 로 간다

줄이 없으면 “생각 안 함”과 “필요 없음”이 구분되지 않는다. 검증도 그 차이를 못 본다. 게다가 로딩·빈·에러는 실행 중에 반드시 등장한다. 스펙에 없으면 구현자가 즉흥으로 만들다 보니 화면마다 달라진다. 어떤 화면은 스피너가 돌고 어떤 화면은 흰 화면으로 남거나 에러를 삼킨다.

각 상태에서 무엇까지 확정하고 넘겨야 하는지도 표로 못박아 뒀다.

상태 반드시 정할 것
state:loading 스켈레톤인가 스피너인가 · 어느 영역만 바뀌고 어느 영역이 유지되는가 · 200ms 미만이면 표시할 것인가
state:empty 문구 원문 · 다음 행동(CTA) · “아직 없음”인지 “검색 결과 없음”인지
state:error 사용자가 읽을 문구 원문 · 재시도 수단 · 부분 실패일 때 남는 데이터

표 2. 상태별로 확정해 넘겨야 하는 값

문구 원문까지 스펙에 적는 것은 과해 보이기 쉽다. 하지만 빈 목록에 뭐라고 쓸지 적어 두지 않으면 구현자가 그 자리에서 정해야 한다. 그리고 ## a11y에는 focus-order를 넣는다. 키보드 Tab이 영역을 훑는 차례를 화살표로 이어 확정한다.

5. 접근성을 나중으로 미루면 무엇이 비싸지나

컴포넌트마다 components/<ComponentName>.md 하나를 쓴다. ## props ## states ## a11y ## responsive 네 섹션은 어느 컴포넌트에서도 생략하지 않는다. 그중 ## a11y에는 다섯 키가 전부 등장한다.

키 확정할 값
a11y:label 아이콘 전용일 때 화면 낭독기가 읽을 라벨 원문. 텍스트 라벨이 있으면 중복 지정 금지
a11y:focus 포커스 순서가 트리 순서와 같은가. 로딩 중 포커스 유지 여부
a11y:contrast fg/bg 쌍이 4.5:1이상. disabled는 3:1이상
a11y:target 24×24 px이상 + 인접 요소와 8px 간격
a11y:role 역할 하나. 버튼을 링크로 쓰지 않는다

표 3. 컴포넌트 문서의 접근성 다섯 키

대비와 타깃 크기는 값만 바꾸면 된다. 그런데 포커스 순서와 role은 마크업 구조에 박힌다. 화면 낭독기는 HTML에 적힌 차례와 역할을 그대로 따라간다. 스펙 시점에는 한 줄로 정할 수 있는 일을 구현 뒤로 미루면 컴포넌트 트리를 다시 짜게 된다.

6. 실제로 어떻게 부르고 무엇이 남나

"와이어프레임", "화면 설계해줘", "디자인 토큰 정해줘", "컴포넌트 스펙", "다크모드 색 정해줘" 같은 말이 트리거다. 구현 전에 화면 구조가 확정되지 않은 상태로 들어가려 할 때도 걸린다.

문서는 전부 .curvez/design/ 아래에 남는다. 핸드오프 파일 하나를 빼면 이 경로 밖에는 쓰지 않는다.

경로 담는 것
.curvez/design/index.md 화면 목록 · 컴포넌트 목록 · 커버리지 표 · 미결 질문
.curvez/design/tokens.md 토큰 표(라이트/다크 동시) · 이름 규칙 · ## 대비 검증 블록
.curvez/design/screens/<screen-id>.md layout / states / responsive / a11y
.curvez/design/components/<ComponentName>.md props / states / a11y / responsive

표 4. 디자인 경로에 남는 문서 네 종류

마지막에 검증을 실제로 돌린다. 아래 표의 여섯 가지를 세는데, 문서·상태·접근성 키는 grep으로, 토큰 표의 빈 칸은 awk로 찾고 대비는 ## 대비 검증 줄을 읽어 WCAG의 계산식으로 실제 계산한다.

완료 기준은 전부 숫자다. screen-missing=0, component-missing=0, token-half-defined=0이다. 그리고 light>=3, dark>=3, contrast-fail=0, impl-files=0이다.

이름 세는 것
screen-missing 상태 4종이나 focus-order가 빠진 화면 수
component-missing 접근성 5키나 필수 섹션 4종이 빠진 컴포넌트 수
token-half-defined 라이트와 다크 중 한쪽만 채운 토큰 행 수
light · dark ## 대비 검증에 적힌 색 쌍이 모드별로 몇 개인가
contrast-fail 계산해 보니 요구한 최소 대비에 못 미친 색 쌍 수
impl-files 디자인 경로에 잘못 생긴 .tsx·.ts·.css 수

표 5. 검증 단계에서 세는 여섯 가지

대비가 FAIL이면 값을 고쳐 다시 돌린다. 3회 안에 못 맞추면 실패한 쌍을 원문 그대로 적는다. status: partial로 낮춘다. 통과했다고 쓰지 않는다.

7. 도입 비용과 한계

무엇을 어떤 식으로
구현 전에 서는 시간 화면 하나 만들자고 문서 네 종류를 먼저 쓴다. 화면이 두세 개인 작업에서도 써야 할 문서 수는 줄지 않는다
안 쓰는 줄이 늘어난다 상태 4종 × 화면 수만큼 줄이 생긴다. 불가능한 상태에도 사유 한 줄을 적어야 해서 “해당 없음” 줄이 문서의 절반을 차지하기도 한다
팔레트를 그대로 못 옮긴다 값-이름을 금지하니 blue-500을 그대로 쓸 수 없고 색마다 그 자리의 역할부터 정해야 한다
형식 정본이 다른 파일에 있다 네 문서의 정확한 서식은 plugins/curvez/agents/curvez-designer.md 쪽이 정본이다. 스펙을 쓸 때 두 문서를 오간다. 한쪽에 두 벌을 두면 규칙을 바꿀 때 한쪽만 고쳐진다는 게 이유다
급할 때 지름길이 없다 .curvez/design/ 밖에 쓰지 않는 규칙이다. 값 하나 확인하려고 컴포넌트를 먼저 만들어 보는 방법이 막혀 있다
대비가 진행을 막는다 contrast-fail이 0이 아니면 done이 안 된다. 시안 색이 기준에 못 미치면 색을 바꾸거나 partial로 내려야 한다

표 6. 이 스킬을 쓸 때 감수하는 것

두 번째 줄에서 비용을 제일 자주 치른다. 단건 조회 화면에 빈 상태가 없다고 적는 일은 형식만 채우는 것처럼 보인다.

확인하지 못한 것

  • 화면이 스무 개 넘는 프로젝트에서도 이 형식이 유지되는지. 지금까지 본 것은 화면 몇 개 규모다.
  • 토큰 승격 기준의 “3곳 이상”이 어디서 나온 숫자인지. 문서에 근거가 적혀 있지 않다.
  • 디자이너가 Figma를 계속 고치는 상황에서 tokens.md를 어떻게 다시 맞추는지. 스킬 문서는 최초 확정만 다루고 이후 동기화는 다루지 않는다.

되묻지 않고 구현하려면 구현자가 고를 자리를 남기지 않을 때까지 값을 정해 둬야 했다.

그 뒤로는 스펙을 다 쓰고 나서 “적당히”, “알아서”, “필요하면”이 남아 있는지 한 번 검색한다. 세 낱말은 문장에서는 자연스럽게 읽히지만 구현할 때는 아무것도 정해 주지 않는다.

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다