
AI를 이용한 디자인 리뷰는 확률적이므로 결정론적으로 검출하는 메커니즘을 시도해 보았다
요약
AI를 활용한 디자인 리뷰의 확률적 한계를 극복하기 위해 결정론적 검증 레이어를 추가하는 방법을 제안합니다. 이미지 수치나 구조적 유사도 등 명확한 기준은 결정론적 방식으로 처리하여 AI 리뷰의 정확도와 신뢰성을 높이는 하네스 엔지니어링 접근법을 다룹니다.
핵심 포인트
- AI 리뷰의 확률적 특성으로 인한 검출 누락 문제 지적
- 결정론적 레이어와 확률적 레이어의 분리 필요성
- 수치 기반 검사는 결정론적, UX/의도 판단은 확률적 레이어로 구성
- 하네스 엔지니어링을 통한 AI 에이전트의 신뢰성 확보
최근 AI를 사용하여 프론트엔드(Frontend)를 개발할 기회가 늘어났습니다. 이제 코드 생성은 모두 AI에게 맡길 수 있는 장면이 늘어난 한편, 프론트엔드 구현의 경우 아무래도 사람의 눈으로 보고 디자인대로 구현되었는지 확인할 필요가 있어, 아직 인간이 개입해야 하는 장면이 많이 남아 있습니다.
실제로 디자인 이미지를 AI에게 전달하여 구현을 의뢰한 후, 화면을 눈으로 보고 "여백이 다르다", "글자가 조금 크다", "카드의 높이가 맞지 않는다" 등등, 세세한 수정을 요청하는 일이 자주 있습니다.
또한 AI를 이용한 디자인 작성에서는 Figma MCP를 사용하거나, Storybook에서 컴포넌트(Component)의 스크린샷을 촬영하여 그것을 AI에게 리뷰하게 하는 프랙티스(Practice)도 최근 유행하고 있다고 생각합니다. 하지만 AI에 의한 이미지 리뷰는 확률적(Probabilistic)이기 때문에, 같은 이미지를 전달해도 항상 같은 지적을 한다고 보장할 수 없습니다. 예를 들어 사전에 체크리스트를 작성하더라도, 컨텍스트(Context)가 너무 방대해져서 잊어버리거나 하는 등의 이유로 AI가 실수로 놓치는 일이 발생할 수 있다고 생각합니다.
이 기사에서는 디자인 이미지와 Storybook의 스크린샷을 결정론적(Deterministic)으로 비교하고, 그 결과를 hooks로부터 AI에게 반환함으로써 디자인 리뷰에 결정론적인 레이어(Layer)를 추가하는 방법을 시도해 보았기에 이를 소개합니다.
AI 에이전트의 하네스(Harness)를 생각할 때는 확률적인 판단과 결정론적인 판정을 나누어 생각할 필요가 있습니다. (자세한 내용은 참고 기사 참조)
예를 들어, AI에 대한 지시나 AI에 의한 리뷰를 확률적인 레이어로, 정규 표현식이나 파일의 존재 확인 등 매번 같은 결과를 반환하는 검사를 결정론적인 레이어로 정리하고 있습니다.
예를 들어 다음과 같이 정리할 수 있습니다.
| 레이어 | 예 |
|---|---|
| 결정론적 | ESLint, TypeScript, Prettier, Biome, 단위 테스트 |
| 확률적 | AI에 의한 확인 전반, LLM에 의한 코드 리뷰, AI에 의한 명명 제안, AI에 의한 리팩터링 제안 등 |
결정론적인 확인은 확인할 수 있는 범위가 좁지만, 대신 놓치는 것이 없고 반드시 결과가 출력되며 그 결과가 변하지 않는다는 특징이 있습니다. 반면 AI에 의한 확률적인 확인은 모호한 것에 대해서도 확인을 시킬 수 있지만, 모델의 성능이나 컨텍스트에 따라 결과가 달라질 수 있다는 특징이 있습니다. 하네스 엔지니어링(Harness Engineering)에서는 결정론적으로 확인할 수 있는 부분을 제외하고 남은 부분을 AI에 의한 확률적인 확인을 수행하며, AI의 생성 결과 확인과 AI로는 판단할 수 없는 것을 인간이 수행하는 구조가 하네스 엔지니어링에서는 흔하다고 생각합니다.
프론트엔드 디자인 관련 하네스 중 제가 알고 있는 것은 디자인 가이드라인 작성이나 Playwright MCP 등의 프롬프트(Prompt)로 제어하는 확률적인 방법이 주를 이루지만, 결정론적인 방법으로 잘 작동할지 의문이 들어 이번 기사를 작성하게 되었습니다.
실제로 생각나는 프론트엔드 디자인 확인을 확률적·결정론적으로 적용하면 다음과 같이 나눌 수 있다고 생각합니다.
| 레이어 | 잘하는 것 | 예 |
|---|---|---|
| 확률적 | 문맥이나 의도를 고려한 판단 | 디자인의 인상, UX, 정보의 우선순위 리뷰, 디자인 가이드라인 준수 여부 |
| 결정론적 | 수치나 조건으로 판정할 수 있는 검사 | 이미지 크기, 변경 픽셀률, 이미지 구조의 유사도, 디자인 토큰(Design Token) 사용 여부 검사 |
확률적인 관점에서는 "이 화면은 사용하기 편한가", "브랜드의 인상에 맞는가"와 같은 UX와 관련된 질문은 AI도 어느 정도 할 수 있겠지만, 세세한 점에 관해서는 인간과 마찬가지로 놓치는 경우가 있을 것이라 생각합니다.
반면 "기준 이미지와 크기가 같은가", "차분이 허용 범위 내인가", "구현에서 규정된 디자인 토큰을 사용하고 있는가" 등은 AI에게 리뷰를 맡기는 것보다 프로그램으로 검사하는 것이 더 확실하고 빠릅니다.
이번에 시도한 흐름은 다음과 같습니다.
- AI에게 페이지 전체의 디자인 이미지를 전달한다
- AI가 페이지를 컴포넌트로 분할한다
- 분할된 컴포넌트별로 기준이 되는 이미지를 트리밍(Trimming)하여 준비한다
- AI가 컴포넌트를 구현한다
- 컴포넌트의 Storybook story를 작성한다
- Storybook을 표시하고 스크린샷을 촬영한다
- 기준 이미지와 스크린샷을 비교한다
- 기준을 만족하지 않는 경우, 비교 리포트를 바탕으로 AI가 수정한다
- 합격할 때까지 촬영·비교·수정을 반복한다
- 각 컴포넌트 완료 후, 페이지 전체를 조립한다
페이지를 처음부터 한꺼번에 구현하는 것이 아니라, 컴포넌트 단위로 피드백 루프 (Feedback Loop)를 돌리는 것은 Figma MCP에서도 권장되는 베스트 프랙티스 (Best Practice) 중 하나입니다. 작은 컴포넌트부터 차례대로 작업하는 것이 필요한 컨텍스트 (Context) 양도 적고 정밀도를 높이기에 유리합니다.
이번 합격/불합격 판정에서는 다음 조건들을 사용했습니다.
- 이미지 크기가 일치할 것
- 변경 픽셀률 (Changed Pixel Rate)이 10% 이하일 것
- SSIM이 0.90 이상일 것
비교할 두 이미지는 동일한 크기로 촬영합니다. 크기가 다른 이미지를 비교할 때 리사이즈 (Resize)하면 레이아웃 오류를 숨겨버릴 가능성이 있기 때문입니다.
따라서 기준 이미지의 치수에 맞춰 Storybook의 뷰포트 (Viewport)를 설정하고, 촬영 시점에 너비와 높이를 일치시켰습니다. 크기가 다를 경우에는 리사이즈하지 않고 실패로 처리합니다.
변경 픽셀률은 기준 이미지와 비교 대상 이미지의 동일한 좌표에 있는 픽셀을 비교하여, "차이가 있다"라고 판정된 픽셀이 이미지 전체에서 차지하는 비율을 나타내는 값입니다. 값이 0%라면 변경된 픽셀이 없다는 뜻이며, 값이 클수록 넓은 범위에서 차분이 발생하고 있음을 의미합니다.
단순히 RGB 값이 1이라도 다르면 차분으로 간주할 경우, 안티앨리어싱 (Anti-aliasing)이나 이미지 저장 시 발생하는 미세한 차이까지 잡아내게 됩니다. 그래서 이번 구현에서는 두 이미지를 그레이스케일 (Grayscale)로 변환한 뒤, 각 좌표의 밝기 차이가 8을 초과하는 픽셀을 "변경 있음"으로 판정했습니다. 변경 픽셀률은 다음과 같은 방식으로 구합니다.
변경 픽셀률 = 변경 있음으로 판정된 픽셀 수 / 전체 픽셀 수
이번에는 이 값이 10%를 초과할 경우 실패로 처리했습니다.
변경 픽셀률의 장점은 차분이 화면 내에 어느 정도 퍼져 있는지를 직관적으로 파악할 수 있다는 점입니다. 예를 들어 배경색이 다르면 넓은 범위가 차분이 되고, 요소가 몇 픽셀 어긋나면 그 윤곽선을 따라 차분이 나타납니다. 차분 이미지나 히트맵 (Heatmap)과 조합하면 어느 위치가 판정에 영향을 주었는지도 확인할 수 있습니다.
반면, 이 값만으로는 "무엇이 다른지"나 "그 차이가 중요한지"까지는 판단할 수 없습니다. 작은 아이콘이 완전히 사라지더라도 화면 전체에서 차지하는 비율은 작기 때문입니다. 또한 요소가 약간 이동한 경우에는 이동 전과 이동 후가 모두 차분으로 계산됩니다. 따라서 어디까지나 동일한 촬영 조건에서 차분의 면적을 측정하는 지표로 사용합니다.
SSIM은 "Structural Similarity Index Measure"의 약자로, 두 이미지의 구조적 유사도를 평가하는 지표입니다. 단순한 오차량뿐만 아니라 인간이 이미지에서 구조를 파악하는 특성을 고려하여, 주로 다음 세 가지 요소로 이미지를 비교합니다.
- 휘도 (Luminance): 이미지의 평균적인 밝기가 유사한가
- 대비 (Contrast): 명암의 폭이 유사한가
- 구조 (Structure): 주변 픽셀 간의 변화나 패턴이 유사한가
이미지 전체를 하나의 값으로만 비교하는 것이 아니라, 국소적인 영역별로 이를 계산하여 집계합니다. 이번에 이용한 구현에서는 동일한 이미지에 가까울수록 값이 1에 가까워집니다. 따라서 변경 픽셀률은 낮을수록 유사하고, SSIM은 높을수록 유사하다는 역방향의 지표가 됩니다.
픽셀 차분이 "동일한 좌표의 색상이 얼마나 다른가"를 본다면, SSIM은 "명암이나 윤곽 같은 구조가 얼마나 유지되고 있는가"를 봅니다. 예를 들어 이미지 전체에 미세한 밝기 차이가 있는 경우, 변경 픽셀률은 높아지더라도 구조 자체는 유사하기 때문에 SSIM은 비교적 높게 나올 수 있습니다.
이번에는 SSIM이 0.90 미만인 경우를 실패로 처리했습니다. 다만 SSIM이 높다고 해서 반드시 디자인대로 구현되었다고 단정할 수는 없습니다. 단색 배경이 넓게 펼쳐진 화면에서는 일부 중요한 요소가 다르더라도 전체 점수에 미치는 영향이 작을 수 있기 때문입니다. 또한 몇 픽셀 정도의 위치 어긋남에도 영향을 받습니다. 그래서 변경 픽셀률과 조합하여 합격 여부를 판정하고 있습니다.
ORB는 "Oriented FAST and Rotated BRIEF"의 약자로, 이미지에서 코너나 무늬의 변화가 큰 특징점 (Feature Point)을 검출하고, 각각의 특징을 짧은 데이터로 표현하는 기법입니다. 두 이미지에서 얻은 특징점을 대조하면 "기준 이미지의 이 특징이 비교 대상 이미지의 어디에 대응하는가"를 조사할 수 있습니다.
픽셀 차분이나 SSIM이 동일한 좌표를 전제로 하는 반면, 특징점 매칭 (Feature Matching)은 이미지 내의 대응 관계를 조사할 수 있습니다. 따라서 요소의 위치가 움직였거나 이미지 전체에 변형이 있는 경우, 유사한 특징이 남아 있는지 확인할 수 있는 보조 정보가 됩니다.
이번 리포트에서는 대응하는 특징점의 수와, 그중에서 동일한 변환 관계에 있다고 판단된 점의 수를 출력합니다. 참고용으로 다음과 같은 이미지가 출력됩니다.
ORB는 본래 객체 탐지(Object Detection)나 이미지 정렬(Image Alignment) 등에 사용되는 특징량(Feature)인 것으로 보입니다. 여백이나 배경색의 차이를 직접 평가하는 것이 아니며, 문자나 반복되는 패턴에서는 잘못된 대응이 발생할 수도 있습니다. UI가 디자인대로인지 단독으로 판정하기에는 적합하지 않으므로, 이번에는 AI나 사람이 큰 위치 어긋남을 조사하기 위한 보충 정보로 활용했습니다.
Butteraugli는 두 이미지의 "인간의 눈에 어느 정도로 다르게 보이는가"를 추정하기 위한 지표입니다. 공식 리포지토리(Repository)에서는 특히 간신히 지각할 수 있는 정도의 차이를 평가하는 것을 목적으로 하는 도구라고 설명되어 있습니다.
실행하면 이미지 전체의 지각 차이를 나타내는 점수(Score)와 차이가 눈에 띄는 위치를 나타내는 히트맵(Heatmap)을 얻을 수 있습니다. Butteraugli의 점수는 낮을수록 지각상의 차이가 작다는 의미로 해석됩니다.
단, Butteraugli는 주로 JPEG와 같은 비가역 압축(Lossy Compression)에 의한 미세한 화질 차이를 평가하는 용도를 상정하고 있으며, 큰 레이아웃 변경에 대한 성능은 보장되지 않습니다. 이번과 같은 UI 비교에 그대로 적용할 수 있는 절대적인 합격 기준도 없기 때문에 합격/불합격 판정에는 사용하지 않았습니다. 히트맵을 AI에 전달하여 사람이 눈으로 차이가 눈에 띌 법한 장소를 찾기 위한 보충 정보로 사용하고 있습니다.
이미지 비교 지표에는 각각 잘 잡아내는 차이와 잘 잡아내지 못하는 차이가 있습니다. 하나의 점수만으로 "디자인대로"라고 판단하는 것이 아니라, 목적이 다른 결과들을 조합합니다.
이번 역할 분담은 다음과 같습니다.
| 지표 | 보고 있는 것 | 수치 읽는 법 | 이번 용도 |
|---|---|---|---|
| 이미지 크기 | 캔버스의 너비와 높이 | 일치 또는 불일치 | 합격/불합격 판정 |
| ... | 1에 가까울수록 유사함 | 합격/불합격 판정 | |
| ORB | 모서리나 패턴 등의 특징점과 그 대응 관계 | 대응점의 수나 위치를 조사 | 특징점 대응을 확인하는 보충 정보 |
| Butteraugli | 인간이 지각할 것으로 추정되는 이미지의 차이 | 낮을수록 차이가 작음 | 지각상의 차이를 확인하는 보충 정보 |
합격 여부는 이미지 크기, 변경 픽셀률, SSIM의 3가지 조건을 모두 만족하는지에 따라 결정합니다. ORB와 Butteraugli는 합격 여부에 영향을 주지 않고, 실패 시 원인을 조사하여 AI에게 추가 정보를 제공하기 위해 이용합니다.
또한 임계값(Threshold) 설정도 디자인된 웹사이트에 따라 달라질 것이라고 생각합니다. 예를 들어, 디자인이 풍부하고 화려한 웹사이트에서는 임계값을 낮게 설정하지 않으면 애초에 PASS하기 어려운 케이스도 있을 수 있습니다. 반대로 CRUD 관리 화면처럼 정형화된 것이라면 임계값을 높게 설정하여 엄격하게 관리하는 것도 쉬울 것 같다는 인상을 받았습니다.
이번 검증에서는 디자인 이미지를 react-app/docs/design에, React 구현체와 Storybook에서 촬영한 이미지를 react-app/src/features에 배치했습니다. 예시 구조는 다음과 같습니다.
react-app/
├── docs/
│ └── design/ # 디자인된 컴포넌트의 스크린샷
...
react-app/docs/design에는 Figma 등에서 가져온 비교 대상 원본 이미지를 둡니다. 페이지 전체 이미지와 AI가 분할한 컴포넌트 단위의 이미지를 나누어 관리하고 있습니다.
docs/design/features/plan/PlanPage.png
docs/design/features/plan/components/HeroSection.png
PlanPage.png는 페이지 전체의 디자인이며, components/HeroSection.png는 HeroSection 부분만 잘라낸 디자인입니다. 이 디렉토리의 PNG 파일들이 이미지 비교에서의 레퍼런스(Reference)가 됩니다.
react-app/src/features에는 기능 단위로 React 컴포넌트, Storybook story, 테스트를 배치합니다. 스크린샷 촬영 스크립트는 story에 대응하는 컴포넌트와 동일한 디렉토리에 PNG를 저장합니다.
src/features/plan/HeroSection.tsx
src/features/plan/HeroSection.stories.tsx
src/features/plan/HeroSection.test.tsx
...
여기에 있는 HeroSection.png
는 디자인 이미지가 아니라, Playwright로 Storybook의 Default story를 촬영한 결과입니다. 이미지 비교 시의 candidate (후보)가 됩니다.
디자인 이미지와 촬영 이미지는 디렉터리 계층이 아닌 파일 이름으로 매칭합니다.
docs/design/features/plan/components/HeroSection.png
↓ 동일한 파일 이름
src/features/plan/HeroSection.png
이 방식을 사용하면 디자인 이미지를 컴포넌트용 디렉터리에 정리해 둔 상태로, 촬영 이미지를 구현 파일 옆에 둘 수 있습니다. 반면, src
이하에 동일한 이름의 PNG가 여러 개 있으면 대응 대상을 일의적으로 결정할 수 없습니다. 그 경우에는 AMBIGUOUS로 처리하여 실패하도록 구성했습니다. 또한 디자인 이미지에 대응하는 촬영 이미지가 없는 경우에는 SKIPPED가 됩니다.
이미지 차분 (diff)을 안정화하려면 매번 동일한 조건으로 스크린샷을 찍어야 합니다. 표시 조건이 바뀌면 구현을 변경하지 않았더라도 차분이 발생하기 때문입니다.
이번 스크립트에서는 주로 다음 조건들을 고정했습니다.
- viewport (뷰포트)
- device scale factor (기기 스케일 팩터)
- color scheme (컬러 스킴)
- reduced motion (동작 줄이기)
- animation (애니메이션)
- caret (커서)
- 폰트 로딩 완료
Playwright로 각 story를 열고, Default story의 스크린샷을 저장합니다. 기준 이미지와 동일한 이름의 컴포넌트를 매칭함으로써 이후의 일괄 비교를 자동화했습니다.
스크린샷 촬영은 다음 명령어로 실행할 수 있습니다.
npm --prefix react-app run storybook:screenshots
package.json에서는 이 명령어를 통해 capture-storybook-screenshots.mjs를 Chromium 지정으로 실행하고 있습니다.
"scripts": {
"storybook": "storybook dev -p 6006",
...
실제로 스크린샷을 촬영하는 react-app/scripts/capture-storybook-screenshots.mjs에서 비교와 관련된 주요 부분만 발췌하겠습니다. Storybook의 기동 처리나 에러 처리는 생략합니다.
const designImages = readdirSync(designDir, { recursive: true })
.filter((path) => extname(path).toLowerCase() === ".png")
.map((path) => resolve(designDir, path))
...
처리 흐름은 다음과 같습니다.
http://127.0.0.1:6006/index.json에 접속하여 Storybook이 기동되었는지 확인한다.- 기동되지 않았다면
npm run storybook으로 Storybook을 기동한다. - Storybook의 index에서 export 이름이
Default인 story만 추출한다. - 컴포넌트 이름과 동일한 이름의 디자인 PNG를 찾는다.
- 디자인 PNG가 있으면 해당 이미지 크기를 viewport로 설정한다.
- story를 iframe으로 열고, 컴포넌트와 폰트의 표시 완료를 기다린다.
- animation과 caret을 무효화하고 스크린샷을 저장한다.
기준 이미지를 찾을 수 없는 story는 #storybook-root의 표시 영역을 촬영합니다. 기준 이미지가 있는 story는 viewport 전체를 기준 이미지와 동일한 치수로 촬영하기 때문에, 비교 시 리사이즈는 수행하지 않습니다.
이미지 비교는 bash 파일로 실행해 보았습니다.
scripts/image-diff/compare_storybook.sh
실제 스크립트는 길기 때문에 핵심 부분만 올립니다.
DESIGN_DIR="${ROOT_DIR}/react-app/docs/design"
STORYBOOK_DIR="${ROOT_DIR}/react-app/src"
OUT_DIR="${ROOT_DIR}/.out/frontend/image-diff"
...
REFERENCE="$1"
CANDIDATE="$2"
shift 2
...
compare.sh
는 이미지 비교 알고리즘이 아니라, 의존 라이브러리를 준비하여 compare_ui_images.py를 실행하는 래퍼(wrapper)입니다. exec를 사용함으로써 Python의 종료 코드(exit code)를 호출자에게 그대로 반환합니다.
def pixel_diff(reference, candidate, threshold):
diff = cv2.absdiff(reference, candidate)
gray_diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
...
이 스크립트는 docs/design 이하에 있는 모든 PNG를 순차적으로 처리합니다. src 이하에서 동일한 이름의 PNG를 찾아, 1건만 발견된 경우에 compare.sh로 전달합니다. 후보가 0건이면 SKIPPED, 2건 이상이면 AMBIGUOUS입니다.
compare.sh에서 실행된 compare_ui_images.py가 Pixel Diff, SSIM, ORB를 계산하며, 임계값(threshold)을 만족하지 않으면 종료 코드 1을 반환합니다. 그 후, compare_storybook.sh가 Butteraugli 점수와 히트맵(heatmap)을 리포트에 추가합니다. 모든 이미지를 처리한 후, 비교 대상이 0건이거나 실패가 1건 이상 있으면 스크립트 전체도 종료 코드 1이 됩니다.
비교 결과는 Markdown 요약과 컴포넌트별 상세 리포트로 출력됩니다.
.out/frontend/image-diff/
├── summary.md
└── pairs/
...
AI에는 합격/불합격뿐만 아니라 변경 픽셀률, SSIM, 차분 이미지, 히트맵도 전달합니다. 이를 통해 "외관을 확인하고 적당히 고친다"가 아니라, "어디에 어느 정도의 차이가 남아있는가"를 기반으로 수정할 수 있습니다. 리포트는 다음과 같은 형식으로 출력됩니다.
# UI Image Diff Report
- Reference: `react-app/docs/design/features/management/components/ProductSalesRanking.png`
- Candidate: `react-app/src/features/management/ProductSalesRanking.png`
...
이미지 비교 스크립트는 기준을 충족하지 않는 이미지가 하나라도 있으면 종료 코드 1을 반환합니다.
이번에는 Lefthook의 pre-push에 Storybook 촬영과 이미지 비교를 통합했습니다.
pre-push:
jobs:
- name: frontend image diff
...
이를 통해 AI가 자신의 구현을 "완성했다"고 판단하더라도, 이미지 비교가 불합격이면 push할 수 없습니다. AI는 리포트를 읽고 구현을 수정한 뒤, 다시 한번 검사를 실행해야 합니다.
"디자인에 맞추세요"라는 지시는 확률적입니다. 반면, "변경 픽셀률 10% 이하 및 SSIM 0.90 이상이 아니면 종료 코드 1을 반환한다"라는 조건은 AI의 주의력이나 컨텍스트(context) 상태에 좌우되지 않습니다.
이 검사가 이번에 추가한 결정론적(deterministic) 레이어입니다.
이번에는 Figma Make를 사용하여 적당한 체중 관리 앱의 메뉴 페이지를 대상으로 했습니다.
페이지 전체를 컴포넌트로 분할하고, 각각에 디자인 이미지와 Storybook story를 준비했습니다. 또한 비교를 위해 이번에 준비한 결정론적 확인을 사용하는 경우와 사용하지 않는 경우로 동작 확인을 해보았습니다.
AI는 codex에서 GPT-5.6-solo로 실행했습니다.
언뜻 보기에는 거의 동일해 보이지만, 겹쳐보면 다음과 같습니다.

전체적으로 여백이나 글자 크기가 조금씩 어긋나 있습니다. 그렇기는 하지만 전체적인 UI에 관해서는 거의 일치하므로, 이 정도의 디자인이라면 문제없다는 결과입니다.
다음으로 결정론적 확인을 사용했을 때의 결과입니다. 이것은 이번에 설정한 임계값을 PASS한 것입니다.
앞서와 마찬가지로 전체적인 UI로서는 문제없이 구현되어 있습니다. 이 차분은 다음과 같습니다.
앞서와 달리 글자 크기나 전체적인 여백이 거의 일치하게 되었습니다. 실제로 실행 중인 로그를 보면 CSS를 px 단위로 미세 조정하는 과정을 반복하고 있어, AI가 px 조정 장인이 되어 있었습니다.
합격 조건이 명확해졌다
기계적으로 실패를 표현할 수 있기 때문에, AI가 결정한 임계값 (Threshold)을 넘을 때까지 계속해서 수정을 진행해 줍니다. 다만, 실제로 구현 가능한 영역인지 구분하지 않으면 할 수 없는 일을 끊임없이 반복하게 되므로, 그 부분에 대한 판단은 필요하다고 생각했습니다. -
여백 조정이 완전 자동
기본적으로 여백 조정이나 글자 크기는 AI가 Python 코드를 작성하여 알아서 수정해 주었습니다. 간단하지만 직접 하면 번거로운 작업들을 어느 정도까지 대신 해 주기 때문에 매우 유용했습니다. -
컴포넌트가 많아도 놓치지 않음
이번에는 push할 때 hook을 통해 강제적으로 검사하기 때문에, AI에게 확인을 요청하는 방식이라면 놓칠 수도 있지만 그런 일이 없습니다. 또한 비주얼 리그레션 (Visual Regression)을 위한 자산을 축적해 나갈 수 있다는 점이 장점이라고 생각했습니다.
폰트나 에셋에 따른 렌더링 차이
렌더링하는 브라우저에 대상 폰트가 설치되어 있지 않거나, 애초에 폰트나 이미지를 준비해 두지 않은 경우 대응하기 어려운 경우가 많았습니다. 따라서 명시적으로 필요한 폰트나 에셋을 준비해 둘 필요가 있었습니다. -
임계값을 너무 엄격하게 설정했을 경우의 과도한 실패
당연한 이야기지만, 임계값을 엄격하게 설정하면 PASS할 때까지 시간이 걸립니다. 그만큼 토큰 (Token)도 소비하기 때문에 적절한 임계값 설정이 어렵습니다. -
임계값의 수치가 컴포넌트에 의존함
예를 들어 페이지와 같은 큰 컴포넌트에서 특정 글자 위치가 10px 어긋나는 것과, 버튼과 같은 작은 컴포넌트에서 10px 어긋나는 것을 상대적으로 비교하면, 페이지 컴포넌트 쪽이 영향이 적고 버튼 컴포넌트 쪽이 영향이 더 큽니다. -
애니메이션은 어려움
스크린샷 기반이기 때문에 애니메이션 확인은 어렵습니다. 애니메이션 확인 자체는 사람의 눈이나 AI를 통한 확인이 될 것이라고 생각합니다. -
디자인 토큰이 있다면 충분한 경우
기본적으로 여백이나 글자 크기는 디자인 토큰 (Design Token) 등으로 규정되는 경우가 많다고 생각합니다. 이번에 개선된 여백이나 폰트 크기는 디자인 토큰만 통일되어 있다면 통일할 수 있으므로, 그것만으로도 문제가 없을 가능성도 있다고 봅니다. (이번에는 검증 단계였기에 그 정도까지 준비하지는 않았습니다.)
사람 또는 AI에 의한 디자인 리뷰가 완료되고 브라우저에 표시된 결과가 승인된 이후에는 docs/design의 역할은 Figma 등의 디자인 스크린샷을 실제 구현된 스크린샷으로 교체함으로써 비주얼 리그레션에 사용할 수 있게 하는 것입니다. 이때 임계값 판정 기준을 더욱 높일 수 있으므로, 비주얼 리그레션 테스트와 마찬가지로 더욱 엄격한 픽셀 단위의 비교로 이행할 수 있습니다.
즉, 이미지 검사를 다음의 2단계로 나눕니다.
| 단계 | 비교 대상 | 목적 | 판정 방식 |
|---|---|---|---|
| 디자인 도달 판정 | 디자인 이미지와 최초 구현 | AI의 구현을 디자인에 가깝게 만듦 | 환경 차이를 허용한 변경률과 SSIM |
| 리그레션 테스트 | 승인된 브라우저 이미지와 변경 후 구현 | 의도하지 않은 외관 변화를 방지 | 더욱 엄격한 픽셀 단위의 비교 |
디자인에 도달하는 것과 그 이후의 외관 유지에서는 동일한 이미지 비교라 하더라도 목적이 다릅니다. 이 두 가지를 분리함으로써, 최초 구현 시에는 현실적인 허용 범위를 두면서도 승인 후에는 엄격하게 변경 사항을 감시할 수 있습니다.
이번에는 디자인 수정을 결정론적 (Deterministic)으로 시도해 보았습니다. 기본적으로 수행한 것은 기존의 비주얼 리그레션 기법을 로컬에서 실행할 수 있도록 옮긴 것뿐이지만, 결정론적으로 디자인을 평가하여 AI가 루프를 돌리도록 만들었습니다.
검증 중에는 굳이 없어도 되지 않을까 생각하면서도, 자동화하여 결정론적으로 판정할 수 있고 딱히 악화되는 부분도 없으므로 리소스가 충분하다면 도입해도 좋겠다는 감상입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기