--- id: frontend-a11y-testing title: A11y Testing — axe / 키보드 / 스크린리더 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, a11y, accessibility, testing, vibe-coding] tech_stack: { language: "TS / React", applicable_to: ["Web"] } applied_in: [] aliases: [a11y, accessibility, axe, WCAG, ARIA, screen reader] --- # A11y Testing > 자동화는 30% — 나머지 70% 는 키보드 + 스크린리더 + 색대비. **`axe-core` (개발) + `@axe-core/playwright` (E2E)** 가 기본. ARIA 보다 native HTML 우선. ## 📖 핵심 개념 - WCAG 2.1 AA: 일반 기준. - ARIA: HTML 으로 표현 안 되는 의미 추가. 그러나 native > ARIA. - Semantic HTML: button, nav, main, header, footer. - Focus management: 모달 열림 → 모달 안 / 닫힘 → 트리거. ## 💻 코드 패턴 ### axe-core dev — 자동 검사 ```tsx // react app, dev only if (process.env.NODE_ENV !== 'production') { import('@axe-core/react').then(({ default: axe }) => { axe(React, ReactDOM, 1000); }); } ``` ### Playwright E2E ```ts import { expect, test } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test('home page is accessible', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); }); ``` ### Storybook + a11y addon ```ts // .storybook/main.ts addons: ['@storybook/addon-a11y'], ``` 스토리에 자동 panel — violations 보여줌. ### React Testing Library + jest-axe ```tsx import { axe } from 'jest-axe'; import { render } from '@testing-library/react'; test('Button has no a11y violations', async () => { const { container } = render(); expect(await axe(container)).toHaveNoViolations(); }); ``` ### 키보드 — 모든 interactive 가능? ```ts test('navigates with keyboard', async ({ page }) => { await page.goto('/'); await page.keyboard.press('Tab'); await expect(page.locator(':focus')).toHaveText('Skip to main content'); await page.keyboard.press('Enter'); await expect(page.locator('main')).toBeFocused(); }); ``` ### Native HTML 우선 ```html
잘못된 이메일
``` ### 모달 focus trap ```tsx import { FocusTrap } from 'focus-trap-react'; {open && (