React에서 웹 접근성: 시맨틱 HTML, ARIA, 포커스 관리, 및 axe 테스트
Source: Dev.to
Semantic HTML First
보통 어려워 보이는 접근성 문제는 대부분 의미론적 HTML 문제에 불과합니다:
// Bad – div soup, no semantics
Home
About
// Good – semantic HTML, keyboard accessible automatically
- Home
- About
랜드마크 요소(````, , , , )는 스크린리더 사용자가 섹션 사이를 빠르게 이동할 수 있게 해줍니다.
ARIA: When and When Not To
ARIA 속성은 HTML만으로는 충분하지 않을 때 의미를 추가하지만, 동작을 추가하지는 않습니다 — 키보드 인터랙션은 직접 구현해야 합니다:
// Custom dropdown – requires ARIA + keyboard handling
function Dropdown({ label, options, value, onChange }) {
const [isOpen, setIsOpen] = useState(false)
const listboxId = useId()
return (
setIsOpen(!isOpen)}
onKeyDown={(e) => {
if (e.key === 'Escape') setIsOpen(false)
if (e.key === 'ArrowDown') {
setIsOpen(true)
// focus first option
}
}}
>
{value || label}
{isOpen && (
{options.map(opt => (
{ onChange(opt.value); setIsOpen(false) }}
tabIndex={0}
>
{opt.label}
))}
)}
)
}
Rule: 네이티브 HTML 요소가 필요를 충족한다면(````, , ) 그대로 사용하세요. ARIA는 커스텀 위젯을 위해 존재합니다.
Focus Management
// Dialog – trap focus inside when open
import { useEffect, useRef } from 'react'
function Dialog({ isOpen, onClose, children }) {
const dialogRef = useRef(null)
useEffect(() => {
if (!isOpen) return
// Focus the dialog on open
dialogRef.current?.focus()
// Trap focus
const focusable = dialogRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
)
const first = focusable?.[0] as HTMLElement
const last = focusable?.[focusable.length - 1] as HTMLElement
const trap = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return
if (e.shiftKey ? document.activeElement === first : document.activeElement === last) {
e.preventDefault()
;(e.shiftKey ? last : first)?.focus()
}
}
document.addEventListener('keydown', trap)
return () => document.removeEventListener('keydown', trap)
}, [isOpen])
return (
e.key === 'Escape' && onClose()}
>
{children}
)
}
Color Contrast and Visual Design
WCAG AA는 일반 텍스트에 대해 4.5:1, 큰 텍스트에 대해 3:1의 대비 비율을 요구합니다.
// Check contrast in your Tailwind config
// Gray‑500 on white: 3.95:1 – FAILS AA for normal text
// Gray‑700 on white: 8.59:1 – passes AAA
// Don't rely on color alone to convey information
// Bad: red = error (invisible to color‑blind users)
// Good: red + icon + error message text
Email is invalid
Automated Testing with axe
npm install -D @axe-core/react jest-axe
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
it('has no accessibility violations', async () => {
const { container } = render()
const results = await axe(container)
expect(results).toHaveNoViolations()
})
CI 환경에서 axe를 실행해 사용자에게 도달하기 전에 회귀를 잡아냅니다.
AI SaaS Starter는 whoffagents.com에서 제공되며, 의미론적 HTML 구조, 모든 인터랙티브 요소에 대한 적절한 ARIA 레이블, 그리고 테스트 스위트에 통합된 axe‑core를 포함합니다. $99 일회성 결제.