도입 스토리
"PR을 올렸더니 팀장이 접근성 검사 결과가 없다고 했어요." 김개발이 말했습니다.
"맞아요. 우리 팀은 이제 접근성 점수가 90점 이하면 PR을 머지할 수 없어요." 박멘토가 GitHub Actions 화면을 보여주었습니다. "CI에서 Lighthouse가 돌아가고, 접근성 점수가 기준 미달이면 빨간 체크가 뜨거든요."
"이걸 어떻게 설정했어요?"
"Lighthouse CI와 GitHub Actions 조합이에요. 설정 파일 몇 개면 자동화돼요."
핵심 개념 설명
GitHub Actions + Playwright + axe 파이프라인
# .github/workflows/accessibility.ymlname: Accessibility CIon: pull_request: branches: [main, develop]jobs: accessibility: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Start server run: npm start & env: CI: true - name: Wait for server run: npx wait-on http://localhost:3000 - name: Install Playwright run: npx playwright install --with-deps chromium - name: Run accessibility tests run: npx playwright test --project=chromium tests/accessibility/ - name: Upload test results uses: actions/upload-artifact@v4 if: failure() with: name: playwright-report path: playwright-report/
Playwright 접근성 테스트 스크립트
// tests/accessibility/pages.test.jsconst { test, expect } = require('@playwright/test');const AxeBuilder = require('@axe-core/playwright').default;const pagesToTest = [ { name: '홈', url: '/' }, { name: '로그인', url: '/login' }, { name: '회원가입', url: '/signup' }, { name: '상품 목록', url: '/products' }, { name: '장바구니', url: '/cart' },];for (const { name, url } of pagesToTest) { test(`${name} 페이지 접근성`, async ({ page }) => { await page.goto(url); const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) // WCAG 2.0 A, AA 기준 .exclude('.third-party-widget') // 제어 불가 외부 위젯 제외 .analyze(); // 위반 사항 있으면 상세 내용 출력 후 실패 if (results.violations.length > 0) { console.log('접근성 위반:'); results.violations.forEach(v => { console.log(`\n[${v.impact.toUpperCase()}] ${v.description}`); console.log(` WCAG: ${v.tags.join(', ')}`); v.nodes.forEach(n => { console.log(` 요소: ${n.html}`); }); }); } expect(results.violations).toHaveLength(0); });}
Lighthouse CI 통합
npm install --save-dev @lhci/cli
// lighthouserc.jsmodule.exports = { ci: { collect: { url: [ 'http://localhost:3000/', 'http://localhost:3000/login', 'http://localhost:3000/products', ], numberOfRuns: 3, }, assert: { assertions: { 'categories:accessibility': ['error', { minScore: 0.9 }], 'categories:best-practices': ['warn', { minScore: 0.85 }], // 구체적인 감사 항목 설정 'image-alt': 'error', 'label': 'error', 'color-contrast': 'error', }, }, upload: { target: 'temporary-public-storage', // 결과를 공개 URL로 저장 }, },};
# .github/workflows/lighthouse.yml- name: Run Lighthouse CI run: | npm start & npx wait-on http://localhost:3000 npx lhci autorun env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
PR 코멘트로 결과 리포팅
- name: Run axe and report run: | npx playwright test tests/accessibility/ --reporter=json > a11y-results.json || true- name: Comment PR with results uses: actions/github-script@v7 with: script: | const fs = require('fs'); const results = JSON.parse(fs.readFileSync('a11y-results.json', 'utf8')); const passed = results.suites.flatMap(s => s.specs).filter(s => s.ok).length; const failed = results.suites.flatMap(s => s.specs).filter(s => !s.ok).length; const body = `## 접근성 테스트 결과\n\n` + `| 상태 | 수 |\n|------|---|\n` + `| ✅ 통과 | ${passed} |\n` + `| ❌ 실패 | ${failed} |\n`; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body });
접근성 게이팅 전략
모든 문제를 한 번에 잡으려 하면 기존 코드베이스에 너무 많은 오류가 발생합니다. 단계별 적용을 권장합니다.
Phase 1 (1주차): Critical 오류만 차단
- axe violations: impact=critical만 테스트 실패 처리
Phase 2 (2~4주차): Serious 오류 추가
- axe violations: critical + serious
Phase 3 (1개월 후): 전체 WCAG AA
- Lighthouse 접근성 점수 90+ 게이팅
Phase 4 (지속): WCAG AAA 목표 항목 추가
// Phase 1: Critical만 차단const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) .analyze();const criticalViolations = results.violations.filter( v => v.impact === 'critical');expect(criticalViolations).toHaveLength(0);
단계별 실습
따라하기: GitHub Actions 파이프라인 설정
.github/workflows/accessibility.yml파일을 생성합니다.tests/accessibility/폴더에 페이지별 테스트 파일을 만듭니다.- PR을 올려 GitHub Actions가 자동 실행되는지 확인합니다.
- 의도적으로 접근성 오류를 만들어 파이프라인이 실패하는지 테스트합니다.
변형하기: 특정 컴포넌트 제외 처리
const results = await new AxeBuilder({ page }) .withTags(['wcag2a', 'wcag2aa']) // 알고 있는 문제이지만 아직 수정 중인 경우 임시 제외 .exclude('#legacy-datepicker') // 서드파티 위젯 (제어 불가) .exclude('[data-third-party]') .analyze();
제외 목록은 이슈 트래커 링크와 함께 주석으로 이유를 문서화합니다.
정리와 확인
핵심 내용 요약
- GitHub Actions + Playwright + axe: PR마다 자동 접근성 테스트
- Lighthouse CI: 점수 기반 게이팅 (
minScore: 0.9) - 단계별 도입: Critical → Serious → 전체 WCAG AA 순서로 적용
- PR 코멘트: 테스트 결과를 자동으로 PR에 보고
- 제외 처리: 서드파티 위젯이나 진행 중인 작업은 명시적으로 제외
확인 문제
문제 1. 기존 코드베이스에 axe 테스트를 추가할 때 단계별로 도입하는 이유는?
한 번에 모든 오류를 차단하면 수백 개의 기존 위반으로 파이프라인이 완전히 막힙니다.
Critical 오류만 먼저 막고, 팀이 적응하면서 단계적으로 기준을 높이는 것이 현실적입니다.
문제 2. Lighthouse 접근성 점수 100점이 WCAG 완전 준수를 의미하지 않는 이유는?
Lighthouse는 자동화로 검사 가능한 항목만 평가합니다.
적절한 레이블 내용, 논리적 읽기 순서, 스크린리더 사용자 경험 등
수동 테스트가 필요한 항목은 점수에 반영되지 않습니다.
PART 09를 마쳤습니다. 다음 PART에서는 실전 프로젝트로 지금까지 배운 모든 것을 통합합니다. 접근성 감사(Audit) → 개선 → 검증의 전체 사이클을 경험해 봅니다.