프론트엔드 개발자를 위한 EAA 웹 접근성 실무 가이드 | WCAG 2.2 + 자동화 테스트
2025년 6월 28일, EU에서 꽤 묵직한 법이 발효됐습니다. European Accessibility Act(EAA) — "장애인도 디지털 서비스를 동등하게 쓸 수 있게 해라"는 법인데, 웹사이트·모바일 앱·인터넷 뱅킹 등 거의 모든 디지털 서비스가 대상입니다. 과태료는 국가에 따라 최대 €1,260,000에 달하고, 2025년 11월에는 프랑스 DGCCRF가 Auchan·Carrefour·E.Leclerc·Picard를 상대로 실제로 소송을 제기했습니다(출처: Pivotal Accessibility). "EU 서비스가 아니라서 관계없다"고 생각하셨다면, EU 회원국에 서비스를 제공하는 순간 한국 기업도 적용 대상이 됩니다.
솔직히 저도 처음엔 "접근성이 또 규정 얘기구나" 싶었는데, 실제로 코드에 적용해 보면 UX 품질 전반이 올라가는 걸 체감했습니다. 이 글을 읽고 나면 EAA가 요구하는 WCAG 2.1/2.2 핵심 기준을 코드로 적용하고, 자동화 테스트까지 팀 워크플로에 녹여낼 수 있습니다. 코드 예시는 HTML을 기본으로, React를 쓰는 분들을 위해 JSX 버전도 함께 담았습니다.
핵심 개념
EAA가 요구하는 기술 표준: EN 301 549와 WCAG
EAA는 기술 준수 기준으로 EN 301 549 유럽 표준을 사용하며, 이 표준의 핵심이 바로 WCAG 2.1 Level AA입니다. WCAG는 네 가지 원칙(POUR)으로 구성됩니다.
| 원칙 | 의미 | 대표 요건 |
|---|---|---|
| Perceivable (인식 가능) | 모든 콘텐츠를 감각적으로 인지 가능 | 이미지 alt 텍스트, 색상 대비 |
| Operable (운용 가능) | 키보드만으로 모든 기능 사용 가능 | 키보드 내비게이션, 포커스 가시성 |
| Understandable (이해 가능) | 언어·오류 메시지가 명확 | 폼 레이블, 오류 안내 |
| Robust (견고) | 다양한 보조 기술과 호환 | 시맨틱 HTML, ARIA 속성 |
이 표가 처음엔 너무 추상적으로 느껴질 수 있는데, 뒤에 나오는 코드 예시들을 보다 보면 각 원칙이 실제로 어떤 코드 패턴에 매핑되는지 자연스럽게 이해가 됩니다.
WCAG 2.2에서 새로 추가된 것들
2023년 10월 W3C 공식 표준이 된 WCAG 2.2는 기존 2.1에 9개 성공 기준을 추가했습니다. EU는 향후 EN 301 549를 2.2 기반으로 업데이트할 예정이라, 지금부터 준비해 두면 나중에 재작업을 피할 수 있습니다.
| 추가 항목 | 요건 |
|---|---|
| Focus Appearance | 포커스 링이 2px 이상, 배경과 3:1 이상 대비 |
| Target Size | 터치/클릭 대상 최소 24×24 CSS 픽셀 (AA 기준) |
| Accessible Authentication | 로그인 시 인지 퍼즐 없는 인증 수단 제공 |
Target Size에서 한 가지 주의할 점이 있습니다. AA 기준은 24×24px이지만, AAA 권장은 44×44px이고 실무에서도 44px 이상을 권고하는 경우가 많습니다. "24px만 맞추면 된다"고 생각하면 실제 모바일 UX에서 불편함이 생길 수 있으니 여유 있게 잡으시면 좋습니다.
Accessible Authentication도 단순히 "CAPTCHA를 없애라"는 의미가 아닙니다. copy-paste 허용, WebAuthn(패스키), 매직 링크, SMS 인증 코드처럼 인지 부하가 낮은 구체적인 대안 중 하나를 제공하면 됩니다.
용어 정리 —
WAI-ARIA(Web Accessibility Initiative – Accessible Rich Internet Applications): 스크린 리더 등 보조 기술이 동적 UI를 이해할 수 있도록 HTML에 역할·상태·속성을 추가하는 W3C 명세입니다.role,aria-label,aria-describedby같은 속성이 여기에 속합니다.
실전 적용
예시 1: 시맨틱 HTML과 키보드 내비게이션
실무에서 자주 맞닥뜨리는 상황인데, div에 클릭 이벤트를 붙이는 패턴이 생각보다 많습니다. 스크린 리더는 이 요소를 버튼으로 인식하지 못하고, 키보드로도 포커스가 안 됩니다.
<!-- 나쁜 예: 키보드·스크린 리더 모두 접근 불가 -->
<div onclick="submitForm()" style="cursor:pointer">제출</div>
<!-- 좋은 예: 시맨틱 버튼 사용 -->
<button type="submit">제출</button>
<!-- 아이콘만 있는 버튼처럼 텍스트가 불명확할 때 aria-label 활용 -->
<button type="button" aria-label="검색창 열기">
<svg aria-hidden="true">...</svg>
</button>| 포인트 | 설명 |
|---|---|
<button> 사용 |
기본적으로 키보드 포커스, Enter/Space 동작 내장 |
aria-label |
버튼 텍스트가 불명확할 때(아이콘 버튼 등) 쓰는 것. 텍스트가 있으면 불필요 |
type="submit" |
폼 내 의도를 명확히 전달 |
aria-label을 버튼 텍스트와 동일하게 쓰는 경우가 종종 있는데, 그러면 스크린 리더가 같은 텍스트를 두 번 읽어서 오히려 어색해집니다. aria-label은 가시적 텍스트로 의미를 충분히 전달할 수 없을 때 보완재로 쓰시면 됩니다.
예시 2: 폼 레이블과 오류 안내
placeholder만 쓰고 label을 생략하는 패턴도 흔한데, 포커스가 들어오는 순간 placeholder가 사라져서 사용자가 뭘 입력해야 하는지 잊어버리게 됩니다. 폼 접근성은 제가 팀 코드 리뷰를 처음 맡았을 때 가장 많이 지적받은 부분이기도 했습니다.
HTML 기본 예시:
<!-- 나쁜 예: label 없음 -->
<input type="email" placeholder="이메일 입력" />
<!-- 좋은 예: label 연결 + 힌트 + 오류 메시지 -->
<label for="email">이메일 주소</label>
<input
id="email"
type="email"
aria-describedby="email-hint email-error"
aria-required="true"
aria-invalid="true"
/>
<span id="email-hint">예: user@example.com</span>
<span id="email-error" role="alert">
올바른 이메일 형식을 입력해 주세요.
</span>React JSX 버전:
const [hasError, setHasError] = useState(false);
return (
<>
<label htmlFor="email">이메일 주소</label>
<input
id="email"
type="email"
aria-describedby="email-hint email-error"
aria-required="true"
aria-invalid={hasError}
onChange={(e) => setHasError(!e.target.value.includes('@'))}
/>
<span id="email-hint">예: user@example.com</span>
{hasError && (
<span id="email-error" role="alert">
올바른 이메일 형식을 입력해 주세요.
</span>
)}
</>
);| 속성 | 역할 |
|---|---|
for / htmlFor + id 연결 |
레이블 클릭 시 인풋 포커스, 스크린 리더 연결 |
aria-describedby |
힌트·오류 메시지를 보조 기술에 연결 |
aria-invalid |
오류 상태를 스크린 리더에 전달 |
role="alert" |
동적으로 추가된 오류 메시지를 즉시 읽어줌 |
예시 3: 포커스 가시성 (WCAG 2.2 Focus Appearance)
많은 팀에서 디자인 때문에 outline: none을 전역으로 때려버리는데, 저도 초기에 그랬다가 키보드 사용자 테스트에서 크게 혼났습니다. WCAG 2.2는 포커스 링의 크기와 대비율까지 명시하고 있습니다.
/* ❌ 절대 피하고 싶은 패턴 */
* { outline: none; }
/* ✅ WCAG 2.2 기준: 2px 이상, 배경과 3:1 이상 대비 */
/* #005fcc는 흰 배경 기준 대비율 약 5.9:1 — AA 통과 */
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
border-radius: 2px;
}
/* 마우스 클릭 시에는 포커스 링을 숨길 수 있음 */
:focus:not(:focus-visible) {
outline: none;
}팁 —
:focus-visible은 키보드 포커스에만 스타일을 적용하고, 마우스 클릭에는 적용하지 않습니다.outline: none전역 제거 없이도 디자인과 접근성을 함께 챙길 수 있는 현대적인 방법입니다.
예시 4: 이미지 대체 텍스트와 Skip Navigation
<!-- 정보를 전달하는 이미지: 맥락 있는 alt 필수 -->
<img src="chart.png" alt="2025년 매출 전년 대비 35% 증가 막대 그래프" />
<!-- 장식용 이미지: alt=""로 스크린 리더 무시 처리 -->
<!-- alt=""만으로도 장식 이미지 처리가 되며, role="presentation"은 선택 사항 -->
<img src="divider.png" alt="" role="presentation" />
<!-- Skip Navigation: 페이지 최상단에 위치 -->
<a href="#main-content" class="skip-link">본문으로 바로 이동</a>
<main id="main-content">...</main>.skip-link {
position: absolute;
transform: translateY(-100%);
transition: transform 0.2s;
}
.skip-link:focus {
transform: translateY(0);
}Skip Navigation은 키보드 사용자가 반복 메뉴를 매번 탭으로 넘기지 않아도 되게 해주는 작은 배려입니다. 구현은 10분이면 되는데 효과는 큽니다.
alt=""와 role="presentation" 차이가 헷갈릴 수 있는데, alt=""만으로 스크린 리더가 해당 이미지를 무시합니다. role="presentation"은 요소의 시맨틱 역할 자체를 제거하는 용도로, 장식 이미지에는 alt=""면 충분합니다.
예시 5: 색상 대비 기준
색상 대비는 처음 제대로 확인했을 때 충격적이었습니다. 흔하게 쓰던 #777777 회색 텍스트가 흰 배경에서 4.5:1을 못 넘긴다는 걸 그때 처음 알았거든요. #595959 정도면 AA 기준을 통과합니다.
| 텍스트 유형 | 최소 대비율 (WCAG AA) |
|---|---|
| 일반 텍스트 (18pt 미만) | 4.5:1 |
| 큰 텍스트 (18pt 이상 / Bold 14pt 이상) | 3:1 |
| UI 컴포넌트·그래픽 경계 | 3:1 |
디자인 토큰 레벨에서 대비율을 미리 검증해 두면 나중에 색상 관련 수정 공수가 크게 줄어듭니다. CSS custom property로 관리하는 예시입니다.
:root {
/* 흰 배경(#fff) 기준 대비율 주석으로 명시 */
--color-text-primary: #1a1a1a; /* 대비율 ~16.7:1 */
--color-text-secondary: #595959; /* 대비율 ~7.0:1 */
--color-text-muted: #767676; /* 대비율 ~4.5:1 — AA 경계 */
--color-text-disabled: #a3a3a3; /* 대비율 ~2.3:1 — 비활성 전용 */
}
body {
color: var(--color-text-primary);
}
.hint-text {
color: var(--color-text-secondary);
}대비율 측정은 Colour Contrast Analyser를 쓰시면 바로 확인할 수 있습니다.
자동화 테스트 세팅
제목에서 약속한 부분인데, 자동화 없이는 접근성 관리가 지속 가능하지 않습니다. 수동으로 모든 컴포넌트를 매번 점검할 수는 없으니까요.
jest-axe: 컴포넌트 단위 자동화
pnpm add -D axe-core jest-axe @types/jest-axe// EmailInput.test.tsx
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { EmailInput } from './EmailInput';
expect.extend(toHaveNoViolations);
it('접근성 위반이 없어야 합니다', async () => {
const { container } = render(<EmailInput />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});이걸 CI에 묶어두면 PR마다 자동으로 접근성 회귀를 잡아줍니다. 완벽하진 않지만(자동화 도구는 전체 이슈의 40~57% 정도만 감지), 명백한 실수가 배포되는 건 막을 수 있습니다.
axe DevTools + Lighthouse: 빠른 현황 파악
새 프로젝트를 맡거나 기존 서비스를 점검할 때는 브라우저 확장부터 시작하는 게 빠릅니다.
| 도구 | 용도 |
|---|---|
| axe DevTools (Chrome/Firefox 확장) | 페이지 전체 WCAG 위반 목록, 요소 하이라이트 |
| Lighthouse (Chrome DevTools 내장) | 접근성 점수 + 항목별 설명 |
| WAVE (WebAIM) | 시각적 접근성 리포트 |
axe-core — Deque Systems가 만든 오픈소스 접근성 감지 엔진으로, 업계 표준 도구입니다. jest-axe, cypress-axe, axe DevTools 등 다양한 형태로 통합할 수 있습니다.
스크린 리더 수동 테스트
자동화 도구가 잡지 못하는 흐름 문제는 직접 스크린 리더로 탐색해보는 수밖에 없습니다. Lighthouse 100점이어도 스크린 리더로 실제 써보면 어색한 구간이 생각보다 많습니다.
| 도구 | 플랫폼 | 시작 방법 |
|---|---|---|
| VoiceOver | macOS / iOS (내장) | Cmd + F5 |
| NVDA | Windows (무료) | nvaccess.org |
| TalkBack | Android (내장) | 설정 > 접근성 |
처음엔 스크린 리더 조작 자체가 낯설어서 5분도 못 버티고 끄게 됩니다. 그냥 Tab 키로만 페이지를 돌아다니면서 포커스 순서가 자연스러운지 확인하는 것부터 시작하시면 됩니다.
주의해야 할 함정과 한계
장점
| 항목 | 내용 |
|---|---|
| 법적 리스크 감소 | 미준수 시 최대 €1,260,000 과태료 및 시장 퇴출 위험 방어 |
| 사용자 범위 확대 | EU 인구 약 26%인 장애인 포함, 전체 UX 품질 향상 |
| SEO 시너지 | 시맨틱 HTML·alt 텍스트·명확한 구조는 검색 엔진 크롤링 효율에도 기여 |
| 모바일 품질 향상 | 터치 타겟 크기·포커스 개선은 비장애인에게도 사용성 향상 |
디자이너와 접근성 요건을 조기에 맞추는 게 중요한 이유가 있는데, 색상 대비를 나중에 수정하면 디자인 시스템 전체를 뒤집어야 하는 경우가 생깁니다. 저도 한 번 그걸 경험하고 나서는 토큰 레벨에서 대비율 주석을 꼭 달게 됐습니다.
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 자동화 한계 | 도구는 이슈의 40~57%만 감지 | 자동화 + 수동 스크린 리더 테스트 병행 |
| 레거시 코드 부채 | 기존 컴포넌트 라이브러리 재검토 필요 | Radix UI·React Aria 등 접근성 내장 라이브러리로 단계적 전환 |
| 디자인 충돌 | 대비 강화·포커스 링이 기존 디자인 시스템과 충돌 | 디자이너와 조기 협의, 디자인 토큰 레벨에서 대비율 반영 |
| 국가별 법률 차이 | 과태료 기준·집행 기관이 나라마다 다름 | 진출 국가별 현지 법무 확인 |
| 복잡한 인터랙션 | 캐러셀·모달·드래그앤드롭의 접근성 구현은 추가 공수 | WAI-ARIA Authoring Practices Guide 패턴 참조 |
실무에서 가장 흔한 실수
outline: none전역 적용 — 디자인 팀 요청으로 무심코 넣었다가 키보드 사용자의 포커스를 완전히 없애버리는 케이스.:focus-visible로 대체할 수 있습니다.aria-label을 무분별하게 남발 — ARIA 속성은 시맨틱 HTML로 해결이 안 될 때 보완재입니다.<label>로 해결될 걸aria-label로 때우면 스크린 리더가 텍스트를 중복 읽거나, 유지보수가 복잡해집니다.- 자동화 테스트만 믿고 수동 테스트 생략 — Lighthouse 100점이어도 실제 탐색 흐름이 어색한 경우가 많습니다. VoiceOver(
Cmd + F5) 또는 NVDA로 Tab 키를 눌러보는 게 중요합니다.
마치며
접근성은 "나중에 하면 되는 것"이 아니라, 지금 쌓아가는 기술 부채 관리의 일부입니다. EAA 발효로 법적 강제가 생겼지만, 시맨틱 HTML과 올바른 ARIA 사용은 결국 코드 품질을 높이는 방향과 정확히 일치합니다.
지금 바로 시작해볼 수 있는 3단계:
- 자동화 스캔 먼저 — Chrome에 axe DevTools 확장을 추가하거나 Lighthouse 접근성 탭으로 현재 이슈 현황을 파악해 보시면 좋습니다. 프로젝트에
pnpm add -D axe-core jest-axe를 추가하고 위 스니펫을 컴포넌트 테스트에 붙여보시면 CI에서 회귀를 자동으로 잡을 수 있습니다. - High-impact 항목부터 수정 — 이미지 alt 텍스트, 폼 label 연결,
:focus-visible포커스 스타일은 공수 대비 효과가 가장 큰 항목입니다. 이 세 가지만 잡아도 체감 개선이 큽니다. - 스크린 리더로 실제 탐색해 보기 — macOS라면
Cmd + F5로 VoiceOver를 켜고 Tab 키로 페이지를 탐색해 보시면 자동화 도구가 잡지 못한 흐름 문제를 직접 체감할 수 있습니다.
오늘 키보드만으로 내 서비스를 처음 탐색해보는 것, 그게 출발점입니다.
다음 글: WAI-ARIA Authoring Practices를 활용한 접근 가능한 모달·드롭다운·캐러셀 컴포넌트 직접 구현하기 (시리즈 링크는 게시 후 업데이트될 예정입니다)
참고 자료
- European Accessibility Act — European Commission 공식 페이지
- EAA Complete Compliance Guide 2025 — AllAccessible
- EAA 2025 Compliance: Web Accessibility Explained — Coaxsoft
- EAA Fines and Penalties by Country 2025 — Web Accessibility Checker
- EAA Enforcement in Europe Following June 2025 Deadline — Pivotal Accessibility
- WCAG 2.2 Complete Guide 2025 — AllAccessible
- WCAG 2 Overview — W3C WAI
- axe-core GitHub — Deque Systems
- Accessibility Testing — Cypress 공식 문서
- 한국형 웹 콘텐츠 접근성 지침(KWCAG) 2.2