--- 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
Save
``` ### Form — label 연결 ```html ``` ### 모달 focus trap ```tsx import { FocusTrap } from 'focus-trap-react'; {open && (

정말 삭제?

)} ``` 또는 Radix UI / Headless UI — focus trap 자동. ### Live region ```tsx
{savedAt && `Saved at ${savedAt}`}
``` ### Skip link ```html Skip to main content
...
``` ### 색대비 ```css /* WCAG AA: text 4.5:1, large text 3:1 */ .text-on-bg { color: #1a1a1a; background: #fff; } /* 16:1 */ ``` 도구: Chrome DevTools color picker, contrast-ratio.com. ## 🤔 의사결정 기준 | 검사 | 도구 | |---|---| | 자동 (CI) | axe-playwright / axe-react | | 컴포넌트 단위 | jest-axe + RTL | | Storybook | addon-a11y | | 키보드만 | manual test | | 스크린리더 | macOS VoiceOver / NVDA / JAWS | | 색대비 | DevTools / Stark | ## ❌ 안티패턴 - **`
` 모든 것을**: 키보드/스크린리더 없음. - **Tabindex 양수**: tab 순서 깨짐. `0` 또는 `-1` 만. - **`outline: none` 만**: focus-visible 대체. - **모달 `
` body 끝에 — focus 안 trap**: focus trap 라이브러리. - **Image alt 없음**: alt 빈 문자열도 의도. 의미 있는 글로. - **placeholder 만 — label 없음**: 스크린리더 사람 못 들음. - **Color only 의미 (빨강 = 에러)**: 아이콘 / 텍스트 같이. ## 🤖 LLM 활용 힌트 - Native HTML > ARIA. - axe + jest-axe + Playwright 3종. - Radix UI / Headless UI 가 a11y 잘 처리. ## 🔗 관련 문서 - [[Frontend_i18n_Patterns]]