f8b21af4be
10_Wiki/Topics 대규모 정리: - 오류 캡처/미완성 stub 문서 227개 제거 - 교차폴더 중복 43클러스터 병합 (63파일 → redirect) - 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건 - 카테고리 MOC 6개 신규 생성 - Graph 섹션 미해결 related-keyword 링크 10,058건 제거 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
4.4 KiB
Markdown
160 lines
4.4 KiB
Markdown
---
|
|
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(<Button>Save</Button>);
|
|
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
|
|
<!-- ❌ button 처럼 만들지 마 -->
|
|
<div onclick="..." role="button">Save</div>
|
|
|
|
<!-- ✅ -->
|
|
<button>Save</button>
|
|
```
|
|
|
|
### Form — label 연결
|
|
```html
|
|
<label for="email">Email</label>
|
|
<input id="email" type="email" required aria-describedby="email-error">
|
|
<p id="email-error" role="alert">잘못된 이메일</p>
|
|
```
|
|
|
|
### 모달 focus trap
|
|
```tsx
|
|
import { FocusTrap } from 'focus-trap-react';
|
|
|
|
{open && (
|
|
<FocusTrap focusTrapOptions={{ initialFocus: '#confirm', returnFocusOnDeactivate: true }}>
|
|
<div role="dialog" aria-modal="true" aria-labelledby="t">
|
|
<h2 id="t">정말 삭제?</h2>
|
|
<button id="confirm">삭제</button>
|
|
<button onClick={() => setOpen(false)}>취소</button>
|
|
</div>
|
|
</FocusTrap>
|
|
)}
|
|
```
|
|
|
|
또는 Radix UI / Headless UI — focus trap 자동.
|
|
|
|
### Live region
|
|
```tsx
|
|
<div role="status" aria-live="polite" className="sr-only">
|
|
{savedAt && `Saved at ${savedAt}`}
|
|
</div>
|
|
```
|
|
|
|
### Skip link
|
|
```html
|
|
<a href="#main" className="sr-only focus:not-sr-only">Skip to main content</a>
|
|
<main id="main">...</main>
|
|
```
|
|
|
|
### 색대비
|
|
```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 |
|
|
|
|
## ❌ 안티패턴
|
|
- **`<div onClick>` 모든 것을**: 키보드/스크린리더 없음.
|
|
- **Tabindex 양수**: tab 순서 깨짐. `0` 또는 `-1` 만.
|
|
- **`outline: none` 만**: focus-visible 대체.
|
|
- **모달 `<div>` 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]]
|