Browser Design QC Skill
Figma 스펙과 브라우저 CSS 실측값을 자동 비교해 색상·타이포그래피·치수·간격·문구 5가지 결함을 검출하는 Claude Code 디자인 QC 스킬을 만들었다.
1. 구현 배경
AI 덕분에 UI 구현은 빨라졌지만 검수 과정의 비효율은 여전하다. 스타일 코드 몇 줄 바뀐 PR이라도 실제 화면이 어떻게 달라졌는지는 결국 직접 눈으로 확인해야만 알 수 있기 때문이다.
수동 검수 과정에서 발생하는 휴먼 에러도 문제다. 색상이 미묘하게 어긋나거나, 폰트 굵기가 달라지거나, 간격이 몇 픽셀 벌어지는 결함은 육안으로 걸러내기 어렵다. 실제로 팀에서 처리한 디자인 QC 티켓 100개를 전수 분석한 결과, 다음과 같이 명확한 패턴이 반복되고 있었다.
- 검색 결과 화면 내 병원 카드 padding 불일치
- 채팅창 내 날짜 표기 박스 border-radius 불일치
- CheckBox Spacing 간격 불일치
분석 결과 결함의 유형은 색상, 타이포그래피, 치수, 간격, 문구 등 5가지 카테고리로 수렴했다. 인프라의 환경 변화나 렌더링 방식에 구애받지 않고, Figma 스펙 수치와 브라우저가 실제로 렌더링한 CSS 실측값을 직접 비교 검증하는 자동화 도구를 구축하기로 결정한 이유다.
2. 도구 선택: agent-browser vs Playwright MCP
브라우저 자동화 도구로는 Playwright MCP가 익숙한 선택지다. 그럼에도 Vercel Labs가 만든 Rust 기반 CLI 도구인 agent-browser를 채택했다. AI 에이전트가 브라우저를 제어하는 아키텍처 관점에서 근본적인 효율성 차이가 존재하기 때문이다.
- Playwright MCP: 에이전트가 외부 MCP 서버에 명령을 위임하고, JSON-RPC 통신을 거쳐 브라우저를 제어하는 구조다. 단일 턴으로 끝나지 않고 파일 저장 등의 추가 스텝이 필요하다.
- agent-browser: 외부 서버 없이 AI가 터미널에서 Bash 명령어 하나로 Chrome DevTools Protocol(CDP)을 직접 조종하는 구조다.
| 항목 | agent-browser (✓ 채택) | Playwright MCP |
|---|---|---|
| 기반 기술 | Rust 네이티브 바이너리 (CDP 직접 제어) | Node.js + Playwright (JSON-RPC 통신) |
| 의존성 | Bash 접근만 필요 (MCP 클라이언트 불필요) | MCP 클라이언트 및 서버 설정 필수 |
| 요소 참조 | 접근성 트리 출력 시 요소별 고유 ref ID(@e1, @e2) 부여 | 별도 ref 시스템 없음 (CSS 셀렉터 의존) |
| JS eval | eval "..."로 런타임 Web API 즉시 호출 | 파라미터 전달 방식으로 표현 제약 있음 |
| 스크린샷 | base64 문자열 직접 반환 (즉시 처리 가능) | 파일 저장 후 경로 전달 (추가 디스크 I/O 발생) |
agent-browser를 선택함으로써 얻는 이점은 명확하다. Bash 명령어 하나로 실행되므로 eval 결과나 스크린샷 데이터가 AI와의 동일한 컨텍스트(Turn) 안에서 즉시 반환된다.
특히 snapshot 명령 시 부여되는 고유 ref ID(@e1 등) 덕분에, UI 변경으로 CSS 셀렉터가 달라져도 에이전트가 오작동 없이 안정적으로 요소를 추적하고 조작할 수 있다. 외부 네트워크 모킹이 필수적인 환경이 아니라면, 개발 환경에서 가볍게 실측값을 뽑아내기에 최적의 선택지다.
3. CSS 측정 방식 비교
디자인 검증 자동화를 구현하기 위해 총 세 가지 접근법을 비교 검토했다.
| 방식 | 감지 원리 | 장점 | 단점 및 채택 여부 |
|---|---|---|---|
| A. 스크린샷 픽셀 diff | 렌더된 이미지를 이전 버전과 픽셀 단위 비교 | 시각적 변화 전체 감지, 초기 설정 용이 | 탈락: OS/디바이스(DPR) 환경 차이로 오탐 다발, 구체적인 오류 속성 파악 불가 |
| B. Figma Code Connect | Figma 컴포넌트와 정적 코드 컴포넌트 매핑 | 디자인-코드 간 매핑 자동화 | 탈락: 런타임에서 부모 컨테이너 압박으로 변하는 실측 렌더링 값을 잡지 못함 |
| C. Web API 활용 ✓채택 | getComputedStyle + getBoundingClientRect | 환경 무관한 실제 렌더 값 추출, 디버깅 데이터 명확 | 채택: SPEC 수동 등록 공수 필요, 토큰 매핑 예외 처리 필요 |
이미지 비교 방식(A)은 macOS와 Linux CI 환경의 폰트 렌더링 차이로 인해 수많은 오탐을 발생시킨다. 정적 코드 비교 방식(B)은 w-[132px]로 선언했더라도 부모 flex 속성에 의해 압박받는 런타임 실측치를 계산하지 못한다.
반면 getComputedStyle과 getBoundingClientRect를 조합하는 방식(C)은 브라우저가 스타일 상속과 우선순위를 모두 계산한 뒤 도출한 ‘최종 결과물’을 측정한다. headless Linux CI에서도 동일한 수치(font-size: 14px)를 보장하며, 결함 발생 시 변경된 속성을 명확한 데이터로 제공하므로 채택했다.
4. 실험 설계 및 서브에이전트 구조
디자인 QC 티켓 100개를 분석해 도출한 5대 결함 카테고리를 바탕으로, 각 항목을 브라우저 환경에서 실측할 검증 방법을 정의했다.
| 카테고리 | 대표 증상 | 검증 방법 (Web API) |
|---|---|---|
| 색상 | 텍스트/배경색 CSS 스펙 미준수 | getComputedStyle → color, backgroundColor hex 비교 |
| 타이포그래피 | 폰트 크기 변경, 굵기 스펙 미준수 | fontSize, fontWeight 추출 후 비교 |
| 치수 | 라벨 및 구분선 너비/두께 변경 | getBoundingClientRect() width/height 실측 |
| 간격 | 요소 간 gap 변경, 마진 미준수 | getBoundingClientRect() 두 요소 간 좌표 차이 계산 |
| 문구 | 오타, 띄어쓰기 오류, 문구 변경 | SPEC 텍스트 → DOM 존재 여부 역방향 확인 (MISSING) |
실험 방식
대상 화면은 ‘예약 오픈 설정 모달’이다. 검증은 사이클 단위로 진행했다. 사이클마다 특정 카테고리의 CSS 스펙을 의도적으로 어긋나게 수정한 뒤(예: text-CoolGray-800 → text-blue-600), 에이전트 스킬이 이를 잡아내는지 확인한다. 변화를 놓치면 SKILL.md에 규칙을 보강한 뒤 다음 사이클로 넘어간다. 마지막 6회차 사이클에서는 5대 카테고리의 결함을 동시에 주입해 전체 회귀 테스트(Full Regression)를 수행했다.
서브에이전트 설계
역할 분담과 데이터의 투명성을 확보하기 위해 5개의 Claude Code 서브에이전트가 상태 파일(state.json)을 매개로 컨텍스트를 주고받도록 설계했다. 그리고 Bug Writer와 Inspector를 엄격히 격리했다. Inspector는 어떤 버그가 주입되었는지 모르는 블라인드 상태로 QC를 실행한다. 검증과 개선 주체를 분리함으로써 검출 실패를 축소 보고할 여지를 차단하고 객관적인 검출률 데이터를 확보했다.
- Planner: 5대 결함 카테고리를 도출하고, 사이클마다 테스트할 버그 주입 명세를 결정한다. 종료 후 Inspector의 리포트를 채점한다.
- Bug Writer: Planner의 주문서에 따라 타깃 컴포넌트 코드를 수정해 의도적인 디자인 결함을 심고, 사이클이 끝나면 원복한다.
- Inspector: 현재
SKILL.md기준에 따라agent-browser를 구동해 디자인 QC를 수행하고 HTML 리포트를 작성한다. - Optimizer: Inspector가 직면한 런타임 병목을 분석해 토큰과 실행 시간을 줄이는 최적화 방안을 도출한다.
- Skill Builder: Planner의 채점 결과와 보강 지시문을 바탕으로
SKILL.md내부 검증 규칙을 수정한다.
5. 실험 기록 및 스킬 진화 과정
Cycle 1. 색상(Color) 검증
구분선 배경색, 텍스트 컬러 2종, 헬프텍스트 컬러 등 총 4개의 색상 CSS 불일치를 주입하고 첫 번째 eval을 실행했다. 테스트 결과 3개의 오류는 감지했으나 1개의 오류를 놓쳤다.
실패 원인은 크게 두 가지였다. 먼저 text-CoolGray-600 클래스가 Tailwind config에 정의되지 않아 실제 브라우저 렌더링 시 아무런 색상 변화가 일어나지 않았다. 여기에 더해 JSX 템플릿 리터럴 뒤쪽에 공백(trailing space)이 포함되면서 SPEC 키 매칭마저 실패했다. 스타일도 적용되지 않은 데다 문자열까지 불일치해 발생한 누락이었다.
이 과정에서 등록된 요소만 검증하는 ‘SPEC 화이트리스트 방식’의 사각지대를 확인했다. 등록이 누락된 요소는 결함이 생겨도 잡아낼 수 없다는 맹점이 있었다. 이를 해결하기 위해 결함 상태를 FAIL / UNVERIFIED / PASS로 세분화하여, 검증 대상에서 누락된 요소까지 추적할 수 있는 3단계 분류 체계를 도입했다.
개선점
- 텍스트 앵커 기반 특정: 배경색만으로 요소를 찾으면(
querySelector('[class*="bg-\\[#f5f7f9\\]"]')) 동일한 스타일을 가진 다른 요소가 오탐된다. 반드시 주변의 텍스트 앵커(el.textContent.includes('예약 오픈'))로 대상을 좁힌 뒤 CSS를 추출해야 정확하다. - Figma 색상 매핑 테이블 표준화: 각
eval스크립트마다 하드코딩하던 색상 값을SKILL.md에 단일 매핑 테이블(예:CoolGray-800→#506073)로 정의하여 수동 입력 오차를 제거했다.
Cycle 2. 타이포그래피(Typography) 검증
font-bold, text-base font-bold, text-base 세 종류의 타이포그래피 CSS 불일치를 적용했다. TreeWalker와 getComputedStyle 조합으로 fontSize와 fontWeight를 실측해 3개 오류를 모두 감지했다.
// ✅ 3종 세트 검증 방식
'오픈 주기': { fontSize: '14px', color: '#506073', fontWeight: '400' },
// ❌ color만 등록 시 — fontSize·fontWeight 오류 미검출
// '오픈 주기': { color: '#506073' },
개선점
- 폰트 속성 3종 세트 검증 필수화:
color만 검증하면fontSize와fontWeight오류를 놓친다. 텍스트 요소는 반드시fontSize,color,fontWeight세 속성을 함께 묶어 SPEC에 등록하도록 규칙을 강제했다.
Cycle 3. 치수(Sizing) 검증
w-[132px] → w-[200px], h-[1px] → h-[4px], w-[140px] → w-[90px] 형태로 세 줄의 치수 변형을 주입했다. 결과는 3개 모두 감지했다.
- 실패 원인 분석: 기존 구분선 탐지 조건이
h > 2px이면 검증을 스킵하도록 설계되어 있어, 4px로 커진 구분선이 인식되지 않고 누락될 뻔했다. 탐색 범위를h = 1~10px범위로 확장하여 즉시 감지했다.
개선점
- getBoundingClientRect() 실측: CSS 클래스 값과 실제 렌더 값은 불일치할 수 있다. 코드에
w-[132px]를 적었더라도, 부모 flex 컨테이너가 자식의 width를 수축시키면 실제 너비는 달라진다. 따라서 정적 클래스명이 아닌getBoundingClientRect()로 런타임 수치를 직접 측정해야 한다. - snapshot → eval 대체 (토큰 100배 감소):
agent-browser snapshot은 전체 접근성 트리를 텍스트로 반환하므로 1회 실행 시 약 2~4K 토큰을 소비한다. 요소 좌표 하나를 얻기 위해 전체 트리를 받을 필요가 없으므로, 필요한 요소만eval로 런타임 호출하여 토큰 소비량을 100배 줄였다.
Cycle 4. 간격·정렬(Spacing) 검증
수직 간격(gap-4 → gap-8), 수평 간격(gap-x-2 → gap-x-8), 구분선 마진(my-6 → my-2) 등 3개 결함을 주입했고, 전원 FAIL로 판정했다(3/3).
- 실패 원인 분석: 결함 감지에는 성공했으나, 행 수직 간격의 측정값이
-41px로 왜곡되어 연산되었다.[class*="flex"][class*="items-center"]조건으로 “dialog 내 flex 행 전체”를 탐색하다 보니 모달 헤더 행이 첫 번째 요소로 오탐된 것이 원인이었다.
개선점
- 상위 트리 탐색 체인 구현: 요소를 특정할 때
앵커 텍스트 → closest 행 → parentElement → children순으로 올라가는 구조를 설계하여 탐색 정확도를 높였다. - getBoundingClientRect() 기반 거리 계산:
gap속성은 flex나 grid 컨테이너에만 존재하며, margin이나 padding으로 구현된 간격은normal로 반환된다. 따라서 두 요소 사이의 픽셀 거리는 각 요소의getBoundingClientRect()값을 구한 뒤 오차를 계산해야 한다. 서브픽셀 렌더링 오차를 흡수하기 위해±2px허용 오차를 적용했다. - eval 응답 페이로드 슬리밍 (토큰 70% 감소): 기존
eval스크립트는 SPEC에 등록되지 않은 텍스트 스타일까지actual에 담아 반환했다. 5개 카테고리, 평균 25개 텍스트 노드, 3개 속성이 겹쳐 수백 개의 데이터가 매번 낭비되었다. 이를 불일치가 발생한fails와unverifiedKeys만 반환하도록 변경하여 응답 크기를 약 70% 감소시켰다.
Cycle 5. 문구·오타(Text Content) 검증
‘당일 예약’ → ‘당일예약’, ‘예약 마감 시간’ → ‘예약마감 시간’, ‘전까지만 예약할 수 있습니다.’ → ‘전까지만 예약하실 수 있습니다.’ 등 3개의 문구 결함을 주입하여 모두 감지했다.
- 실패 원인 분석: 기존
eval방식은 DOM 텍스트를 키(Key)로 사용해 SPEC과 스타일을 비교했다. 문구 자체가 바뀌면 키가 달라지므로 SPEC 매칭이 불가능해지고, 오타가 아닌unverified로 분류되는 한계가 있었다.
개선점
- SPEC_TEXTS MISSING 패턴 도입: DOM을 기준으로 매칭하는 대신, SPEC에 명시된 올바른 텍스트 리스트가 DOM 내에 존재하는지 확인하는 역방향 체크 로직을 추가했다. 이를 통해 오타와 문구 변경을 정확히 가려냈다.
const SPEC_TEXTS = ['당일 예약', '예약 마감 시간', '전까지만 예약할 수 있습니다.'];
const textFails = {};
for (const expected of SPEC_TEXTS) {
if (!domTexts.has(expected)) {
textFails[expected] = 'MISSING: text not found in DOM';
}
}
- 모달 상태 유지 및 eval만 재실행: 코드를 수정하고 재검증할 때마다 페이지 전체를 navigate하고 모달을 다시 여는 과정에서 병목이 있었다. HMR이 적용 중이거나 코드 변경 없는 검증이라면 navigate를 생략하고
eval명령어만 재실행하도록 유도하여 회당 약 3초를 절약했다.
Cycle 6. Full Regression
앞선 5대 카테고리에서 2개씩 추출한 총 10개의 CSS 스펙 미준수를 동시에 적용하고 단일 eval 스크립트로 회귀 테스트를 돌렸다. 결과는 10/10 전원 검출이다. Cycle 1에서 3/4 검출에 그쳤던 스킬이 5번의 단계적 보강을 거치며 복합 결함을 공백 없이 잡아내는 상태로 진화했음을 확인했다.
최적화 결과 분석 요약
| 검증 단계 | 최적화 전 | 최적화 후 | 절약 지표 |
|---|---|---|---|
| open + 모달 오픈 | ~5초 | ~5초 (첫 실행) / 0초 (재실행) | 재실행 시 인터랙션 비용 제로 |
| agent-browser snapshot | ~3초 + 3K 토큰 | ~0.5초 + 30 토큰 | 실행 속도 6배 단축, 토큰 100배 감소 |
| eval 실행 및 데이터 반환 | ~1초 + 5K 토큰 | ~1초 + 1.5K 토큰 | 페이로드 압축으로 토큰 70% 감소 |
| 전체 프로세스 합계 | ~11초 | ~7초 | 런타임 타임아웃 약 35% 감소 |
6. 에이전트 및 스킬 구조 개선 회고
에이전트 설계 개선
실험을 마치고 에이전트 구조를 다시 돌아보니 잘 작동한 부분과 아쉬운 점이 명확히 보였다.
- 에이전트 역할 격리:
Bug Writer와Inspector를 철저히 분리했다. Inspector가 버그 주입 내역을 모르는 블라인드 상태로 QC를 진행해 인지 편향을 차단했고, 덕분에 ‘3/4 검출’이라는 객관적인 데이터를 얻을 수 있었다.Inspector와Skill Builder역시 분리하여 검증 실패가 축소 보고될 동기를 차단했다. - 선순환 루프 형성:
Skill Builder가 업데이트한 최신SKILL.md를 다음 사이클의Inspector가 매번 새로 학습하도록 설계했다. 사이클이 반복될수록 스킬이 고도화되며 검출률이 상승하는 선순환이 완성됐다. - state.json 기반 정보 격리:
Bug Writer → Inspector채널에서는 블라인드 테스트 무결성을 위해 버그 명세를 제외했다. 반면Inspector → Optimizer채널에는 실제 작성한 eval 코드를 완전 공유하여 구체적인 스크립트 병목(중복 호출 등)을 진단할 수 있도록 채널을 이원화했다.
- Optimizer 배치 주기: Optimizer가 마지막 Full Regression 직전에만 호출되어, 도출된 최적화 방안을 매 사이클에 점진적으로 반영하지 못했다. 다음 설계에는 매 사이클 종료 시마다 Optimizer가 자동 실행되는 루프가 필요하다.
- Planner 역할 중첩: Planner가 기획과 채점을 동시에 수행하여 주관이 개입될 여지가 있었다. 채점 기준 수식을 사이클 시작 전에 고정해 두는 엄밀함이 보완되어야 한다.
스킬 구조 개선
실험 종료 후 Anthropic의 skill-creator 가이드를 기준으로 스킬 구조를 리팩토링했다.
- SKILL.md 다이어트 (594줄 → 187줄):
SKILL.md가 트리거될 때마다 발생하는 토큰 낭비를 막기 위해, 결정론적 코드와 상세 원칙을 외부로 이관하고 워크플로 골격과 포인터만 남겼다. - 결정론적 코드의 스크립트화: 인라인에 섞여 있던 코드를
qc-eval.js,crop-coords.js,crop-modal.py,cleanup-reports.sh등scripts/디렉토리로 격리했다. 이를 통해 스킬 구조를 건드리지 않고 로직만 업데이트할 수 있는 구조를 확립했다. - 상세 원칙의 레퍼런스화: 앵커 텍스트 특정법, 허용 오차 표, Figma 매칭 원칙 등 인지적 판단 기준들을
references/하위의measurement.md,report-build.md등 4개 문서로 분리해 필요할 때만 참조하도록 개선했다. - 에셋 참조의 명시적 선언: 에이전트가 탐색 범위 밖의 에셋을 인지하지 못하는 한계를 해결하기 위해, 루트에 방치되어 있던 리포트 HTML 템플릿의 경로를 스킬 내부에 명시적으로 선언했다.
- description 트리거 조건 구체화 (WHAT + WHEN): 수행 작업(WHAT)만 적혀 있어 발생하던 언더트리거를 막기 위해, “디자인 QC / Figma 대조 / browser-agent-qc 언급 시 발동”과 같이 구체적인 실행 시점(WHEN)을 추가 기술했다.
7. 실제 QC 실행 결과
browser-design-qc 스킬로 실제 화면을 검증했다. 테스트 브랜치에 결함을 의도적으로 주입하고, 스킬이 이를 검출하는지 확인했다. 대상은 예약 오픈 설정 모달의 매일·매주·매월 3개 모드로 가정했다.
검출 결함 내역
| 모드 | 카테고리 | 속성 | Figma 기준 | 실측값 |
|---|---|---|---|---|
| 공통 | 색상 | 뱃지 background | #dbeafe | #dcfce7 |
| 공통 | 색상 | 뱃지 color | #1e40af | #166534 |
| 공통 | border-radius | 카드 border-radius | 8px | 0px |
| 공통 | 색상 | 구분선 배경색 | CoolGray-200 | red-300 |
| 매일 | 타이포 | ”열려요.” fontSize | 14px | 16px |
| 매일 | 타이포 | ”열려요.” fontWeight | 400 | 700 |
| 매일 | 치수 | 입력 필드 width | 110px | 60px |
| 매주 | 간격 | 요소 간 flex gap | 8px | 32px |
| 매주 | 문구·오타 | ”열려요.” 텍스트 | 열려요. | 열여요. (오타) |
| 매월 | 문구·오타 | ”열려요.” 텍스트 | 열려요. | 열립니다. |
| 매월 | 간격 | flex-col gap-y | 4px | 24px |
8. 다음 목표
AI 도입으로 코드 생산성이 급증하는 상황에서, 검증 단계의 수동 의존성을 제거하는 것은 전체 프로세스의 병목을 해결하기 위한 필수 과제다. 본 작업에서는 브라우저 실측값을 기반으로 디자인 스펙을 검증하는 도구를 구축하여 이 검수 병목을 해결할 수 있는 기반을 마련했다.
다음 고도화 목표는 시각적 실측을 넘어 기능적 명세를 자동으로 추적하는 기획 QC(browser-spec-qc) 파이프라인의 구축이다. 이번에 확보한 agent-browser 인프라를 그대로 활용하되, 버튼 클릭에 따른 모달 개폐 상태, 토스트 팝업 감지, API 응답 status 매칭 등 비즈니스 로직의 인터랙션 흐름까지 자동 검증하는 단계로 영역을 확장할 계획이다.