Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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억 명이 어떤 형태로든 장애를 가지고 있습니다. 웹 접근성은 단순히 "좋은 일"이 아니라 비즈니스 필수 사항입니다.

작성 글자: 0원문 글자: 11,279작성 단락: 0/392