1. 왜 접근성인가
10억 명의 사용자
전 세계 인구의 약 15%, 약 10억 명이 어떤 형태로든 장애를 가지고 있습니다. 웹 접근성은 단순히 "좋은 일"이 아니라 비즈니스 필수 사항입니다.
| 장애 유형 | 전 세계 인구 | 웹에서의 영향 |
|---|---|---|
| 시각 장애 | 2.2억 명 | 스크린 리더, 확대기 사용 |
| 청각 장애 | 4.66억 명 | 자막, 수화 필요 |
| 운동 장애 | 수억 명 | 키보드, 음성 입력 |
| 인지 장애 | 다양 | 단순한 UI, 명확한 언어 |
법적 요구사항
- **미국 ADA**: 웹사이트도 "공공 편의시설"로 간주. 소송 급증
- **EU EAA (European Accessibility Act)**: 2025년 6월 시행. 디지털 서비스 필수
- **한국 장애인차별금지법**: 웹 접근성 의무. 공공기관 + 민간 확대
- **WCAG 2.2**: 국제 표준. 대부분의 법률이 AA 레벨을 요구
비즈니스 가치
접근성은 비용이 아니라 투자입니다.
- **SEO 개선**: 시맨틱 HTML과 alt 텍스트는 검색 엔진이 좋아합니다
- **사용자 확대**: 전체 인구의 15%에게 도달
- **법적 리스크 감소**: 소송 비용 대비 사전 투자가 훨씬 저렴
- **일반 사용자 경험 향상**: 키보드 단축키, 명확한 레이블은 모든 사용자에게 유익
2. WCAG 2.2: 4가지 원칙
POUR 원칙
WCAG는 4가지 핵심 원칙 POUR(Perceivable, Operable, Understandable, Robust)을 기반으로 합니다.
| 원칙 | 의미 | 예시 |
|---|---|---|
| Perceivable (인지 가능) | 정보를 인지할 수 있어야 함 | alt 텍스트, 자막, 색상 대비 |
| Operable (운용 가능) | UI를 조작할 수 있어야 함 | 키보드, 충분한 시간, 발작 방지 |
| Understandable (이해 가능) | 콘텐츠와 UI를 이해할 수 있어야 함 | 명확한 언어, 일관된 내비게이션 |
| Robust (견고) | 다양한 기술에서 작동해야 함 | 유효한 HTML, ARIA 호환 |
적합성 레벨
- **Level A**: 최소 요구사항 (필수)
- **Level AA**: 대부분의 법률이 요구하는 수준 (권장 목표)
- **Level AAA**: 최고 수준 (전체 사이트에 적용하기 어려움)
WCAG 2.2의 새로운 성공 기준
WCAG 2.2는 2023년 10월에 발표되었으며, 다음과 같은 새로운 기준이 추가되었습니다.
| 성공 기준 | 레벨 | 설명 |
|---|---|---|
| 2.4.11 Focus Not Obscured (Minimum) | AA | 포커스된 요소가 다른 콘텐츠에 완전히 가려지면 안 됨 |
| 2.4.12 Focus Not Obscured (Enhanced) | AAA | 포커스된 요소가 부분적으로도 가려지면 안 됨 |
| 2.4.13 Focus Appearance | AAA | 포커스 인디케이터 크기와 대비 요구사항 |
| 2.5.7 Dragging Movements | AA | 드래그가 필요한 기능에 대안 제공 |
| 2.5.8 Target Size (Minimum) | AA | 터치 대상 최소 24x24 CSS 픽셀 |
| 3.2.6 Consistent Help | A | 도움말 메커니즘이 일관된 위치에 |
| 3.3.7 Redundant Entry | A | 이전에 입력한 정보를 다시 요구하지 않음 |
| 3.3.8 Accessible Authentication | AA | 인지 기능 테스트 없는 인증 |
| 3.3.9 Accessible Authentication (Enhanced) | AAA | 더 엄격한 인증 접근성 |
3. 시맨틱 HTML: 접근성의 기초
올바른 요소 사용
시맨틱 HTML은 접근성의 80%를 해결합니다. 스크린 리더는 HTML 요소의 의미를 이해합니다.
<!-- 나쁨: div로 모든 것을 만들기 -->
<!-- 좋음: 시맨틱 요소 사용 -->
`<button>`은 자동으로 키보드 포커스를 받고, Enter/Space로 활성화되며, 스크린 리더가 "버튼"으로 인식합니다. `<div>`는 이 모든 것을 수동으로 구현해야 합니다.
랜드마크(Landmarks)
...
스크린 리더 사용자는 랜드마크 간에 빠르게 이동할 수 있습니다. VoiceOver에서 Rotor를 사용하면 header, nav, main, footer로 즉시 점프합니다.
제목 계층 구조
<!-- 나쁨: 제목 레벨 건너뛰기 -->
<!-- 좋음: 순차적 제목 구조 -->
스크린 리더 사용자의 67%가 제목으로 페이지를 탐색합니다. 제목 레벨을 건너뛰면 문서 구조를 파악하기 어렵습니다.
4. ARIA: 접근성 의미 추가
ARIA의 첫 번째 규칙
**ARIA의 첫 번째 규칙: ARIA를 사용하지 마세요.** 네이티브 HTML 요소로 충분하다면 ARIA가 필요 없습니다.
<!-- ARIA 불필요: 네이티브 요소로 충분 -->
<!-- ARIA 필요: 네이티브 요소가 없는 경우 -->
핵심 ARIA 속성
<!-- aria-label: 시각적 텍스트가 없는 요소에 레이블 제공 -->
<!-- aria-labelledby: 다른 요소의 텍스트를 레이블로 참조 -->
<!-- aria-describedby: 추가 설명 연결 -->
type="password"
aria-describedby="pw-hint"
/>
<!-- aria-live: 동적 콘텐츠 변경 알림 -->
장바구니에 3개 상품이 있습니다.
<!-- aria-expanded: 확장/축소 상태 -->
메뉴
<!-- aria-hidden: 스크린 리더에서 숨기기 -->
Live Regions
<!-- aria-live="polite": 현재 읽기를 마친 후 알림 -->
검색 결과: 42건
<!-- aria-live="assertive": 즉시 알림 (에러 등) -->
세션이 만료되었습니다. 다시 로그인해 주세요.
<!-- role="status": 상태 메시지 (polite와 유사) -->
파일 업로드 완료
<!-- role="log": 채팅 메시지 등 -->
<!-- 새 메시지가 추가됨 -->
5. 키보드 접근성
포커스 관리의 기본
모든 대화형 요소는 키보드로 접근 가능해야 합니다.
| 키 | 동작 |
|---|---|
| Tab | 다음 포커스 가능한 요소로 이동 |
| Shift + Tab | 이전 포커스 가능한 요소로 이동 |
| Enter | 링크 활성화, 버튼 클릭 |
| Space | 버튼 클릭, 체크박스 토글 |
| Escape | 모달/팝업 닫기 |
| 화살표 키 | 메뉴, 탭, 라디오 그룹 내 이동 |
Skip Link (건너뛰기 링크)
<!-- 페이지 최상단에 배치 -->
본문으로 건너뛰기
<!-- 긴 내비게이션 메뉴 -->
<!-- 메인 콘텐츠 -->
.skip-link {
position: absolute;
left: -9999px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: fixed;
top: 10px;
left: 10px;
width: auto;
height: auto;
padding: 12px 24px;
background: #000;
color: #fff;
z-index: 9999;
font-size: 1rem;
}
Focus Trap (포커스 트랩)
모달이 열렸을 때 포커스가 모달 안에서만 순환해야 합니다.
function useFocusTrap(containerRef: React.RefObject<HTMLElement>) {
useEffect(() => {
const container = containerRef.current
if (!container) return
const focusableSelector = [
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
].join(', ')
const focusableElements = container.querySelectorAll(focusableSelector)
const firstElement = focusableElements[0] as HTMLElement
const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault()
lastElement.focus()
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault()
firstElement.focus()
}
}
}
container.addEventListener('keydown', handleKeyDown)
firstElement?.focus()
return () => container.removeEventListener('keydown', handleKeyDown)
}, [containerRef])
}
Tab 순서 관리
<!-- tabindex 값 -->
<!-- tabindex="0": 자연스러운 순서에 포함 -->
<!-- tabindex="-1": 프로그래밍 방식으로만 포커스 가능 -->
<!-- tabindex 양수 사용 금지! 순서가 꼬입니다 -->
<!-- 나쁨: tabindex="1", tabindex="2", tabindex="3" -->
6. 색상과 대비
대비율 요구사항
| 텍스트 종류 | AA 레벨 | AAA 레벨 |
|---|---|---|
| 일반 텍스트 (14px 미만) | 4.5:1 | 7:1 |
| 큰 텍스트 (18px 이상 또는 14px 볼드) | 3:1 | 4.5:1 |
| UI 컴포넌트, 그래픽 | 3:1 | - |
CSS로 대비 확보
/* 좋은 대비: #333 on #fff = 12.63:1 */
body {
color: #333333;
background-color: #ffffff;
}
/* 링크: 주변 텍스트와 3:1 대비 + 밑줄 또는 다른 시각적 단서 */
a {
color: #0066cc; /* #333과 3:1 이상 대비 */
text-decoration: underline;
}
/* 포커스 인디케이터: 3:1 대비 필수 */
:focus-visible {
outline: 3px solid #1a73e8;
outline-offset: 2px;
}
/* 다크 모드에서도 대비 유지 */
@media (prefers-color-scheme: dark) {
body {
color: #e0e0e0; /* #121212와 13.28:1 */
background-color: #121212;
}
a {
color: #8ab4f8; /* #e0e0e0와 3:1 이상 */
}
}
색맹 대응
/* 색상만으로 정보를 전달하지 않기 */
/* 나쁨: 색상만으로 에러 표시 */
.error-field {
border-color: red;
}
/* 좋음: 색상 + 아이콘 + 텍스트 */
.error-field {
border-color: #d32f2f;
border-width: 2px;
}
.error-field::before {
content: "⚠ ";
}
.error-message {
color: #d32f2f;
font-weight: bold;
}
대비 확인 도구
- **Chrome DevTools**: 요소 검사 시 대비 비율 표시
- **axe DevTools**: 전체 페이지 대비 검사
- **Colour Contrast Analyser (CCA)**: 독립 실행형 도구
- **Stark**: Figma/Sketch 플러그인
7. 이미지와 미디어
alt 텍스트 가이드
<!-- 정보성 이미지: 내용을 설명 -->
<!-- 장식성 이미지: 빈 alt -->
<!-- 기능성 이미지 (링크/버튼): 동작을 설명 -->
<!-- 복잡한 이미지: 긴 설명 제공 -->
전 세계 10억 명이 장애를 가지고 있으며,
웹사이트의 97%가 접근성 오류를 포함합니다.
가장 흔한 오류는 낮은 색상 대비(83%)입니다.
<!-- SVG 접근성 -->
비디오 접근성
<!-- 자막 (캡션) -->
<!-- 음성 해설 -->
오디오 콘텐츠
모든 오디오 콘텐츠에는 텍스트 대안(트랜스크립트)이 필요합니다.
8. 폼 접근성
레이블과 입력 연결
<!-- 방법 1: for/id 연결 (권장) -->
<!-- 방법 2: label로 감싸기 -->
이메일 주소
<!-- 방법 3: aria-labelledby -->
에러 메시지와 유효성 검사
type="password"
id="password"
aria-describedby="pw-requirements pw-error"
aria-invalid="true"
autocomplete="new-password"
/>
8자 이상, 대소문자, 숫자, 특수문자 포함
비밀번호가 요구사항을 충족하지 않습니다.
필수 필드
<!-- aria-required + 시각적 표시 -->
이름 <span aria-hidden="true" class="required">*</span>
type="text"
id="name"
required
aria-required="true"
autocomplete="name"
/>
자동 완성 (autocomplete)
<!-- WCAG 1.3.5: autocomplete 속성 사용 -->
9. React/Next.js 접근성 패턴
SPA에서의 포커스 관리
SPA(Single Page Application)에서는 페이지 전환 시 포커스가 자동으로 이동하지 않습니다.
// 라우트 변경 시 포커스 이동
function useRouteAnnounce() {
const pathname = usePathname()
const announceRef = useRef<HTMLDivElement>(null)
useEffect(() => {
// 메인 콘텐츠로 포커스 이동
const main = document.querySelector('main')
if (main) {
main.setAttribute('tabindex', '-1')
main.focus()
}
}, [pathname])
return (
ref={announceRef}
role="status"
aria-live="polite"
className="sr-only"
>
페이지가 로드되었습니다
)
}
라우트 변경 알림
// Next.js App Router: 라우트 변경 알림
'use client'
function RouteAnnouncer() {
const pathname = usePathname()
const [announcement, setAnnouncement] = useState('')
useEffect(() => {
const pageTitle = document.title
setAnnouncement(`${pageTitle} 페이지로 이동했습니다`)
}, [pathname])
return (
role="status"
aria-live="assertive"
aria-atomic="true"
className="sr-only"
>
{announcement}
)
}
접근성 있는 모달 (Dialog)
'use client'
interface DialogProps {
isOpen: boolean
onClose: () => void
title: string
children: React.ReactNode
}
function AccessibleDialog({ isOpen, onClose, title, children }: DialogProps) {
const dialogRef = useRef<HTMLDialogElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => {
const dialog = dialogRef.current
if (!dialog) return
if (isOpen) {
previousFocusRef.current = document.activeElement as HTMLElement
dialog.showModal()
} else {
dialog.close()
previousFocusRef.current?.focus()
}
}, [isOpen])
return (
ref={dialogRef}
aria-labelledby="dialog-title"
onClose={onClose}
>
{children}
)
}
Radix UI / Headless UI 활용
// Radix UI는 접근성을 자동으로 처리합니다
function MyDialog() {
return (
프로필 정보를 변경하세요.
{/* 폼 필드 */}
)
}
스크린 리더 전용 텍스트
/* sr-only 유틸리티 클래스 */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
// 사용 예시
10. 테스팅과 자동화
axe-core로 자동 테스트
// jest + axe-core
expect.extend(toHaveNoViolations)
describe('Button', () => {
it('접근성 위반 없음', async () => {
const { container } = render(<Button>클릭</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
Playwright + axe 통합 테스트
test('홈페이지 접근성', async ({ page }) => {
await page.goto('/')
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
// 키보드 내비게이션 테스트
test('키보드로 메뉴 탐색', async ({ page }) => {
await page.goto('/')
// Tab으로 Skip Link로 이동
await page.keyboard.press('Tab')
const skipLink = page.getByText('본문으로 건너뛰기')
await expect(skipLink).toBeFocused()
// Enter로 Skip Link 활성화
await page.keyboard.press('Enter')
const main = page.locator('main')
await expect(main).toBeFocused()
})
Lighthouse 접근성 점수
CI에서 Lighthouse 실행
npx lighthouse http://localhost:3000 \
--only-categories=accessibility \
--output=json \
--output-path=./lighthouse-report.json
// CI 파이프라인에서 접근성 점수 확인
const report = JSON.parse(fs.readFileSync('./lighthouse-report.json', 'utf-8'))
const accessibilityScore = report.categories.accessibility.score * 100
if (accessibilityScore < 90) {
console.error(`접근성 점수 ${accessibilityScore}점 - 90점 이상 필요`)
process.exit(1)
}
스크린 리더 수동 테스트 체크리스트
| 항목 | VoiceOver (Mac) | NVDA (Windows) | TalkBack (Android) |
|---|---|---|---|
| 페이지 제목 읽기 | Cmd + F5 | Insert + T | 자동 |
| 랜드마크 탐색 | Rotor (VO + U) | D/Shift+D | 스와이프 |
| 제목 탐색 | VO + Cmd + H | H/Shift+H | 스와이프 |
| 폼 필드 | VO + Tab | Tab | 터치 탐색 |
| 링크 목록 | Rotor | Insert + F7 | 메뉴 |
11. 법적 요구사항과 규정
미국: ADA와 Section 508
- **ADA Title III**: 웹사이트는 "공공 편의시설" 적용. WCAG 2.1 AA 요구
- **Section 508**: 연방 정부 웹사이트 의무. WCAG 2.0 AA 기준
- **소송 현황**: 2023년 웹 접근성 소송 4,600건 이상
EU: European Accessibility Act (EAA)
- **2025년 6월 28일** 시행
- 디지털 서비스 제공 기업 대상
- WCAG 2.1 AA 이상 요구
- 위반 시 벌금 부과
한국: 장애인차별금지법
- **장애인차별금지 및 권리구제 등에 관한 법률** (2008년)
- 웹 접근성 인증마크: 한국웹접근성인증평가원
- 공공기관 의무, 민간으로 확대 적용
- **한국형 웹 콘텐츠 접근성 지침 (KWCAG) 2.2**: WCAG 2.2 기반
접근성 성명서(Accessibility Statement)
<!-- 웹사이트에 접근성 성명서 포함 권장 -->
저희는 모든 사용자에게 접근 가능한 웹 경험을 제공하기 위해
WCAG 2.2 AA 기준을 준수하고 있습니다.
접근성 관련 문제를 발견하시면
연락해 주세요.
12. 면접 퀴즈
Perceivable(인지 가능): 모든 정보를 인지할 수 있어야 합니다. 이미지에 alt 텍스트 제공, 비디오에 자막 추가가 예시입니다. Operable(운용 가능): 모든 기능을 조작할 수 있어야 합니다. 키보드만으로 모든 기능 사용 가능, 충분한 시간 제공이 예시입니다. Understandable(이해 가능): 콘텐츠와 UI를 이해할 수 있어야 합니다. 명확한 에러 메시지, 일관된 내비게이션이 예시입니다. Robust(견고): 다양한 기술에서 작동해야 합니다. 유효한 HTML, 보조 기술 호환이 예시입니다.
ARIA의 첫 번째 규칙은 "네이티브 HTML 요소로 충분하다면 ARIA를 사용하지 마세요"입니다. 예를 들어 버튼에 `role="button"`을 추가하는 것은 불필요합니다. 네이티브 HTML 요소는 이미 접근성 의미, 키보드 동작, 포커스 관리가 내장되어 있기 때문입니다. ARIA를 잘못 사용하면 오히려 접근성을 해칠 수 있습니다.
4.5:1은 일반 크기 텍스트(18px 미만)에 대한 AA 레벨 요구사항입니다. 3:1은 큰 텍스트(18px 이상 또는 14px 볼드)와 UI 컴포넌트(버튼 테두리, 입력 필드 등)에 대한 AA 레벨 요구사항입니다. AAA 레벨은 일반 텍스트 7:1, 큰 텍스트 4.5:1을 요구합니다.
SPA에서 클라이언트 사이드 라우팅은 브라우저의 기본 페이지 로드와 달리 스크린 리더에 자동 알림을 제공하지 않습니다. 해결 방법으로는 라우트 변경 시 메인 콘텐츠로 포커스 이동, aria-live 리전으로 새 페이지 제목 알림, 문서 title 업데이트, Skip Link 제공 등이 있습니다. Next.js의 App Router는 내장 라우트 알림 기능을 제공합니다.
axe-core는 jest-axe(유닛 테스트)나 @axe-core/playwright(E2E 테스트)로 CI에 통합할 수 있습니다. WCAG 태그로 필터링하여 특정 기준만 검사하고, 위반이 있으면 빌드를 실패시킵니다. 한계로는 자동 도구가 접근성 문제의 약 30-40%만 감지할 수 있다는 점입니다. 키보드 사용성, 스크린 리더 호환성, 인지적 접근성 등은 수동 테스트가 필수입니다.
References
1. [WCAG 2.2 - W3C Recommendation](https://www.w3.org/TR/WCAG22/)
2. [WAI-ARIA 1.2 Specification](https://www.w3.org/TR/wai-aria-1.2/)
3. [MDN Web Accessibility Guide](https://developer.mozilla.org/en-US/docs/Web/Accessibility)
4. [A11y Project Checklist](https://www.a11yproject.com/checklist/)
5. [Deque axe-core](https://github.com/dequelabs/axe-core)
6. [WebAIM Million Report](https://webaim.org/projects/million/)
7. [Radix UI Accessibility](https://www.radix-ui.com/primitives/docs/overview/accessibility)
8. [React Accessibility Docs](https://react.dev/reference/react-dom/components#form-components)
9. [Next.js Accessibility](https://nextjs.org/docs/architecture/accessibility)
10. [Inclusive Components by Heydon Pickering](https://inclusive-components.design/)
11. [EU European Accessibility Act](https://ec.europa.eu/social/main.jsp?catId=1202)
12. [한국웹접근성인증평가원](https://www.wa.or.kr/)
13. [Chrome DevTools Accessibility](https://developer.chrome.com/docs/devtools/accessibility/reference/)
14. [Stark Accessibility Tools](https://www.getstark.co/)
현재 단락 (1/392)
전 세계 인구의 약 15%, 약 10억 명이 어떤 형태로든 장애를 가지고 있습니다. 웹 접근성은 단순히 "좋은 일"이 아니라 비즈니스 필수 사항입니다.