디자인 QA 지옥에서 탈출하기: 픽셀 비교 기반 자동화 테스트 도입기

팀에서 오래 고민하던 문제를 정리해 사내 발표용으로 쓴 글입니다.

문제의 시작: 손에서 손으로 전해지는 버그들

5년차 프론트엔드 개발자로서 최근 가장 큰 고민은 디자인 시스템 업데이트였습니다. 우리 팀은 새로운 디자인 토큰을 도입하고, 컴포넌트 라이브러리를 전면 개편하는 대규모 작업을 진행했습니다. 문제는 배포 후였습니다.

“버튼이 이상하게 보여요!”, “헤더가 겹쳐져 있어요!”, “모바일에서 레이아웃이 깨졌어요!”

하루에도 수십 개씩 쏟아지는 버그 리포트. 더 큰 문제는 이런 시각적 버그들이 기능 테스트를 모두 통과했다는 점이었습니다. Jest로 작성한 유닛 테스트도, E2E 테스트도 모두 그린이었죠. 왜냐하면 이들은 DOM 구조나 이벤트 동작만 검증할 뿐, 실제로 화면에 어떻게 보이는지는 검증하지 않았기 때문입니다.

수동 검증의 한계

처음에는 팀원들과 함께 체크리스트를 만들어 수동으로 검증했습니다:

text
□ 홈페이지 - 데스크탑
□ 홈페이지 - 모바일
□ 상품 목록 - 데스크탑
□ 상품 목록 - 모바일
□ 상품 상세 - 데스크탑
□ 상품 상세 - 모바일
... (100개 이상의 항목들)

이 방법의 문제점은 명확했습니다:

  • 시간 소모적: 전체 페이지를 확인하는 데 최소 2-3시간
  • 휴먼 에러: 피로감으로 인한 실수, 미묘한 변화 놓침
  • 일관성 부족: 검증하는 사람마다 다른 기준
  • 반복 작업: 매 배포마다 동일한 작업 반복

해결책 탐색: Visual Regression Testing의 세계

이 문제를 해결하기 위해 Visual Regression Testing(시각적 회귀 테스트)에 대해 조사하기 시작했습니다. 여러 접근 방법이 있었습니다:

1. Pixel-by-Pixel Comparison

  • 도구: Percy, Chromatic, Applitools
  • 장점: 매우 정밀한 비교, 클라우드 기반 관리
  • 단점: 비용 발생, 외부 서비스 의존성

2. DOM Snapshot Testing

  • 도구: Jest Snapshot, React Testing Library
  • 장점: 빠른 실행, 기존 테스트 환경과 통합 용이
  • 단점: 시각적 변화 감지 불가, HTML 구조만 검증

3. CSS Regression Testing

  • 도구: BackstopJS, Wraith
  • 장점: 오픈소스, 설정 간단
  • 단점: 유지보수 중단되거나 활발하지 않음

4. Browser Automation with Screenshots

  • 도구: Playwright, Puppeteer, Cypress
  • 장점: 강력한 자동화 기능, 활발한 커뮤니티
  • 단점: 초기 설정 복잡도

왜 Playwright를 선택했나?

여러 옵션을 검토한 후, Playwright를 선택한 이유는 다음과 같습니다:

1. 내장된 Visual Comparison 기능

Playwright는 별도의 플러그인 없이 toHaveScreenshot() API를 통해 시각적 비교를 지원합니다. 이는 추가 의존성을 최소화하면서도 강력한 기능을 제공합니다.

javascript
await expect(page).toHaveScreenshot();

2. 크로스 브라우저 지원

Chrome, Firefox, Safari를 모두 지원하여 다양한 브라우저에서의 렌더링 차이를 한 번에 검증할 수 있습니다.

3. 유연한 설정 옵션

Flaky한 테스트를 다루기 위한 다양한 옵션들:

  • Custom CSS 주입을 통한 동적 콘텐츠 숨김
  • Viewport 크기 조절
  • 네트워크 대기 조건 설정

4. 활발한 개발과 커뮤니티

Microsoft가 관리하는 프로젝트로, 지속적인 업데이트와 훌륭한 문서화가 장점입니다.

5. CI/CD 통합 용이성

GitHub Actions, Jenkins 등 다양한 CI 도구와의 통합이 간단합니다.

실제 구현: 단계별 접근

Step 1: 기본 환경 설정

프로젝트 구조를 깔끔하게 유지하기 위해 별도의 디렉토리를 생성했습니다:

text
test/
└── visual/
    ├── package.json
    ├── playwright.config.js
    └── tests/

package.json에 필요한 스크립트들을 정의:

json
{
  "scripts": {
    "test": "playwright test",
    "update": "playwright test --update-snapshots",
    "report": "playwright show-report"
  }
}

Step 2: 자동 테스트 생성을 위한 크롤러 구현

수백 개의 페이지를 일일이 테스트 코드로 작성하는 것은 비현실적이었습니다. 대신 사이트맵을 기반으로 자동으로 테스트를 생성하는 방식을 채택했습니다:

javascript
// 사이트 크롤링 및 URL 수집
async function createSiteMap(baseURL, page) {
    await page.goto(baseURL + "/sitemap");
    let urls = await page.evaluate(() => {
        let links = new Set();
        for(let { href } of document.links) {
            if(href.startsWith(location.origin)) {
                links.add(new URL(href).pathname);
            }
        }
        return Array.from(links);
    });
    return urls;
}

Step 3: 동적 테스트 생성

수집된 URL들을 기반으로 테스트를 동적으로 생성:

javascript
for(let url of sitemap) {
    test(`페이지 스크린샷: ${url}`, async ({ page }) => {
        await page.goto(url);
        await page.waitForLoadState('networkidle');
        await expect(page).toHaveScreenshot({
            fullPage: true,
            animations: 'disabled'
        });
    });
}

Step 4: Flaky 테스트 대응

시각적 테스트의 가장 큰 문제는 불안정성(flakiness)입니다. 이를 해결하기 위한 전략들:

동적 콘텐츠 숨김

css
/* 랜덤 배너, 애니메이션 등 숨김 */
.random-banner,
.loading-spinner,
.timestamp {
    visibility: hidden !important;
}

네트워크 안정화 대기

javascript
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500); // 추가 안정화 시간

Threshold 설정

javascript
await expect(page).toHaveScreenshot({
    maxDiffPixels: 100,
    threshold: 0.2 // 20% 차이는 허용
});

실전 적용 결과

성과

  1. 버그 감소율: 시각적 버그 리포트 85% 감소
  2. 검증 시간: 3시간 → 15분으로 단축
  3. 배포 신뢰도: 팀원들의 배포 불안감 해소

실제 잡아낸 버그 예시

  • CSS 우선순위 문제로 인한 버튼 스타일 깨짐
  • 반응형 브레이크포인트에서의 레이아웃 겹침
  • 다크모드 전환 시 누락된 컬러 토큰
  • 폰트 로딩 타이밍 이슈로 인한 레이아웃 시프트

도입 시 고려사항

  1. 스크린샷 관리
    • Git LFS 사용 권장 (이미지 파일 크기)
    • 주기적인 베이스라인 업데이트 필요
  2. CI 파이프라인 최적화
    • 병렬 실행으로 시간 단축
    • 실패 시에만 상세 리포트 생성
  3. 팀 교육
    • 베이스라인 업데이트 방법
    • 실패한 테스트 디버깅 방법

추가 개선 아이디어

1. Component-level Testing

전체 페이지 대신 개별 컴포넌트 수준의 시각적 테스트:

javascript
test('Button 컴포넌트 상태별 스크린샷', async ({ page }) => {
    await page.goto('/storybook/button');

    // 각 상태별로 스크린샷
    for(const state of ['default', 'hover', 'active', 'disabled']) {
        await page.click(`[data-state="${state}"]`);
        await expect(page.locator('.button')).toHaveScreenshot(`button-${state}.png`);
    }
});

2. Performance Budget 통합

시각적 테스트와 함께 성능 지표도 검증:

javascript
const metrics = await page.evaluate(() => ({
    lcp: performance.getEntriesByType('largest-contentful-paint')[0]?.startTime,
    cls: // Cumulative Layout Shift 계산
}));

expect(metrics.lcp).toBeLessThan(2500); // 2.5초 이내

3. A/B 테스트 지원

Feature flag에 따른 다른 UI 버전 테스트:

javascript
for(const variant of ['control', 'variant-a', 'variant-b']) {
    await page.addInitScript(`window.FEATURE_FLAGS = { variant: '${variant}' }`);
    await page.goto(url);
    await expect(page).toHaveScreenshot(`${url}-${variant}.png`);
}

마치며

Visual Regression Testing 도입은 단순히 버그를 줄이는 것 이상의 가치를 제공했습니다. 개발자들이 자신감을 가지고 리팩토링할 수 있게 되었고, 디자인 시스템 변경의 영향도를 정확히 파악할 수 있게 되었습니다.

물론 만능은 아닙니다. 초기 설정의 복잡도, 테스트 유지보수의 부담, 그리고 완벽하지 않은 안정성 등의 과제가 남아있습니다. 하지만 이러한 단점들을 감안하더라도, 대규모 프론트엔드 프로젝트에서 Visual Regression Testing은 필수적인 안전장치라고 생각합니다.

특히 Playwright의 강력한 기능과 유연성은 이러한 도전을 극복하는 데 큰 도움이 되었습니다. 앞으로도 계속해서 테스트 전략을 개선하며, 더 안정적이고 신뢰할 수 있는 프론트엔드 개발 환경을 만들어 나갈 예정입니다.


참고 자료

관련 도구