Files
2nd/10_Wiki/Topics/Coding/React_Accessibility_Patterns.md
T
2026-05-09 21:08:02 +09:00

120 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: react-accessibility-patterns
title: React 접근성 (a11y) 실전
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [react, accessibility, a11y, aria, vibe-coding]
tech_stack: { language: "TypeScript / React", applicable_to: ["Web"] }
applied_in: []
aliases: [WAI-ARIA, screen reader, focus management, semantic HTML]
---
# React 접근성 실전
> 시멘틱 HTML 우선 → 그래도 부족하면 ARIA. **`<div onClick>` 은 한 번도 정답이 아니다**. `<button>` 으로 시작하라. 그 다음 키보드 / 포커스 / 스크린리더 4가지 점검.
## 📖 핵심 개념
- 시멘틱: button / a / input / nav / main / section — 의미 가진 태그.
- ARIA: 시멘틱이 부족할 때만. role / aria-label / aria-describedby 등.
- 키보드: Tab / Enter / Space / Esc / Arrow 동작.
- 포커스 trap: modal 안에 갇히도록.
- announcement: live region 으로 동적 변화 알림.
## 💻 코드 패턴
### Modal — focus trap + ESC + announcement
```tsx
import { useEffect, useRef } from 'react';
import FocusTrap from 'focus-trap-react';
function Modal({ open, onClose, title, children }) {
const triggerRef = useRef<HTMLElement>();
useEffect(() => {
if (open) {
triggerRef.current = document.activeElement as HTMLElement;
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('keydown', onKey);
triggerRef.current?.focus(); // 닫힐 때 trigger 로 포커스 복귀
};
}
}, [open, onClose]);
if (!open) return null;
return (
<FocusTrap>
<div role="dialog" aria-modal="true" aria-labelledby="dlg-title">
<h2 id="dlg-title">{title}</h2>
{children}
<button onClick={onClose} aria-label="닫기">×</button>
</div>
</FocusTrap>
);
}
```
### Live region — 비동기 변화 알림
```tsx
const [status, setStatus] = useState('');
return <>
<button onClick={async () => {
setStatus('저장 중...');
await save();
setStatus('저장 완료');
}}></button>
<div role="status" aria-live="polite" className="sr-only">{status}</div>
</>;
```
### Form — label / error / aria-invalid
```tsx
<label htmlFor="email"></label>
<input
id="email"
type="email"
aria-invalid={!!errors.email}
aria-describedby={errors.email ? "email-error" : undefined}
/>
{errors.email && <p id="email-error" role="alert">{errors.email}</p>}
```
### Skip link
```tsx
<a href="#main" className="skip-link"> </a>
<main id="main" tabIndex={-1}>...</main>
```
## 🤔 의사결정 기준
| 요소 | 시멘틱 / ARIA |
|---|---|
| 클릭 가능 | `<button>` (이동은 `<a>`) |
| Modal | `role="dialog"` + focus trap |
| Tab / Tooltip / Combobox | Headless UI / Radix UI 사용 권장 |
| 동적 토스트 | `aria-live="polite"` |
| 즉시 알림 (에러) | `role="alert"` 또는 `aria-live="assertive"` |
| 아이콘 버튼 | `aria-label` 필수 |
## ❌ 안티패턴
- **`<div onClick>`**: 키보드 접근 X. 포커스 X. role 없음. 항상 button/a.
- **placeholder 만 + label 없음**: 스크린리더에게 의미 없음.
- **색상으로만 에러 표시**: 색맹 사용자 + 스크린리더 못 봄. 텍스트 + 아이콘.
- **focus indicator outline:none**: 키보드 사용자 길 잃음. 커스텀 outline 으로 대체.
- **modal 안 닫힐 때 trigger 로 포커스 안 돌림**: 사용자가 어디서 시작했는지 잃음.
- **랜덤 ARIA roles**: `role="button"``<div>`. 그냥 `<button>` 쓰면 됨.
- **aria-hidden 으로 시각적 hide**: 스크린리더도 안 읽음. 시각만 hide 면 CSS clip path / sr-only.
- **테스트 안 함**: VoiceOver / NVDA / 키보드만 정기 테스트.
## 🤖 LLM 활용 힌트
- "div onClick 금지, button/a 우선. modal 은 focus trap + ESC + label" 강제.
- Headless UI 라이브러리 (Radix, ARIA Headless) 적극 활용 권장.
## 🔗 관련 문서
- [[React_Component_Composition]]
- [[React_Form_State_Patterns]]