핵심 포인트: aria-invalid는 필드가 에러 상태임을 알리고, aria-describedby는 에러 메시지를 해당 필드에 연결한다. role="alert"는 에러가 나타나는 즉시 스크린 리더가 읽어준다.
Chrome DevTools의 Accessibility 탭에서 ARIA 트리 구조를 직접 확인할 수 있다 (출처: Chrome DevTools 문서)
자동화 도구와 수동 테스트 조합 전략
접근성 테스트의 불편한 진실: 자동화 도구는 전체 접근성 이슈의 30~40%만 잡는다(Deque 연구 기준). 나머지는 수동 테스트가 필수다. 다만, 그 30~40%가 가장 빈번하고 수정이 쉬운 항목이므로 자동화부터 시작하는 게 맞다.
도구별 커버리지
도구
용도
커버 영역
axe DevTools
브라우저 확장 + CI
시맨틱, 대비, ARIA 기본
Lighthouse
빌트인 감사
기본 접근성 점수
WAVE
시각적 오버레이
구조, 대비, 라벨
VoiceOver (macOS)
스크린 리더 수동 테스트
실제 사용자 경험
NVDA (Windows)
스크린 리더 수동 테스트
실제 사용자 경험
Playwright + axe-core로 접근성 테스트를 E2E에 통합
// tests/a11y.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('메인 페이지 접근성 위반 없음', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});
test('로그인 폼 접근성 위반 없음', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page })
.include('#login-form')
.analyze();
expect(results.violations).toEqual([]);
});
Playwright + axe-core 조합은 CI에서 자동으로 접근성을 검증하는 가장 실용적인 방법이다. 페이지별로 테스트를 나누면, 어떤 페이지에서 어떤 위반이 생겼는지 PR 단계에서 바로 확인할 수 있다.
자주 놓치는 실수 5가지
체크리스트를 모두 통과해도 반복적으로 나타나는 패턴이 있다. 코드 리뷰에서 특히 주의해야 할 항목이다.
실수 1: aria-hidden="true"를 포커스 가능한 요소에 사용 모달을 닫을 때 모달 전체에 aria-hidden을 걸면, 안에 있는 포커스 가능 요소가 보이지 않는데 포커스는 잡히는 유령 상태가 된다. 모달은 DOM에서 제거하거나 display:none을 사용할 것.
실수 2: SVG 아이콘에 접근성 처리 누락 의미 있는 아이콘: <svg role="img" aria-label="설정">. 장식용 아이콘: <svg aria-hidden="true">. 아이콘 버튼이면 버튼 자체에 aria-label을 달 것.
실수 3: SPA 라우트 전환 시 포커스/알림 누락 페이지가 전환됐는데 스크린 리더에 아무 알림이 없으면, 사용자는 무슨 일이 일어났는지 모른다. 라우트 전환 시 새 페이지 제목을 aria-live 영역에 알리거나, 메인 콘텐츠에 포커스를 이동시킬 것.
실수 4: 커스텀 셀렉트(select) 컴포넌트 디자인 때문에 <select>를 div 기반으로 재구현하면서 키보드 내비게이션, ARIA 역할, 스크린 리더 호환을 모두 놓치는 경우가 많다. Radix UI, Headless UI 같은 접근성 내장 라이브러리를 사용하는 것이 현실적이다.
실수 5: 타임아웃이 있는 세션에 연장 옵션 없음 WCAG 2.2 기준 2.2.1: 시간 제한이 있는 경우 사용자가 연장하거나 해제할 수 있어야 한다. 인증 세션 만료 전 경고 모달과 연장 버튼을 제공할 것.