본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 01. 10:56

Claude Design이 만든 HTML이 요구사항을 충족하는지 스스로 검증하게 하기

요약

Claude Design으로 생성된 HTML을 단순 목업이 아닌 구현의 단일 진실 공급원(SSOT)으로 활용하는 워크플로우를 제안합니다. 디자인과 구현 사이의 괴리(design drift)를 방지하기 위해 HTML이 스스로 요구사항 충족 여부를 검증하도록 설계하는 방법을 다룹니다.

핵심 포인트

  • Claude Design 결과물을 구현의 SSOT로 취급하여 Git으로 관리
  • 디자인과 실제 구현 간의 괴리(design drift) 방지 전략
  • HTML 사양을 코드 생성의 입력 계약으로 활용
  • Playwright 등을 활용한 시각적 검증 메커니즘 구축

서론

프리랜서 Android 개발자인 とだやま.R입니다. 평소에는 모바일 앱을 개발하고 있지만, 최근에는 Claude Design으로 화면 디자인 사양을 만드는 기회가 늘었습니다. 프롬프트나 스크린샷을 제공하면 HTML/CSS/JS로 동작하는 디자인을 생성해 줍니다.

하지만 출력된 HTML을 그대로 구현의 본보기로 삼으려 하면 곤란한 상황이 발생하곤 했습니다. 겉보기에는 깔끔해 보여도 "iPhone과 iPad, Light 모드와 Dark 모드 등 모든 상태를 요구사항에 맞게 제대로 만들었는가"에 대한 보장이 어디에도 없기 때문입니다. 손으로 직접 구현 코드로 옮기는 순간부터 디자인과 구현은 조용히 어긋나기 시작합니다.

이 글에서 다룰 내용은 제가 시도하고 있는 다음과 같은 방식입니다. 생성된 HTML을 "나중에 참조할 목업(Mock)"이 아니라, 구현의 유일한 정답인 SSOT (Single Source of Truth, 단일 진실 공급원)로서 Git으로 관리합니다. 그 위에서 HTML 스스로가 "요구사항을 충족하는가"를 실행 시점에 계산하게 하고, "PASS 문구를 붙여넣어 속이는 행위"까지 결과물 자체에 봉인하도록 합니다. 이를 통해 디자인과 구현의 괴리(design drift)를 막습니다.

주제는 제가 운영하고 있는 가상의 의약품 레퍼런스 앱 디자인 사양 리포지토리입니다.

AI가 만든 디자인은 "그럴싸할" 뿐, 구현과 금방 괴리된다

Claude Design은 프론트엔드만을 만드는 도구이며, 출력물은 표준적인 HTML/CSS/JS입니다 (Anthropic Labs의 발표). 그렇기에 나온 파일들을 그대로 Git에 둘 수 있습니다. 문제는 내용의 정확성입니다.

제가 다루고 있는 검색 화면의 사양은 화면 하나당 74개의 상태를 가집니다. 세로 모드 iPhone에 9가지 상태가 있고, 여기에 iPad 등의 폭(width) 클래스, 그리고 Light·Dark 모드를 조합해 나가면 사람이 육안으로 "전부 OK"라고 단언할 수 있는 양이 아닙니다. Claude Design은 이 74가지 상태를 한 번에 그려주지만, 그 하나하나가 요구사항대로인지는 별개의 문제입니다.

그리고 까다로운 점은, 이 어긋남을 아무도 알아차릴 수 없다는 것입니다. 디자인을 업데이트했는데 구현에 반영하는 것을 잊어버리거나, 반대로 구현을 리팩터링(Refactoring)하는 과정에서 외관이 변해버리기도 합니다. 두 경우 모두 옆에 나란히 놓고 비교하는 메커니즘이 없다면 조용히 진행됩니다. 이것이 이른바 design drift이며, 생성 AI에게 디자인을 맡기면 "요구사항을 충족하는 척(그럴싸한 외관만 갖춤)"이 섞이기 쉽기 때문에 더욱 빈번하게 발생합니다.

생성된 HTML을 목업이 아닌 구현의 입력 계약으로 취급하기

생성된 HTML을 "나중에 훑어볼 참조 이미지"라고 생각하는 한, 괴리는 멈추지 않습니다. 그렇게 하는 대신, 다음 구현이 따라야 할 입력 계약, 즉 구현의 SSOT로서 취급합니다. 제 리포지토리의 방침 파일(AGENTS.md)에는 다음과 같이 적혀 있습니다.

Treat implementation-backed HTML specs as code-generation inputs, not presentation mockups. A wrong spec can cause the next Codex implementation to build the wrong app.

HTML 사양이 틀리면 그것을 입력으로 사용하는 다음 구현 전체가 틀리게 된다는 뜻입니다. 따라서 사양을 "구현의 SSOT"로 Git에서 관리하고, 그 렌더링 결과를 기준 이미지로 삼아 변화를 검출할 수 있는 상태로 만들어 둡니다. README에도 "HTML 사양을 SSOT로 관리"한다고 명시되어 있습니다.

전체 흐름은 다음과 같습니다. Claude Design이 HTML 사양을 생성하면, 이를 Codex로 구현용에 맞게 조정 및 감사(Audit)하고, pages.json에 검증 대상으로 등록합니다. 그 후 Playwright와 pixelmatch를 사용하여 렌더링 결과를 이미지로 만들어 이전 결과와 비교함으로써 의도하지 않은 외관 변화를 검출합니다. 이것이 이른바 VRT (Visual Regression Test, 시각적 회귀 테스트)입니다. 추출한 기준 이미지를 Git이 추적하게 하고, Husky의 Git hook을 통해 commit / push 할 때마다 실행합니다.

HTML 스스로가 "요구사항을 충족하는지" 계산하게 하기

SSOT로 배치한 HTML에 대해, 요구사항을 충족하는지를 HTML 스스로가 검증하게 합니다.

검색 화면에는 Flutter 버전과 iOS 버전이 있습니다. Flutter 버전은 모든 행을 자동으로 계산하는 단계까지 구현되어 있고, iOS 버전은 거기에 위조 탐지 기능을 추가한 것입니다. 먼저 Flutter 버전부터 살펴보겠습니다.

구체적으로는, 생성된 HTML 내부에 검증 리포트가 임베디드된 JavaScript (JavaScript) 형태로 포함되어 있어, 페이지를 여는 순간 실행됩니다. 작동 방식은 다음과 같습니다. 렌더링된 후의 실제 DOM을 querySelectorAll로 수집하고, getBoundingClientRect()로 위치와 크기를 실측합니다. 그 후, 요구사항을 충족하는지 여부를 한 줄씩 PASS / FAIL로 계산합니다.

손으로 "여기는 OK"라고 적는 것이 아니라, 열 때마다 다시 측정하는 방식입니다. 코드 주석에도 모든 행이 실제 DOM, 정규 표현식 (Regular Expression), 집계로부터 계산되며, 임시방편으로 PASS / FAIL을 결정하는 행은 없다고 명시되어 있습니다.

아래는 해당 검증 리포트를 실제로 열었을 때의 모습입니다. Flutter 버전의 검색 화면입니다. 상단의 녹색 배너가 "ALL PASS 전 28행 PASS. Frame count = 74."라고 표시되어 있으며, 그 아래로 ID · Method · Result · Evidence 표가 이어집니다.

Flutter検索画面スペックの検証レポート(ALL PASS)

Method 열의 C는 Computed, 즉 렌더링 후의 DOM을 실측한 행입니다. S는 Static으로, HTML 소스 문자열 자체에 대한 정규 표현식 확인(예를 들어 "iOS 고유의 단어가 섞여 있지 않은지"를 outerHTML에 대해 확인)입니다. 74개의 상태를 모두 실제 렌더링한 후, 그 실측 결과에 따라 녹색 배너가 나타납니다.

"PASS"라고 적으면 속을 수 있는 허점을 결과물 스스로가 막게 하기

하지만 검증 기능을 삽입하면 곧바로 까다로운 의문이 생깁니다. ">PASS<라고 손으로 적어두면 녹색으로 표시되지 않을까?" 하는 점입니다. 이 허점을 막을 수 있느냐가 검증 리포트를 신뢰할 수 있는지의 갈림길이며, 이 부분이 이 메커니즘에서 가장 흥미로운 지점이었습니다.

그래서 iOS 버전의 사양에는 검증 위조를 탐지하는 두 가지 감사(Audit) 행이 포함되어 있습니다.

첫 번째는 VR-AUDIT-A입니다. 검증 JavaScript는 표에 행을 삽입하기 전에, 표의 내용(tbody)이 비어 있었는지 여부를 기록해 둡니다. 누군가 PASS 행을 HTML에 직접 써넣으면, 페이지를 열었을 때 tbody가 비어 있지 않게 되어 __INITIAL_TBODY_HTML === ""가 거짓(False)이 되어 FAIL 처리됩니다. 코드 주석에서 말하는 "이것이 실제로 하드코딩된 <tr>` 위조를 방지한다"는 부분입니다.

두 번째는 VR-AUDIT-B입니다. 페이지의 소스 HTML을 통째로 가져옵니다. 거기서 <script> 블록만 제거한 나머지 부분에 >PASS<, >FAIL<, >ALL PASS<와 같은 판정 문자가 나타나는지 스캔합니다.

var __auditScan = (__SOURCE_HTML || "").replace(
/<script\b[\s\S]*?<\/script>/gi,
"",
...

판정 문자가 소스에 단 하나라도 직접 작성되어 있다면 FAIL이 됩니다. 배너와 표는 이 검증 JavaScript가 집계 결과로부터 동적으로 작성하기 때문에, 사람이 판정 문자를 붙여넣을 여지가 없습니다.

이 검사기는 자기 자신을 함정에 빠뜨리지 않도록 설계되었습니다. 어떤 아이콘을 검사할 때 소스에서 "tune"이라는 단어를 찾는데, 검사 코드 자체가 소스에 "tune"을 남기면 그 자체로 오탐(False Positive)이 되어 버립니다. 그래서 String.fromCharCode(116, 117, 110, 101)를 사용하여 실행 시점에 조합함으로써, 소스에 "tune"이라는 문자열을 남기지 않도록 했습니다.

그리고 마지막으로 집약 관문이 있어, 모든 행과 감사 행이 PASS일 때만 녹색 배너가 나타납니다. 아래는 iOS 버전 검증 리포트 하단부로, VR-AUDIT-A · VR-AUDIT-B부터 마지막 집약 행("전 71행 PASS")까지의 영역입니다.

iOS版レポートのanti-forgery行(VR-AUDIT-A/B〜VR-AGG)

다만, 제약 사항이 있습니다. VR-AUDIT-B는 스캔하기 전에 <script>

를 제거하기 때문에, 봉쇄할 수 있는 것은 "정적인 결과물(HTML의 본문 부분)에 판정을 붙여서 합격을 위장하는 것"뿐입니다. 검증 로직의 JavaScript 자체를 수정하여 항상 true를 반환하도록 만드는 공격은 이와는 별개의 문제이며, 각 검증 행의 설계가 올바른가에 대한 문제입니다. 따라서 여기서 말할 수 있는 것은 "모든 위장을 봉쇄했다"가 아니라, "정적인 결과물에 판정을 붙이는, 가장 흔히 발생하는 위장을 봉쇄했다"입니다.

그럼에도 불구하고 이것은 효과가 있습니다. 생성형 AI (Generative AI)에게 화면을 다시 만들게 했을 때, AI는 초록색 배너를 하드코딩하여 "완료했습니다"라고 말할 수 없습니다. 초록색으로 만들기 위해서는 실제로 요구사항을 충족할 수밖에 없습니다. 검증이 단순한 "증명"에서 "요구사항 충족의 강제"로 바뀌는 이유는 바로 이 위장 봉쇄가 있기 때문입니다.

생각하는 방식 자체는 새롭지 않습니다. 대상이 요구사항을 충족하는지를 대상 스스로가 가진 테스트로 확인한다는 발상은, 자기 테스트를 하는 코드(Self-testing code)나 TDD (Test-Driven Development)에서 익숙한 개념입니다. 특히 TDD에서 구현 전에 테스트 실패(red)를 확인하는 것은, 테스트가 내용 없이 통과하고만 있는 상태를 피하기 위해서입니다. 이번 위장 탐지 역시, 초록색 배너가 실체를 동반하지 않은 채 나타나지 않았는지를 확인한다는 점에서 발상이 유사하다고 생각합니다.

그것을 AI가 생성한 HTML 결과물 그 자체에 도입한 형태입니다. 일본어로 검색한 범위 내에서는 AI가 생성한 디자인 사양에 이러한 종류의 자기 감사(Self-audit)를 갖추게 한다는 이야기는 찾을 수 없었습니다.

위장의 탐지는 Flutter 버전과 iOS 버전에서 다르게 취급됩니다. 이 위장 탐지(VR-AUDIT-A/B)가 포함되어 있는 것은 iOS 버전의 사양이며, 앞서 본 Flutter 버전의 스크린샷에는 감사 행이 없습니다.

자기 감사를 pre-commit 파이프라인에 연결하기

"페이지를 열면 알 수 있다" 정도라면, 열지 않으면 알 수 없다는 약점이 남습니다. 그래서 이 자기 감사가 commit 할 때마다 자동으로 수행되도록 합니다. pre-commit hook은 3줄입니다.

node scripts/lint-hook.mjs --staged || exit 1
node scripts/visual-review-hook.mjs --staged || exit 1
node scripts/vrt-hook.mjs --staged

Playwright가 이 페이지를 열어 촬영하면, 검증 JavaScript는 페이지를 여는 순간 실행되므로 초록색 "ALL PASS" 배너와 함께 스크린샷에 찍힙니다. 따라서 VRT (Visual Regression Test)의 기준 이미지에는 "ALL PASS였다는 증적"이 각인됩니다. 만약 요구사항이 무너져 검증이 FAIL로 바뀌면, 배너가 빨간색으로 그려지고 픽셀 차분(Pixel difference)이 이를 포착합니다.

한편, 브라우저를 실행하지 않는 정적 감사도 있습니다. lint-hook 내에서 실행되는 html-audit은 HTML을 정적으로 파싱하여 구조나 data-*의 계약 값을 고정합니다. 이는 JavaScript를 실행하지 않으므로 검증 계산 결과 자체는 읽을 수 없습니다. 대신 "검증 리포트(#sec-verify)가 삭제되지 않았는지", "선언된 상태 수가 수정되지 않았는지"를 정적으로 잠급니다. 실제로 audit 규칙에는 Inline verification report must remain present.라는 필수 조건이 포함되어 있습니다.

성질에 따라 사용할 수 있는 검증 계층이 달라지기 때문에, 하나의 계층만으로는 전체를 확인할 수 없습니다. 정리하자면, 성질이 다른 계층들이 서로의 사각지대를 메워주고 있습니다.

계층무엇을 검증하는가언제 실행되는가
① HTML 내의 자기 감사 (검증 행 + 위장 탐지)실제 DOM을 실측하여 요구사항을 계산하고, 판정 붙이기를 탐지함브라우저에서 페이지를 여는 순간
...pre-commit / pre-push
④ 모든 상태의 visual review모든 상태의 crop을 생성하여 육안으로 확인함pre-commit

①의 검증 결과는 브라우저에서 JavaScript를 실행해야만 얻을 수 있습니다. 반면 정적인 ③은 JavaScript를 실행할 수 없습니다. 그래서 ③이 "①의 자기 감사기가 삭제되지 않았음"을 정적으로 보증하고, ①이 "요구사항을 충족하는가"를 런타임에 계산합니다. 그리고 ②가 "외관상의 증적"을 픽셀로 고정합니다. 이렇게 각자의 약점을 다른 계층이 보완합니다.

왜 CI가 아니라 pre-commit인가, 그리고 VRT가 증명할 수 없는 것

VRT를 실행하는 사람들은 대개 CI(지속적 통합)에 배치한다고 생각합니다. 제가 pre-commit(커밋 전 단계)으로 옮긴 이유는 staged(스테이징)된 파일과 변경된 프로젝트로 범위를 좁혀 속도를 높이고 싶기 때문입니다. 그렇게 하면 커밋하기 전에 로컬에서 문제를 발견할 수 있습니다. CI 단계까지 진행된 후 빨간불이 들어오는 것보다, 로컬에서 멈추는 것이 재작업(rework)을 줄이는 길입니다.

VRT는 렌더링 결과의 픽셀을 비교하는 방식이기에, 그대로 두면 불안정(flaky)해지기 쉽습니다. 이를 해결하기 위해 몇 가지 조치를 취했습니다. 폰트를 직접 보유하여 네트워크 대기 시간에 따른 변동을 제거하고, 페이지 로딩이 안정될(networkidle) 때까지 기다린 후 스크린샷을 찍습니다. 애니메이션을 멈추고, 페이지마다 차이(diff) 임계값을 다르게 설정합니다. 그리고 촬영된 이미지의 크기를 통일한 뒤에 비교합니다.

이 메커니즘에는 공수가 많이 듭니다. 페이지마다 감사(audit) 규칙을 작성하고, 임계값을 조정하며, 폰트를 직접 준비해야 합니다.

그리고 당연하게도 VRT는 만능이 아닙니다. 방침 파일(policy file)에도 다음과 같이 명시되어 있습니다.

"VRT와 visual manifests는 HTML 아티팩트(artifact)에 대한 증거일 뿐이며, 그 자체로 아티팩트가 구현(implementation)과 일치한다는 증거는 아닙니다."

VRT가 초록색이라는 것은 "HTML이 이전과 달라지지 않았다"는 증거이지, "HTML이 구현과 일치한다"는 증거가 아닙니다. 따라서 VRT를 통과했다고 해서 구현까지 올바르다고 말할 수는 없습니다.

픽셀 차분 판정은 AI에게 맡긴다

픽셀 차분은 "1px이라도 다르면 빨간색"이 되기 쉬우며, 이것이 의미 있는 변화인지 오차인지까지는 판단해주지 않습니다. 그래서 VRT가 차분을 찾아냈을 때만 Expected(기대값), Diff(차이), Actual(실제값)을 나란히 배치한 한 장의 이미지를 만듭니다. 이를 이미지를 직접 읽을 수 있는 AI (Claude나 Codex)에게 전달하여, "사양을 얼마나 충족하는지"를 fulfillment_percent로 JSON 형식으로 반환하게 합니다.

VRT 3ペイン比較(Expected|Diff|Actual)

비용이 저렴한 결정론적 관문(픽셀 차분)을 트리거로 삼아, 비용이 높은 AI의 판단은 차분이 발생했을 때만 호출합니다. fulfillment_percent가 80 이상이고 변경 사항이 사양대로라면 기준 이미지(baseline image) 업데이트를 권장하는 2단계 구조로 설계했습니다.

마치며

지금까지의 메커니즘을 일반화하면 다음과 같습니다. AI가 만든 결과물은 단순히 훑어보는 목업(mock)이 아니라 구현을 위한 입력 계약(input contract)으로 취급합니다. 그 결과물 자체에 검증을 내장하여, 열 때마다 스스로를 증명하게 합니다. 그리고 판정을 단순히 붙여넣는 식의 쉬운 위조를 차단함으로써, 검증을 "강제"로 바꿉니다. 나머지는 이를 커밋할 때마다 자동 실행하여 조용한 괴리(divergence)를 막는 것입니다.

이는 Claude Design에만 국한된 이야기가 아닙니다. v0나 Figma Make에서도 출력이 표준 HTML 형태로 전달된다면 동일한 방식이 유효할 것입니다. AI에게 무언가를 만들게 하는 상황이 늘어날수록, "그럴싸하게 만들어진 척"을 어떻게 간파하느냐가 중요해집니다. 결과물 스스로 증명하게 한다는 발상은 디자인뿐만 아니라 다른 분야에도 응용 가능하다고 생각합니다.

저 자신도 처음에는 "생성된 HTML을 어떻게 믿을 것인가"라는 소극적인 질문에서 시작했습니다. 그것이 "결과물 스스로 증명하게 하면 된다"라는 적극적인 설계로 바뀌었을 때, 갑자기 다루기 쉬워졌다는 느낌을 받았습니다. 이와 마찬가지로 생성형 AI 출력의 확실성 때문에 고민하고 계신 분들에게 실마리가 된다면 기쁘겠습니다. 더 좋은 방법을 알고 계신 분이 있다면 댓글로 알려주시면 감사하겠습니다.

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0