WCAG 2.2 실전 가이드: 프론트엔드 개발자를 위한 웹 접근성 의무화 대응
Lighthouse 접근성 점수가 40점대로 찍혔을 때, 팀장에게 이런 말을 들어본 적 있을 겁니다. "접근성은 나중에 봐요, 지금은 기능이 우선이에요." 저도 비슷한 상황을 반복하다가, 어느 날 레거시 코드베이스에 접근성을 소급 적용하는 업무를 맡았습니다. 리팩터링 공수가 기능 개발의 세 배는 나왔어요. 설계 단계부터 녹이지 않으면 나중에 얼마나 큰 비용이 드는지 그때 뼈저리게 체감했습니다.
그런데 이제는 "나중에"가 통하지 않는 환경이 됐습니다. 유럽연합 EAA(European Accessibility Act)가 2025년 6월 28일 발효됐고, 국내에서는 디지털포용법이 2026년 1월 시행 예정입니다. 행정안전부는 전자정부 웹사이트의 KWCAG 2.2 충족 기한을 2026년으로 못 박았고, 미국에서는 2024년 한 해만 연방 접근성 소송이 4,605건 접수됐습니다. 더 이상 접근성을 QA 마지막 체크리스트로 미룰 수 없는 시대가 된 겁니다.
이 글에서는 WCAG 2.2 핵심 기준부터 포커스 트랩 구현, 색 대비 수치, @axe-core/playwright 자동화 셋업까지 프론트엔드 개발자가 실무에서 바로 적용할 수 있는 접근성 패턴을 정리합니다. 규제 뉴스는 이미 충분히 접하셨을 테니, 코드 레벨의 실전 내용에 집중하겠습니다.
핵심 개념
POUR 원칙과 준수 수준
웹 접근성의 국제 표준은 W3C가 제정한 **WCAG(Web Content Accessibility Guidelines)**입니다. 현재 최신 버전은 2023년 10월 발표된 WCAG 2.2이며, 국내에서는 이를 기반으로 한 KWCAG 2.2가 4개 원칙, 14개 지침, 33개 검사항목으로 적용됩니다.
WCAG의 뼈대는 POUR 원칙 4가지입니다.
| 원칙 | 설명 |
|---|---|
| Perceivable(인식 가능) | 정보와 UI 요소를 사용자가 인식할 수 있어야 합니다 (대체 텍스트, 자막 등) |
| Operable(운용 가능) | 모든 기능이 키보드 등 다양한 입력 수단으로 조작 가능해야 합니다 |
| Understandable(이해 가능) | 정보와 UI 동작 방식이 이해하기 쉬워야 합니다 |
| Robust(견고) | 스크린 리더 등 보조 기술을 포함한 다양한 사용자 에이전트가 콘텐츠를 해석할 수 있어야 합니다 |
**준수 수준(Conformance Level)**은 A(최소), AA(표준), AAA(고급) 세 단계입니다. 국내 장차법, 유럽 EAA, 미국 ADA 모두 Level AA 준수를 요구하므로 AA를 기준선으로 잡으면 됩니다.
WCAG 2.2에서 프론트엔드에 직접 영향을 미치는 신규 기준
WCAG 2.1에서 2.2로 올라오면서 9개 신규 성공 기준이 추가됐습니다. 이 중 코드 수정이 필요한 핵심 항목들입니다.
| 기준 | 수준 | 핵심 내용 |
|---|---|---|
| 2.4.11 Focus Not Obscured (Minimum) | AA | 포커스 받은 컴포넌트가 다른 콘텐츠에 완전히 가려지면 안 됩니다 (sticky header 주의) |
| 2.4.13 Focus Appearance | AA | 포커스 인디케이터가 2px 두께 이상, 명암비 3:1 이상이어야 합니다 |
| 2.5.7 Dragging Movements | AA | 드래그가 필요한 기능에 단일 포인터 대안을 제공해야 합니다 |
| 2.5.8 Target Size (Minimum) | AA | 터치 타겟이 24×24 CSS 픽셀 이상이어야 합니다 |
| 3.3.8 Accessible Authentication (Minimum) | AA | 인증 단계에서 인지 기능 테스트(CAPTCHA 퍼즐 등)를 강요하면 안 됩니다 |
| 3.3.7 Redundant Entry | A | 동일 세션에서 이미 입력한 정보를 재입력하지 않도록 해야 합니다 |
실전 적용
outline: none이 망가뜨린 키보드 탐색 복구하기 (2.4.13 Focus Appearance)
많은 프로젝트의 reset.css를 열면 이런 코드가 있습니다.
* {
outline: none; /* 브라우저 기본 포커스 스타일 제거 */
}디자인과 안 맞는다는 이유로 넣어둔 코드인데, 이 한 줄이 키보드 사용자의 탐색을 완전히 차단합니다. 포커스가 어디 있는지 전혀 보이지 않으니까요. :focus-visible을 활용하면 마우스 클릭 시에는 포커스 링이 보이지 않고, 키보드 탐색 시에만 표시되도록 할 수 있습니다. 디자인 팀도 납득하기 쉬운 방식이라 실무에서 설득하기도 수월했습니다.
/* reset.css의 outline: none 대신 이걸로 교체 */
:focus-visible {
outline: 3px solid #005FCC;
outline-offset: 2px;
border-radius: 2px;
}| 속성 | 이유 |
|---|---|
3px solid |
WCAG 2.4.13의 2px 두께 요건 충족 + 시인성 확보 |
outline-offset: 2px |
요소 테두리와 살짝 간격을 둬서 가독성 향상 |
:focus-visible |
마우스 클릭 시 포커스 링 없음, 키보드 포커스만 표시 |
포커스 인디케이터 색은 배경색과 3:1 이상 명암비를 맞춰야 WCAG 2.4.13을 통과합니다. #005FCC(파란색)와 흰색 배경의 명암비는 약 6.5:1로 기준을 충분히 넘습니다.
포커스 스타일을 잡았다면, 다음은 레이아웃 레벨 문제를 살펴볼 차례입니다.
Sticky Header 포커스 충돌 해결 (2.4.11 Focus Not Obscured)
sticky header가 있는 레이아웃에서 Tab 키를 눌러 이동하다 보면 포커스 요소가 헤더에 가려지는 상황이 자주 발생합니다. WCAG 2.4.11이 새로 추가된 것도 이 문제 때문입니다. scroll-margin-top으로 간단하게 해결할 수 있는데, 헤더 높이를 하드코딩하면 레이아웃이 바뀔 때 깨지니 CSS 변수로 관리하는 게 낫습니다.
:root {
--header-height: 64px;
}
:focus-visible {
scroll-margin-top: var(--header-height, 80px);
}헤더 높이를 --header-height 변수 한 곳에서 관리하면, 반응형으로 헤더 크기가 달라지는 경우에도 @media 안에서 변수값만 바꿔주면 됩니다.
터치 타겟 44px 확보 (2.5.8 Target Size)
아이콘 버튼처럼 시각적으로 작은 요소도 터치 영역은 충분히 확보해야 합니다. WCAG 2.2 기준은 24×24px이지만, iOS HIG는 44×44px을 권장하므로 44px로 맞추면 두 기준을 동시에 충족합니다.
.icon-btn {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}아이콘 자체의 크기는 20~24px로 유지하면서 클릭 영역만 넓히는 방식이라, 시각 디자인을 건드리지 않고 접근성 기준을 맞출 수 있습니다.
텍스트·비텍스트 색 대비 기준 맞추기 (1.4.3, 1.4.11)
포커스 인디케이터 명암비(3:1)만 챙기고 텍스트 명암비를 빠뜨리는 경우가 많습니다. 색 대비는 접근성 이슈의 상당 부분을 차지하므로 기준을 한 번에 정리해두면 좋습니다.
| 대상 | 최소 명암비 | 기준 |
|---|---|---|
| 일반 텍스트 (18pt 미만) | 4.5:1 | WCAG 1.4.3 |
| 큰 텍스트 (18pt 이상, 또는 굵은 14pt 이상) | 3:1 | WCAG 1.4.3 |
| UI 컴포넌트 경계·아이콘 | 3:1 | WCAG 1.4.11 |
| 포커스 인디케이터 | 3:1 | WCAG 2.4.13 |
디자인 시스템에 색상 토큰을 설정할 때 이 기준을 함께 기록해두면, 디자이너와 접근성 토큰을 논의할 때 공통 언어가 생깁니다. 저희 팀은 Figma 토큰에 명암비 값을 주석으로 달아 "이 색 조합은 AA 통과" 형태로 관리하게 됐는데, 그 뒤로 리뷰 단계에서 접근성 문제가 훨씬 줄었습니다. 색 대비 확인에는 WebAIM Contrast Checker나 Chrome DevTools의 색 선택기가 편리합니다.
색 대비가 정리됐다면, 동적 UI에서 가장 복잡한 케이스인 모달로 넘어갑니다.
모달 접근성 완성하기 — 포커스 트랩까지 (WAI-ARIA)
모달은 접근성에서 가장 까다로운 컴포넌트 중 하나입니다. role="dialog"와 aria-modal만 붙이면 된다고 생각하기 쉬운데, 이것만으로는 키보드 사용자가 Tab을 눌러 모달 뒤의 요소로 빠져나갈 수 있습니다. 포커스 트랩(focus trap) 구현이 반드시 필요합니다.
<!-- 모달 마크업 -->
<button id="open-modal-btn">구매하기</button>
<div
id="purchase-modal"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
hidden
>
<h2 id="dialog-title">구매 확인</h2>
<p id="dialog-desc">선택한 상품을 구매하시겠습니까?</p>
<button id="confirm-btn">확인</button>
<button id="cancel-btn">취소</button>
</div>const modal = document.getElementById('purchase-modal');
const trigger = document.getElementById('open-modal-btn');
const FOCUSABLE = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
function openModal() {
modal.removeAttribute('hidden');
// 모달이 열리면 첫 번째 포커스 가능한 요소로 이동
modal.querySelector(FOCUSABLE)?.focus();
document.addEventListener('keydown', handleKeyDown);
}
function closeModal() {
modal.setAttribute('hidden', '');
document.removeEventListener('keydown', handleKeyDown);
// 모달을 닫으면 트리거 버튼으로 포커스 복귀
trigger.focus();
}
function handleKeyDown(e) {
if (e.key === 'Escape') {
closeModal();
return;
}
if (e.key !== 'Tab') return;
// 포커스 트랩: 마지막 요소에서 Tab → 첫 번째로, 첫 번째에서 Shift+Tab → 마지막으로
const focusable = [...modal.querySelectorAll(FOCUSABLE)];
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
trigger.addEventListener('click', openModal);
document.getElementById('cancel-btn').addEventListener('click', closeModal);실무에서는 이 로직을 직접 구현하기보다 Radix UI나 React Aria 같은 헤드리스 라이브러리를 활용하는 편이 훨씬 낫습니다. 위 코드는 원리 파악용으로 보시고, 프로덕션에서는 검증된 라이브러리에 위임하는 걸 권장합니다.
**WAI-ARIA(Web Accessibility Initiative – Accessible Rich Internet Applications)**는 HTML 시맨틱만으로 표현하기 어려운 동적 콘텐츠의 역할·상태·속성을 보조 기술에 전달하기 위한 W3C 명세입니다.
role,aria-*속성으로 구성됩니다.
헤드리스 UI 라이브러리: 스타일 없이 WAI-ARIA 패턴과 키보드 인터랙션이 미리 구현된 컴포넌트 모음입니다. Radix UI, React Aria(Adobe), Headless UI(Tailwind Labs) 등이 대표적이며, 접근성 구현 비용을 크게 줄여줍니다.
이미지 alt 속성과 실시간 피드백 (aria-live)
alt 텍스트는 "이미지 설명"이 아니라 그 이미지가 전달하는 정보를 대체하는 텍스트입니다.
<!-- 정보 전달 이미지: 이미지가 담고 있는 데이터 자체를 설명 -->
<img src="chart.png" alt="2025년 1분기 매출: 전년 대비 23% 증가" />
<!-- 장식 이미지: alt=""만으로 스크린 리더가 무시함 -->
<img src="divider.png" alt="" />장식 이미지에서 alt=""만 있어도 스크린 리더는 이미 무시합니다. role="presentation"을 함께 붙이는 건 중복이라 필요 없습니다.
폼 제출이나 저장 완료처럼 동적으로 바뀌는 상태 메시지는 aria-live로 스크린 리더에 즉시 알릴 수 있습니다.
<div aria-live="polite" aria-atomic="true" id="status-message">
<!-- JavaScript로 메시지 삽입 시 스크린 리더가 자동으로 읽어줌 -->
</div>function showStatus(message) {
document.getElementById('status-message').textContent = message;
}
// 폼 제출 성공 시
showStatus('저장이 완료되었습니다.');@axe-core/playwright로 CI에 접근성 자동화하기
솔직히 수동 테스트만으로는 모든 이슈를 잡기 어렵습니다. E2E 테스트에 @axe-core/playwright를 붙이면 WCAG 기준 위반을 자동으로 걸러낼 수 있습니다.
패키지명에 주의가 필요합니다. axe-playwright는 서드파티 래퍼이고, 공식 패키지는 @axe-core/playwright입니다. 그냥 axe-playwright를 설치하면 다른 패키지가 설치됩니다.
import AxeBuilder from '@axe-core/playwright';
import { test, expect } from '@playwright/test';
test('홈페이지 접근성 검사', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
expect(results.violations).toEqual([]);
});| 도구 | 사용 환경 |
|---|---|
| jest-axe | Jest 단위 테스트 |
| @axe-core/playwright | Playwright E2E |
| @axe-core/react | React 개발 환경 런타임 경고 |
| Cypress-axe | Cypress E2E |
다만, 자동화 도구는 전체 접근성 이슈의 약 40%만 감지한다는 점은 알아두는 게 좋습니다. 나머지 60%는 NVDA(Windows), VoiceOver(macOS/iOS) 같은 스크린 리더로 직접 확인해야 합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 법적 리스크 감소 | 국내외 소송·행정제재·과태료 위험을 사전에 차단할 수 있습니다 |
| SEO 개선 | 시맨틱 마크업, 대체 텍스트, 구조화된 헤딩은 검색 엔진 크롤링에 직접 기여합니다 |
| 사용성 향상 | 키보드 탐색, 명확한 레이블, 충분한 색 대비는 비장애인 사용자 경험도 함께 개선합니다 |
| 시장 확대 | 국내 장애인 약 265만 명, 고령자 포함 시 잠재 사용자가 크게 늘어납니다 |
| 글로벌 진출 용이 | EAA, ADA 등 글로벌 규제 준수로 해외 서비스 출시 장벽이 낮아집니다 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 레거시 코드 부채 | 오래된 코드베이스 소급 적용 시 상당한 리팩터링 공수가 발생합니다 | 새 컴포넌트부터 적용하고 점진적으로 확산하는 방식을 권장합니다 |
| 자동화 한계 | 도구는 전체 이슈의 약 40%만 감지하며 나머지는 수동 테스트가 필요합니다 | NVDA, VoiceOver 등 스크린 리더 직접 테스트를 병행하는 것이 좋습니다 |
| 디자인 충돌 | 포커스 인디케이터, 색 대비 요구사항이 브랜드 디자인 가이드와 충돌할 수 있습니다 | 디자인 시스템 단계에서 접근성 기준을 디자인 토큰에 반영해두는 것이 효과적입니다 |
| 동적 콘텐츠 복잡성 | SPA, 무한 스크롤, 모달 등 동적 UI는 ARIA 상태 관리가 복잡합니다 | Radix UI, React Aria 같은 접근성 내장 헤드리스 라이브러리 활용을 권장합니다 |
| 팀 내 인식 격차 | 접근성을 QA 마지막 단계로 미루는 관행이 여전히 많습니다 | 스프린트 Definition of Done에 접근성 기준을 포함해두면 도움이 됩니다 |
실무에서 가장 흔한 실수
outline: none전역 적용 — reset.css에서 포커스 스타일을 모두 제거하면 키보드 사용자의 탐색이 완전히 차단됩니다.:focus-visible기반 스타일로 교체하는 것이 좋습니다.tabindex양수 값 오남용 —tabindex="2",tabindex="5"같은 양수 값은 DOM 순서와 시각 순서가 달라져 Tab 이동이 예측 불가능해집니다.tabindex="0"또는-1만 사용하는 것을 권장합니다.- 색상만으로 오류 상태 표시 — 빨간 테두리만으로 에러를 나타내면 색맹 사용자가 인식하지 못합니다. 아이콘과 텍스트 메시지를 함께 제공하는 것이 필요합니다.
- 장식 이미지에
role="presentation"중복 선언 —alt=""만으로 스크린 리더가 이미 무시합니다.role="presentation"을 함께 붙이는 건 불필요합니다. - 모달에서 포커스 트랩 누락 —
role="dialog"선언만으로는 키보드 사용자가 Tab으로 모달 뒤 요소에 접근할 수 있습니다. 포커스 트랩 구현이 반드시 필요합니다.
마치며
접근성을 나중으로 미룰수록 비용은 기하급수적으로 늘어납니다. 설계 단계에서 30분 쓰는 것과 레거시 코드베이스를 소급 적용하는 것의 차이를 저는 직접 경험으로 배웠습니다. 팀장 설득이 어렵다면, "2026년 이후 소송 리스크"보다 "Lighthouse 접근성 점수를 올리면 SEO에도 직접 영향이 있다"는 쪽이 훨씬 설득력 있었습니다. 법적 의무라는 프레이밍보다 제품 품질과 비즈니스 지표로 연결하는 게 조직을 움직이는 데 더 효과적이었어요.
지금 바로 시작할 수 있는 3단계:
- 현재 점수 파악하기 — Chrome DevTools에서 Lighthouse를 열고 Accessibility 항목 점수를 확인해보는 것이 좋습니다. WAVE 브라우저 확장 프로그램도 문제 위치를 시각적으로 표시해줘서 편리합니다.
- @axe-core/playwright CI에 붙이기 — 기존 Playwright 테스트 환경에 추가하면 됩니다. 신규 코드가 접근성 기준을 깨는 순간 CI에서 바로 감지됩니다.
- 포커스 스타일 복구 + 새 컴포넌트에 Radix UI 도입하기 —
reset.css의outline: none을:focus-visible기반으로 교체하고, 새로 만드는 모달·드롭다운은 Radix UI로 구현해보면 접근성 구현 부담이 눈에 띄게 줄어드는 걸 느낄 수 있습니다.
다음 글: WAI-ARIA 패턴 심화 — 탭, 드롭다운, 무한 스크롤, 라이브 리전까지 동적 UI를 스크린 리더 친화적으로 구현하는 실전 패턴
참고 자료
- 한국형 웹 콘텐츠 접근성 지침(KWCAG) 2.2
- 웹 콘텐츠 접근성 지침 2.2 한국어 번역본
- WCAG 2.2 공식 문서 — W3C
- WCAG 2.2 신규 기준 — W3C WAI
- 장애인차별금지법 — 국가법령정보센터
- 키오스크·모바일앱 접근성 편의 제공 — 보건복지부
- ADA 웹 접근성 규칙 팩트시트 2024 — ADA.gov
- European Accessibility Act 준수 가이드 — AllAccessible
- WAI-ARIA 작성 방법 1.2 한국어
- ARIA — MDN Web Docs 한국어
- 개발자가 직접 접근성 테스트하기 — 네이버 NULI
- Playwright + axe-core 접근성 테스트 — DEV Community
- Headless UI 라이브러리 비교 — LogRocket