Playwright와 Axe로 접근성 테스트를 자동화하는 방법

발행: (2025년 12월 10일 오후 10:18 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

우리의 툴킷: Axe + Playwright

우리는 접근성 테스트 엔진으로 Axe(오픈‑소스 라이브러리, Deque Systems 제공)를 선택했습니다. 브라우저에서 직접 테스트를 실행할 수 있는 JavaScript API를 제공하며, @axe-core/playwright 패키지를 통해 통합이 매끄럽게 이루어집니다.

이미 시각적 회귀 테스트와 엔드‑투‑엔드 스위트에 Playwright를 사용하고 있었기 때문에, 그 위에 접근성 검사를 추가하는 것이 다음 단계로 자연스러웠습니다—새로운 도구를 배울 필요 없이, Axe 엔진을 동일한 Playwright 워크플로우 안에서 확장하는 것이었습니다.

설정

먼저, 사전 설정된 Axe 인스턴스를 반환하는 헬퍼를 만들었습니다. 우리의 설정은 WCAG 2.1 Level A 및 AA 기준에 초점을 맞춥니다.

WCAG란? 웹 콘텐츠 접근성 지침(WCAG)은 W3C에서 웹 콘텐츠를 보다 접근 가능하게 만들기 위해 개발한 표준입니다.

  • Level A: 최소 수준의 준수.
  • Level AA: 우리가 목표로 하는 중간 수준으로, 보다 복잡한 장벽을 다룹니다.
  • Level AAA: 가장 높은, 가장 엄격한 수준.

또한, 제3자 광고와 같이 직접 제어할 수 없는 요소들을 명시적으로 제외하여 오탐지를 방지합니다.

// /test/utils/axe.ts
import { Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

export const getAxeInstance = (page: Page) => {
  return new AxeBuilder({ page })
    // Target WCAG 2.1 A and AA success criteria
    .withTags(['wcag2a', 'wcag2aa'])
    // Exclude elements we don't control, like ads
    .exclude('[id^="google_ads_iframe_"]')
    .exclude('#skinadvtop2')
    .exclude('#subito_skin_id');
};

구현: 보고서 생성 및 저장

다음으로, 분석을 실행하고 결과를 JSON 파일에 기록하는 헬퍼 generateAxeReport를 추가했습니다.

// /test/utils/axe.ts
import { Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { Result } from 'axe-core';
import * as fs from 'fs';
import * as path from 'path';
import { getAxeInstance } from './axe'; // assuming same file

export const generateAxeReport = async (
  name: string,
  page: Page,
  isMobile: boolean,
  includeSelector?: string
) => {
  let axe = getAxeInstance(page);

  // Optionally scope the analysis to a specific selector
  if (includeSelector) {
    axe = axe.include(includeSelector);
  }

  const results = await axe.analyze();
  const violations = results.violations;

  // Save the results to a JSON file
  await saveAccessibilityResults(name, violations, isMobile);

  return violations;
};

async function saveAccessibilityResults(
  fileName: string,
  violations: Array,
  isMobile: boolean
) {
  const outputDir = 'test/a11y/output';

  if (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { recursive: true });
  }

  const filePath = path.join(
    outputDir,
    `${fileName}-${isMobile ? 'mobile' : 'desktop'}.json`
  );

  // Map violations to a clean object for serialization
  const escapedViolations = violations.map((violation) => ({
    id: violation.id,
    impact: violation.impact,
    description: violation.description,
    help: violation.help,
    helpUrl: violation.helpUrl,
    nodes: violation.nodes,
  }));

  fs.writeFileSync(filePath, JSON.stringify(escapedViolations, null, 2));
  console.log(`Accessibility results saved to ${filePath}`);
}

A11y 테스트

이 헬퍼들을 준비하면, 어떤 Playwright 테스트에도 접근성 검사를 손쉽게 추가할 수 있습니다.

// /test/a11y/example.spec.ts
import { test } from '@playwright/test';
import { generateAxeReport } from '../utils/axe';

test('check Login page', async ({ page }) => {
  await page.goto('/login_form');
  await page.waitForLoadState('domcontentloaded');

  // Run the helper
  await generateAxeReport('login-page', page, false);
});

테스트를 실행하면 JSON 보고서(예: login-page-desktop.json)가 생성되어 모든 접근성 이슈가 포함됩니다.

Accessibility report example

지속적 통합(CI)과의 연동

우리 CI 워크플로우는 모든 스테이징 배포 시 트리거됩니다. 흐름은 다음과 같습니다.

  1. 실행: 사전 정의된 핵심 페이지 목록에 대해 접근성 테스트를 수행합니다.
  2. 생성: JSON 보고서를 생성합니다.
  3. 업데이트 또는 생성: 위반 사항이 발견될 때마다 전용 GitHub Issue를 업데이트하거나 새로 만듭니다.

자동으로 생성된 이슈는 다음과 같은 형태입니다:

GitHub issue with accessibility report

그리고 위반 상세 목록:

Violation details

왜 GitHub Issue인가? (빌드 실패가 아닌 이유)

우리의 시각적 회귀 테스트와 달리, PR을 열고 Slack 알림을 보내는 대신 접근성 결과를 GitHub Issue에 기록하기로 했습니다.

아직 접근성 커버리지를 구축 중이기 때문에, 모든 위반에 대해 파이프라인을 실패시키는 것은 지속 가능하지 않습니다. Issue를 활용함으로써:

  • 접근성 부채에 대한 영구적인 기록을 유지합니다.
  • 저장소 관리자가 triage, 우선순위 지정, 수정 일정을 잡는 책임을 가집니다.

아래는 GitHub Issue에 기록된 내용을 해결한 예시 풀 리퀘스트입니다.

(이미지는 생략)

Back to Blog

관련 글

더 보기 »

1283일 차: 시도와 시도

프로페셔널하게 꽤 여유로운 하루였어요. 몇 차례 회의에서 제가 작업해 온 것을 데모했으며 매번 성공적이었습니다. 커뮤니티 질문 몇 개에 답변했습니다.