스킬: Visual Diff
두 URL(기준, 예: 프로덕션 사이트와 현재, 예: 로컬 개발 서버 또는 Storybook 스토리)을 시각적으로 비교하여 픽셀 수준의 차이를 찾고 수정을 안내합니다.
사용 시기
- 기존 페이지나 섹션을 Storybook 또는 Astro 사이트에서 복제할 때
- 컴포넌트의 시각적 정확성을 참조 디자인과 비교하여 확인할 때
- 스타일 변경 후 브레이크포인트별 반응형 충실도를 검사할 때
- CSS/HTML을 반복적으로 수정하여 컴포넌트가 참조와 일치할 때까지 개선할 때
사용하지 말아야 할 경우
- 콘텐츠만 변경된 경우(텍스트 업데이트, 카피 편집)로 레이아웃이 변경되지 않은 경우
- Figma 디자인과 비교하는 경우 - Figma MCP 스킬을 사용하세요.
- 성능 또는 접근성 감사 - Lighthouse나 a11y addon을 사용하세요.
사전 준비 사항
시작하기 전에 다음을 확인하세요:
- Playwright MCP가
.vscode/mcp.json에--caps=vision옵션으로 구성되어 있어야 합니다. - pixelmatch와 pngjs가 설치되어 있어야 합니다.
package.jsondevDependencies를 확인하세요. - 기준 URL에 접근 가능해야 합니다(외부 사이트 또는 실행 중인 서버).
- 현재 URL에 접근 가능해야 합니다(
:4321의 개발 서버,:6006의 Storybook 등). - 워크스페이스 루트에
temp/디렉터리가 존재해야 합니다(.gitignore에 포함됨).
개발 서버 또는 Storybook이 실행 중이지 않은 경우 VS Code 작업을 사용하여 시작하세요:
- 개발 서버: Dev Server 작업
- Storybook: Storybook 작업
워크플로우
1단계: 입력 받기
에이전트는 다음 입력이 필요합니다:
| 입력 | 필수 | 예시 |
|---|---|---|
| 기준 URL | 예 | https://www.bitovi.com/ |
| 현재 URL | 예 | http://localhost:4321/ 또는 http://localhost:6006/?path=/story/card--default |
| CSS 선택자 | 아니오 | #hero-section 또는 .card-grid (요소 범위 비교용) |
| 브레이크포인트 | 아니오 | 기본값: 모바일(375×667), 태블릿(768×1024), 데스크톱(1280×720) |
2단계: 기준 스크린샷 촬영
컴포넌트 범위 비교 (단일 컴포넌트 비교 시 권장)
특정 컴포넌트를 비교할 때 – 특히 Storybook 스토리가 해당 컴포넌트만 렌더링하지만 기준 페이지는 전체 페이지를 포함하는 경우 – 컴포넌트의 경계 상자(bounding box)로 크롭하여 관련 없는 페이지 섹션(네비게이션 바, 고정 오버레이, 위/아래 모듈 등)이 diff 결과를 오염시키지 않도록 합니다.
기준 페이지에서 크롭하는 방법:
- 기준 URL로 이동합니다.
- 뷰포트를
{ width: 1280, height: 720 }으로 설정합니다. - 대상 요소가 보이도록 스크롤합니다:
() => document.querySelector('<selector>').scrollIntoView({ behavior: 'instant', block: 'start' }) - 컴포넌트의 렌더링된 높이를 측정합니다:
() => document.querySelector('<selector>').getBoundingClientRect().height - 뷰포트 높이를 측정된 높이와 정확히 일치하도록 설정합니다(너비는 유지):
mcp_playwright_browser_resize → { width: 1280, height: <측정된 높이> } - 스크린샷을 촬영합니다 – 이제 컴포넌트 영역만 캡처됩니다.
배경 샘(bleed) 경고: 컴포넌트 뒤에 보이는 내용은 Storybook(빈 배경)과 기준 페이지(다른 모듈)에서 다릅니다. 일반적인 경우: sticky/local 네비게이션 바가 상단을 침범하거나, 모달/오버레이 뒤에 다른 페이지 콘텐츠가 보이거나, 부분적으로 투명한 컴포넌트입니다. 이러한 잔여 diff는 컴포넌트 자체 콘텐츠 외부에 클러스터링되며 수정 대상이 아닙니다 – 무시하세요.
Storybook 스토리에서 크롭하는 방법:
Storybook iframe.html은 주변 크롬 없이 컴포넌트만 렌더링합니다 – 크기 조정이 필요 없습니다. 그냥 이동하여 스크린샷을 촬영하세요:
mcp_playwright_browser_navigate → { url: "http://localhost:6006/iframe.html?id=<story-id>&viewMode=story" }
mcp_playwright_browser_resize → { width: 1280, height: <기준 크롭과 같은 높이> }
mcp_playwright_browser_take_screenshot → { filename: "temp/vdiff-current-desktop.png" }
전체 페이지 뷰포트 스크린샷 (전체 페이지를 비교할 때)
1. mcp_playwright_browser_navigate → { url: "<baseline-url>" }
2. mcp_playwright_browser_resize → { width: 1280, height: 720 }
3. mcp_playwright_browser_wait_for → { time: 2 } # 페이지가 완전히 렌더링될 때까지 대기
4. mcp_playwright_browser_take_screenshot → {
type: "png",
filename: "temp/vdiff-baseline-desktop.png"
}
각 브레이크포인트에 대해 반복:
| 브레이크포인트 | 너비 | 높이 | 파일명 접미사 |
|---|---|---|---|
| 모바일 | 375 | 667 | -mobile |
| 태블릿 | 768 | 1024 | -tablet |
| 데스크톱 | 1280 | 720 | -desktop |
3단계: 현재 스크린샷 촬영
현재 URL에 대해 정확히 동일한 과정을 반복하고 temp/vdiff-current-{breakpoint}.png 파일명을 사용합니다.
중요: 양쪽에서 동일한 뷰포트 크기와 동일한 크롭 전략(컴포넌트 범위 또는 전체 페이지)을 사용하여 비교가 동등하도록 합니다.
4단계: diff 스크립트 실행
각 브레이크포인트에 대해 터미널에서 visual-diff 스크립트를 실행합니다:
node scripts/visual-diff.mjs \
--baseline temp/vdiff-baseline-desktop.png \
--current temp/vdiff-current-desktop.png \
--output temp/vdiff-diff-desktop.png
스크립트는 JSON을 stdout으로 출력합니다:
{
"totalPixels": 921600,
"diffPixels": 4521,
"diffPercent": 0.49,
"width": 1280,
"height": 720,
"baselineDimensions": { "width": 1280, "height": 720 },
"currentDimensions": { "width": 1280, "height": 720 },
"dimensionsMismatch": false,
"diffImagePath": "/absolute/path/to/temp/vdiff-diff-desktop.png"
}
dimensionsMismatch가 true인 경우, 이미지 크기가 서로 다릅니다. 스크립트는 비교 전에 작은 이미지에 투명 픽셀을 패딩합니다. 이는 종종 레이아웃 문제를 나타냅니다.
5단계: diff 이미지 보기
임시 HTTP 서버를 시작하여 temp/ 디렉터리를 제공하고, Playwright를 diff 이미지로 이동합니다:
# 서버를 백그라운드에서 시작 (세션당 한 번 실행)
npx -y http-server temp/ -p 8787 --cors -c-1 &
그런 다음 Playwright를 diff 이미지로 이동합니다:
mcp_playwright_browser_navigate → { url: "http://localhost:8787/vdiff-diff-desktop.png" }
mcp_playwright_browser_take_screenshot → {} # vision 트리거 – 에이전트가 diff를 볼 수 있음
Playwright MCP의 --caps=vision 플래그 덕분에 에이전트가 스크린샷 출력에서 이미지를 직접 볼 수 있습니다.
참고: file:// URL은 Playwright MCP에서 차단되므로 HTTP 서버가 필요합니다. 세션 동안 한 번 시작하고 모든 diff 이미지에 재사용하세요.
diff 이미지 읽기:
- 빨간색 픽셀 = 기준과 현재의 차이
- 노란색 픽셀 = 안티앨리어싱 차이(보통 무시 가능)
- 어두운 원본 = 일치하는 영역(컨텍스트를 위해 30% 불투명도로 표시)
- 큰 빨간색 클러스터 = 구조적 문제(누락된 요소, 레이아웃 변경, 잘못된 크기)
- 흩어진 빨간색 점 = 서브픽셀 렌더링, 글꼴 스무딩 또는 안티앨리어싱(보통 허용 가능)
- 가장자리의 빨간색 띠 = 크기 불일치 또는 패딩/마진 차이
6단계: 해석 및 조치
수치적 JSON 데이터와 시각적 diff 검사를 결합합니다:
| diffPercent | 해석 | 조치 |
|---|---|---|
| < 1% | 시각적으로 동일 | 미미한 서브픽셀 차이만 있음. 조치 불필요. |
| 1–5% | 거의 일치 | 간격, 글꼴 두께 또는 테두리 차이에 대해 diff 이미지 검사. 작은 CSS 수정이 필요할 수 있음. |
| 5–15% | 주목할 만한 차이 | 레이아웃 변경, 색상 불일치 또는 누락된 요소. diff에서 빨간색 클러스터를 검토하여 작업이 필요한 섹션 식별. |
| > 15% | 큰 차이 | 주요 구조적 또는 스타일링 차이. 가장 큰 빨간색 영역부터 집중 – 이는 가장 큰 레이아웃 불일치를 나타냄. |
diff 이미지를 해석할 때 다음을 주목하세요:
- 빨간색 클러스터가 페이지의 어디에 있는가? (상단, 중간, 하단, 왼쪽, 오른쪽)
- 특정 요소와 연관되어 있는가? (네비게이션, 히어로, 카드, 푸터)
- 클러스터가 간격 문제, 색상 차이 또는 누락된 콘텐츠를 나타내는가?
7단계: 수정 및 재-diff
- diff 분석에 따라 CSS/HTML 수정
- 3~6단계를 반복(현재 URL만 다시 스크린샷 촬영)
diffPercent가 허용 가능한 수준에 도달할 때까지 계속
일반적인 목표:
- 정확한 복제: < 2%
- 합리적 일치: < 5%
- 구조적 일치(다른 콘텐츠): < 15%
전체 페이지 스크린샷
기본적으로 스크린샷은 보이는 뷰포트만 캡처합니다. 긴 페이지의 경우 fullPage 옵션을 사용하세요:
mcp_playwright_browser_take_screenshot → {
type: "png",
fullPage: true,
filename: "temp/vdiff-baseline-desktop-full.png"
}
경고: 전체 페이지 스크린샷은 큰 이미지를 생성하고 diff 속도가 느려집니다. 전체 페이지 레이아웃을 비교할 때만 사용하세요.
Storybook 특화 팁
Storybook 스토리를 스크린샷할 때:
-
iframe URL을 사용하여 깔끔한 스크린샷 확보(Storybook 크롬 없음):
http://localhost:6006/iframe.html?id=components-card--default&viewMode=story -
렌더링 대기 – Storybook 스토리는 하이드레이션에 시간이 걸릴 수 있음:
mcp_playwright_browser_wait_for → { time: 3 } -
요소 스크린샷은 Storybook 패딩에서 스토리 콘텐츠를 분리하는 데 효과적입니다.
임계값 조정
--threshold 플래그(0~1)는 pixelmatch 감도를 제어합니다:
| 값 | 감도 | 사용 사례 |
|---|---|---|
| 0.05 | 매우 엄격 | 정확한 픽셀 일치, 모든 것을 포착 |
| 0.1 | 기본 | 좋은 균형, 대부분의 안티앨리어싱 무시 |
| 0.2 | 관대 | 시스템 간 글꼴 렌더링 차이를 허용 |
| 0.3 | 매우 관대 | 주요 색상/레이아웃 차이만 포착 |
node scripts/visual-diff.mjs \
--baseline temp/vdiff-baseline-desktop.png \
--current temp/vdiff-current-desktop.png \
--output temp/vdiff-diff-desktop.png \
--threshold 0.2
예시: 히어로 섹션 비교하기
# 1. 기준 스크린샷 (프로덕션 사이트 히어로)
mcp_playwright_browser_navigate → { url: "https://www.bitovi.com/" }
mcp_playwright_browser_resize → { width: 1280, height: 720 }
mcp_playwright_browser_wait_for → { time: 2 }
mcp_playwright_browser_snapshot → {}
# 히어로 섹션의 ref 찾기, 예: ref="hero-[1]"
mcp_playwright_browser_take_screenshot → {
type: "png",
element: "hero section",
ref: "hero-[1]",
filename: "temp/vdiff-baseline-hero-desktop.png"
}
# 2. 현재 스크린샷 (Storybook 스토리)
mcp_playwright_browser_navigate → { url: "http://localhost:6006/iframe.html?id=components-hero--default" }
mcp_playwright_browser_resize → { width: 1280, height: 720 }
mcp_playwright_browser_wait_for → { time: 3 }
mcp_playwright_browser_take_screenshot → {
type: "png",
filename: "temp/vdiff-current-hero-desktop.png"
}
# 3. diff 실행 (터미널에서)
node scripts/visual-diff.mjs \
--baseline temp/vdiff-baseline-hero-desktop.png \
--current temp/vdiff-current-hero-desktop.png \
--output temp/vdiff-diff-hero-desktop.png
# 4. 임시 서버 시작 (세션당 한 번, 백그라운드 터미널에서)
npx -y http-server temp/ -p 8787 --cors -c-1 &
# 5. Playwright를 통해 diff 이미지 보기
mcp_playwright_browser_navigate → { url: "http://localhost:8787/vdiff-diff-hero-desktop.png" }
mcp_playwright_browser_take_screenshot → {} # vision 트리거
# 6. 해석: JSON 출력 읽기 + 시각적으로 diff 검사
# 7. 스타일 수정, 현재 스크린샷 재촬영, 다시 diff
정리
모든 출력 파일은 temp/ 디렉터리에 저장되며, 이는 .gitignore에 포함됩니다. 세션 후 정리하려면:
rm -f temp/vdiff-*.png