--- id: testing-visual-regression title: Visual Regression — Percy / Chromatic / Playwright category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [testing, visual, vrt, screenshot, vibe-coding] tech_stack: { language: "TS / Playwright / Storybook", applicable_to: ["Frontend"] } applied_in: [] aliases: [visual regression, VRT, Chromatic, Percy, Applitools, screenshot diff] --- # Visual Regression Testing > 디자인 변경 = unit test 가 못 잡음. **screenshot 비교** = pixel diff. **Chromatic (Storybook), Percy (Browserstack), Applitools (AI), Playwright snapshot**. ## 📖 핵심 개념 - Baseline: 첫 screenshot. - Diff: PR 가 baseline 과 다름 → 사람 review. - Approved: 의도된 변경 → 새 baseline. - Cross-browser / device: 같은 컴포넌트 다양한 환경. ## 💻 코드 패턴 ### Playwright snapshot ```ts import { test, expect } from '@playwright/test'; test('home page visual', async ({ page }) => { await page.goto('/'); await expect(page).toHaveScreenshot('home.png', { maxDiffPixelRatio: 0.01, fullPage: true, }); }); ``` ```bash # 첫 run — baseline 생성 npx playwright test --update-snapshots # 이후 비교 npx playwright test ``` ### Storybook + Chromatic ```bash yarn add -D chromatic ``` ```yaml # .github/workflows/chromatic.yml - run: yarn build-storybook - uses: chromaui/action@v1 with: projectToken: ${{ secrets.CHROMATIC_TOKEN }} ``` → 각 story 자동 screenshot + diff. PR 에 review UI. ### Story 기준 component 단위 ```tsx // Button.stories.tsx import { Button } from './Button'; export default { component: Button }; export const Primary = { args: { variant: 'primary', children: 'Click' } }; export const Disabled = { args: { variant: 'primary', disabled: true, children: 'Click' } }; export const Long = { args: { children: 'Very long button text here' } }; ``` → Chromatic 가 모든 story 자동 snapshot. ### Multi-viewport / theme ```tsx export default { component: Card, parameters: { chromatic: { viewports: [320, 768, 1280], modes: { light: { theme: 'light' }, dark: { theme: 'dark' } }, }, }, }; ``` ### Flake 방어 ```tsx // 시간 / random / animation 고정 parameters: { chromatic: { pauseAnimationAtEnd: true, delay: 500, // 데이터 load 기다림 }, } ``` ```ts // 시간 mock (Playwright) await page.evaluate(() => { const fixedDate = new Date('2026-05-09T10:00:00Z').valueOf(); Date.now = () => fixedDate; }); // Font ready await page.waitForLoadState('networkidle'); await page.evaluate(() => document.fonts.ready); ``` ### Threshold ```ts await expect(page).toHaveScreenshot('home.png', { maxDiffPixelRatio: 0.005, // 0.5% pixel 차이까지 OK (anti-aliasing) threshold: 0.1, // pixel 차이 정도 }); ``` ### Mask (변경 영역 가리기) ```ts await expect(page).toHaveScreenshot({ mask: [page.locator('.timestamp'), page.locator('[data-dynamic]')], }); ``` → 시간 / random 부분 mask. ### Cross-browser ```ts // playwright.config.ts projects: [ { name: 'chromium', use: devices['Desktop Chrome'] }, { name: 'firefox', use: devices['Desktop Firefox'] }, { name: 'webkit', use: devices['Desktop Safari'] }, ], ``` → 각 browser 별 baseline. ### Applitools (AI 기반) ```ts import { Eyes, Target } from '@applitools/eyes-playwright'; test('home', async ({ page }) => { const eyes = new Eyes(); await eyes.open(page, 'My App', 'home'); await page.goto('/'); await eyes.check('main', Target.window().fully()); await eyes.close(); }); ``` → AI 가 의미 있는 변경 / 노이즈 구분. ### Workflow ``` 1. PR 만듦 2. Chromatic / Percy 가 자동 screenshot 3. PR comment 에 diff URL 4. Designer / dev review 5. Approve = 새 baseline / Reject = 코드 fix ``` ### Approve 정책 - 모든 변경 디자이너 review 또는 - Spec 변경 시만 review, 일반 = dev approve. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Storybook 사용 중 | Chromatic | | 브라우저 / 디바이스 다양 | Percy | | 정밀 + 의미 분석 | Applitools | | 자체 / 무료 | Playwright snapshot | | Storybook 없음 | Playwright + 페이지 스크린샷 | | Email rendering | Mailosaur / Litmus | ## ❌ 안티패턴 - **Animation 안 정지**: 매번 다른 frame. - **시간 / random data**: noise. mock. - **External 이미지 / font load**: lazy. preload + ready check. - **모든 page snapshot**: 비용 폭발. 핵심 + storybook component. - **Threshold 0**: false positive. 0.5% 정도. - **Baseline 매번 update**: 의도 모름. 변경 review 필수. - **Cross-browser 무시**: Safari rendering 차이. ## 🤖 LLM 활용 힌트 - Storybook + Chromatic = 컴포넌트 단위 강력. - Pause animation + mask dynamic + font ready 3종. - Threshold 0.5% + cross-browser. ## 🔗 관련 문서 - [[Testing_Playwright_Advanced]] - [[Frontend_A11y_Testing]] - [[Frontend_Design_Tokens]]