- Published on
웹 접근성(a11y) 완전 가이드 2025: WCAG 2.2, ARIA, 키보드 내비게이션 — 모두를 위한 웹
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 1. 왜 접근성인가
- 2. WCAG 2.2: 4가지 원칙
- 3. 시맨틱 HTML: 접근성의 기초
- 4. ARIA: 접근성 의미 추가
- 5. 키보드 접근성
- 6. 색상과 대비
- 7. 이미지와 미디어
- 8. 폼 접근성
- 9. React/Next.js 접근성 패턴
- 10. 테스팅과 자동화
- 11. 법적 요구사항과 규정
- 12. 면접 퀴즈
- References
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로 모든 것을 만들기 -->
<div class="button" onclick="submit()">제출</div>
<div class="header">사이트 제목</div>
<div class="nav">
<div class="link" onclick="goto('/')">홈</div>
</div>
<!-- 좋음: 시맨틱 요소 사용 -->
<button type="submit">제출</button>
<header><h1>사이트 제목</h1></header>
<nav>
<a href="/">홈</a>
</nav>
<button>은 자동으로 키보드 포커스를 받고, Enter/Space로 활성화되며, 스크린 리더가 "버튼"으로 인식합니다. <div>는 이 모든 것을 수동으로 구현해야 합니다.
랜드마크(Landmarks)
<body>
<header>
<nav aria-label="메인 내비게이션">...</nav>
</header>
<main>
<article>
<h1>기사 제목</h1>
<section aria-labelledby="section-1">
<h2 id="section-1">섹션 1</h2>
...
</section>
</article>
<aside aria-label="관련 링크">...</aside>
</main>
<footer>...</footer>
</body>
스크린 리더 사용자는 랜드마크 간에 빠르게 이동할 수 있습니다. VoiceOver에서 Rotor를 사용하면 header, nav, main, footer로 즉시 점프합니다.
제목 계층 구조
<!-- 나쁨: 제목 레벨 건너뛰기 -->
<h1>페이지 제목</h1>
<h3>하위 섹션</h3> <!-- h2를 건너뜀! -->
<h5>세부 항목</h5> <!-- h4를 건너뜀! -->
<!-- 좋음: 순차적 제목 구조 -->
<h1>페이지 제목</h1>
<h2>섹션 A</h2>
<h3>하위 섹션 A-1</h3>
<h3>하위 섹션 A-2</h3>
<h2>섹션 B</h2>
<h3>하위 섹션 B-1</h3>
스크린 리더 사용자의 67%가 제목으로 페이지를 탐색합니다. 제목 레벨을 건너뛰면 문서 구조를 파악하기 어렵습니다.
4. ARIA: 접근성 의미 추가
ARIA의 첫 번째 규칙
ARIA의 첫 번째 규칙: ARIA를 사용하지 마세요. 네이티브 HTML 요소로 충분하다면 ARIA가 필요 없습니다.
<!-- ARIA 불필요: 네이티브 요소로 충분 -->
<button>삭제</button> <!-- role="button" 불필요 -->
<input type="checkbox" /> <!-- role="checkbox" 불필요 -->
<nav> <!-- role="navigation" 불필요 -->
<!-- ARIA 필요: 네이티브 요소가 없는 경우 -->
<div role="tablist">
<button role="tab" aria-selected="true">탭 1</button>
<button role="tab" aria-selected="false">탭 2</button>
</div>
<div role="tabpanel">탭 1 내용</div>
핵심 ARIA 속성
<!-- aria-label: 시각적 텍스트가 없는 요소에 레이블 제공 -->
<button aria-label="메뉴 닫기">
<svg><!-- X 아이콘 --></svg>
</button>
<!-- aria-labelledby: 다른 요소의 텍스트를 레이블로 참조 -->
<h2 id="cart-heading">장바구니</h2>
<ul aria-labelledby="cart-heading">
<li>상품 1</li>
<li>상품 2</li>
</ul>
<!-- aria-describedby: 추가 설명 연결 -->
<input
type="password"
aria-describedby="pw-hint"
/>
<p id="pw-hint">8자 이상, 특수문자 포함</p>
<!-- aria-live: 동적 콘텐츠 변경 알림 -->
<div aria-live="polite">
장바구니에 3개 상품이 있습니다.
</div>
<!-- aria-expanded: 확장/축소 상태 -->
<button aria-expanded="false" aria-controls="menu">
메뉴
</button>
<ul id="menu" hidden>...</ul>
<!-- aria-hidden: 스크린 리더에서 숨기기 -->
<span aria-hidden="true">🔥</span>
<span class="sr-only">인기</span>
Live Regions
<!-- aria-live="polite": 현재 읽기를 마친 후 알림 -->
<div aria-live="polite" aria-atomic="true">
검색 결과: 42건
</div>
<!-- aria-live="assertive": 즉시 알림 (에러 등) -->
<div role="alert" aria-live="assertive">
세션이 만료되었습니다. 다시 로그인해 주세요.
</div>
<!-- role="status": 상태 메시지 (polite와 유사) -->
<div role="status">
파일 업로드 완료
</div>
<!-- role="log": 채팅 메시지 등 -->
<div role="log" aria-live="polite">
<!-- 새 메시지가 추가됨 -->
</div>
5. 키보드 접근성
포커스 관리의 기본
모든 대화형 요소는 키보드로 접근 가능해야 합니다.
| 키 | 동작 |
|---|---|
| Tab | 다음 포커스 가능한 요소로 이동 |
| Shift + Tab | 이전 포커스 가능한 요소로 이동 |
| Enter | 링크 활성화, 버튼 클릭 |
| Space | 버튼 클릭, 체크박스 토글 |
| Escape | 모달/팝업 닫기 |
| 화살표 키 | 메뉴, 탭, 라디오 그룹 내 이동 |
Skip Link (건너뛰기 링크)
<!-- 페이지 최상단에 배치 -->
<a href="#main-content" class="skip-link">
본문으로 건너뛰기
</a>
<nav>
<!-- 긴 내비게이션 메뉴 -->
</nav>
<main id="main-content" tabindex="-1">
<!-- 메인 콘텐츠 -->
</main>
.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": 자연스러운 순서에 포함 -->
<div role="button" tabindex="0">커스텀 버튼</div>
<!-- tabindex="-1": 프로그래밍 방식으로만 포커스 가능 -->
<div id="error-message" tabindex="-1">에러 발생!</div>
<!-- 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 텍스트 가이드
<!-- 정보성 이미지: 내용을 설명 -->
<img src="chart.png" alt="2025년 매출 추이: 1분기 100만, 2분기 150만, 3분기 200만" />
<!-- 장식성 이미지: 빈 alt -->
<img src="decorative-line.png" alt="" />
<!-- 기능성 이미지 (링크/버튼): 동작을 설명 -->
<a href="/home">
<img src="logo.png" alt="홈으로 이동" />
</a>
<!-- 복잡한 이미지: 긴 설명 제공 -->
<figure>
<img src="infographic.png" alt="접근성 통계 인포그래픽" aria-describedby="info-desc" />
<figcaption id="info-desc">
전 세계 10억 명이 장애를 가지고 있으며,
웹사이트의 97%가 접근성 오류를 포함합니다.
가장 흔한 오류는 낮은 색상 대비(83%)입니다.
</figcaption>
</figure>
<!-- SVG 접근성 -->
<svg role="img" aria-labelledby="svg-title">
<title id="svg-title">다운로드 아이콘</title>
<path d="..." />
</svg>
비디오 접근성
<video controls>
<source src="tutorial.mp4" type="video/mp4" />
<!-- 자막 (캡션) -->
<track kind="captions" src="captions-ko.vtt" srclang="ko" label="한국어" default />
<track kind="captions" src="captions-en.vtt" srclang="en" label="English" />
<!-- 음성 해설 -->
<track kind="descriptions" src="descriptions-ko.vtt" srclang="ko" label="음성 해설" />
</video>
오디오 콘텐츠
모든 오디오 콘텐츠에는 텍스트 대안(트랜스크립트)이 필요합니다.
8. 폼 접근성
레이블과 입력 연결
<!-- 방법 1: for/id 연결 (권장) -->
<label for="email">이메일 주소</label>
<input type="email" id="email" name="email" autocomplete="email" />
<!-- 방법 2: label로 감싸기 -->
<label>
이메일 주소
<input type="email" name="email" autocomplete="email" />
</label>
<!-- 방법 3: aria-labelledby -->
<span id="email-label">이메일 주소</span>
<input type="email" aria-labelledby="email-label" autocomplete="email" />
에러 메시지와 유효성 검사
<div class="form-group">
<label for="password">비밀번호</label>
<input
type="password"
id="password"
aria-describedby="pw-requirements pw-error"
aria-invalid="true"
autocomplete="new-password"
/>
<p id="pw-requirements" class="hint">
8자 이상, 대소문자, 숫자, 특수문자 포함
</p>
<p id="pw-error" class="error" role="alert">
비밀번호가 요구사항을 충족하지 않습니다.
</p>
</div>
필수 필드
<!-- aria-required + 시각적 표시 -->
<label for="name">
이름 <span aria-hidden="true" class="required">*</span>
</label>
<input
type="text"
id="name"
required
aria-required="true"
autocomplete="name"
/>
<p class="form-note">* 표시는 필수 항목입니다</p>
자동 완성 (autocomplete)
<!-- WCAG 1.3.5: autocomplete 속성 사용 -->
<input type="text" autocomplete="given-name" /> <!-- 이름 -->
<input type="text" autocomplete="family-name" /> <!-- 성 -->
<input type="email" autocomplete="email" /> <!-- 이메일 -->
<input type="tel" autocomplete="tel" /> <!-- 전화번호 -->
<input type="text" autocomplete="street-address" /> <!-- 주소 -->
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 (
<div
ref={announceRef}
role="status"
aria-live="polite"
className="sr-only"
>
페이지가 로드되었습니다
</div>
)
}
라우트 변경 알림
// Next.js App Router: 라우트 변경 알림
'use client'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
function RouteAnnouncer() {
const pathname = usePathname()
const [announcement, setAnnouncement] = useState('')
useEffect(() => {
const pageTitle = document.title
setAnnouncement(`${pageTitle} 페이지로 이동했습니다`)
}, [pathname])
return (
<div
role="status"
aria-live="assertive"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>
)
}
접근성 있는 모달 (Dialog)
'use client'
import { useEffect, useRef } from 'react'
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 (
<dialog
ref={dialogRef}
aria-labelledby="dialog-title"
onClose={onClose}
>
<h2 id="dialog-title">{title}</h2>
{children}
<button onClick={onClose}>닫기</button>
</dialog>
)
}
Radix UI / Headless UI 활용
import * as Dialog from '@radix-ui/react-dialog'
// Radix UI는 접근성을 자동으로 처리합니다
function MyDialog() {
return (
<Dialog.Root>
<Dialog.Trigger asChild>
<button>프로필 편집</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="overlay" />
<Dialog.Content className="content">
<Dialog.Title>프로필 편집</Dialog.Title>
<Dialog.Description>
프로필 정보를 변경하세요.
</Dialog.Description>
{/* 폼 필드 */}
<Dialog.Close asChild>
<button aria-label="닫기">X</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
스크린 리더 전용 텍스트
/* 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;
}
// 사용 예시
<button>
<TrashIcon />
<span className="sr-only">항목 삭제</span>
</button>
<a href="/cart">
<CartIcon />
<span className="sr-only">장바구니 (3개 상품)</span>
</a>
10. 테스팅과 자동화
axe-core로 자동 테스트
// jest + axe-core
import { render } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend(toHaveNoViolations)
describe('Button', () => {
it('접근성 위반 없음', async () => {
const { container } = render(<Button>클릭</Button>)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
})
Playwright + axe 통합 테스트
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
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)
<!-- 웹사이트에 접근성 성명서 포함 권장 -->
<h1>접근성 성명서</h1>
<p>
저희는 모든 사용자에게 접근 가능한 웹 경험을 제공하기 위해
WCAG 2.2 AA 기준을 준수하고 있습니다.
</p>
<p>
접근성 관련 문제를 발견하시면
<a href="mailto:a11y@example.com">a11y@example.com</a>으로
연락해 주세요.
</p>
12. 면접 퀴즈
Q1. WCAG의 4가지 원칙(POUR)을 설명하고, 각각의 예시를 들어주세요.
Perceivable(인지 가능): 모든 정보를 인지할 수 있어야 합니다. 이미지에 alt 텍스트 제공, 비디오에 자막 추가가 예시입니다. Operable(운용 가능): 모든 기능을 조작할 수 있어야 합니다. 키보드만으로 모든 기능 사용 가능, 충분한 시간 제공이 예시입니다. Understandable(이해 가능): 콘텐츠와 UI를 이해할 수 있어야 합니다. 명확한 에러 메시지, 일관된 내비게이션이 예시입니다. Robust(견고): 다양한 기술에서 작동해야 합니다. 유효한 HTML, 보조 기술 호환이 예시입니다.
Q2. ARIA의 첫 번째 규칙은 무엇이며, 왜 중요한가요?
ARIA의 첫 번째 규칙은 "네이티브 HTML 요소로 충분하다면 ARIA를 사용하지 마세요"입니다. 예를 들어 버튼에 role="button"을 추가하는 것은 불필요합니다. 네이티브 HTML 요소는 이미 접근성 의미, 키보드 동작, 포커스 관리가 내장되어 있기 때문입니다. ARIA를 잘못 사용하면 오히려 접근성을 해칠 수 있습니다.
Q3. 색상 대비 비율 4.5:1과 3:1은 각각 어떤 상황에 적용되나요?
4.5:1은 일반 크기 텍스트(18px 미만)에 대한 AA 레벨 요구사항입니다. 3:1은 큰 텍스트(18px 이상 또는 14px 볼드)와 UI 컴포넌트(버튼 테두리, 입력 필드 등)에 대한 AA 레벨 요구사항입니다. AAA 레벨은 일반 텍스트 7:1, 큰 텍스트 4.5:1을 요구합니다.
Q4. SPA에서 라우트 변경 시 접근성을 어떻게 보장하나요?
SPA에서 클라이언트 사이드 라우팅은 브라우저의 기본 페이지 로드와 달리 스크린 리더에 자동 알림을 제공하지 않습니다. 해결 방법으로는 라우트 변경 시 메인 콘텐츠로 포커스 이동, aria-live 리전으로 새 페이지 제목 알림, 문서 title 업데이트, Skip Link 제공 등이 있습니다. Next.js의 App Router는 내장 라우트 알림 기능을 제공합니다.
Q5. axe-core를 CI/CD 파이프라인에 통합하는 방법과 한계는?
axe-core는 jest-axe(유닛 테스트)나 @axe-core/playwright(E2E 테스트)로 CI에 통합할 수 있습니다. WCAG 태그로 필터링하여 특정 기준만 검사하고, 위반이 있으면 빌드를 실패시킵니다. 한계로는 자동 도구가 접근성 문제의 약 30-40%만 감지할 수 있다는 점입니다. 키보드 사용성, 스크린 리더 호환성, 인지적 접근성 등은 수동 테스트가 필수입니다.
References
- WCAG 2.2 - W3C Recommendation
- WAI-ARIA 1.2 Specification
- MDN Web Accessibility Guide
- A11y Project Checklist
- Deque axe-core
- WebAIM Million Report
- Radix UI Accessibility
- React Accessibility Docs
- Next.js Accessibility
- Inclusive Components by Heydon Pickering
- EU European Accessibility Act
- 한국웹접근성인증평가원
- Chrome DevTools Accessibility
- Stark Accessibility Tools