웹 접근성이 중요한 이유
웹 접근성(a11y, accessibility의 a와 y 사이 11글자)은 장애가 있는 사용자도 웹을 동등하게 이용할 수 있도록 만드는 것입니다. 건물에 계단만 있으면 휠체어 사용자는 들어갈 수 없습니다. 웹에서 경사로 역할을 하는 것이 접근성입니다.
전 세계 인구의 약 15%(10억 명)가 어떤 형태의 장애를 가지고 있습니다. 접근성은 도덕적 의무이자, 법적 요구사항이며, 비즈니스 기회이기도 합니다.
WCAG 4대 원칙 (POUR)
| 원칙 | 의미 | 예시 |
|---|---|---|
| Perceivable (인식 가능) | 모든 콘텐츠를 인식할 수 있어야 함 | 이미지에 대체 텍스트 제공 |
| Operable (조작 가능) | 키보드만으로 모든 기능 사용 가능 | 탭으로 네비게이션, Enter로 활성화 |
| Understandable (이해 가능) | 콘텐츠와 UI를 이해할 수 있어야 함 | 명확한 에러 메시지, 일관된 UI |
| Robust (견고함) | 다양한 기술로 접근 가능 | 스크린 리더, 음성 인식 등 |
시맨틱 HTML: 접근성의 기초
시맨틱 HTML은 접근성의 80%를 차지합니다. ARIA를 사용하기 전에 올바른 HTML 요소를 먼저 사용하세요.
<!-- 잘못됨: div로 모든 것을 만듦 (스크린 리더가 구조 파악 불가) -->
<div class="header">
<div class="nav">
<div class="link" onclick="navigate('/')">홈</div>
<div class="link" onclick="navigate('/about')">소개</div>
</div>
</div>
<div class="main">
<div class="article">
<div class="title">웹 접근성 가이드</div>
<div class="content">...</div>
</div>
</div>
<!-- 올바름: 시맨틱 요소 사용 (자동으로 역할과 구조 전달) -->
<header>
<nav aria-label="메인 네비게이션">
<a href="/">홈</a>
<a href="/about">소개</a>
</nav>
</header>
<main>
<article>
<h1>웹 접근성 가이드</h1>
<p>...</p>
</article>
</main>
<footer>
<p>Copyright 2026</p>
</footer>
시맨틱 요소 → ARIA 역할 매핑
| HTML 요소 | 암묵적 ARIA 역할 | 스크린 리더 읽음 |
|---|---|---|
nav | navigation | ”네비게이션” |
main | main | ”메인 콘텐츠” |
header | banner | ”배너” |
footer | contentinfo | ”콘텐츠 정보” |
button | button | ”버튼” |
a[href] | link | ”링크” |
h1~h6 | heading | ”제목 레벨 N” |
ul/ol | list | ”목록, N개 항목” |
ARIA 속성 실전
ARIA(Accessible Rich Internet Applications)는 시맨틱 HTML로 표현할 수 없는 복잡한 UI에 접근성 정보를 추가합니다.
핵심 규칙: ARIA보다 시맨틱 HTML 우선
<!-- 잘못됨: div에 ARIA 역할 부여 -->
<div role="button" tabindex="0" onclick="submit()">제출</div>
<!-- 올바름: 네이티브 button 사용 -->
<button onclick="submit()">제출</button>
<!-- button은 자동으로 role="button", 키보드 지원, 포커스 관리 제공 -->
자주 쓰는 ARIA 패턴
<!-- 1. 아코디언 -->
<button
aria-expanded="false"
aria-controls="panel-1"
id="accordion-1">
자주 묻는 질문
</button>
<div
id="panel-1"
role="region"
aria-labelledby="accordion-1"
hidden>
<p>접근성은 모든 사용자를 위한 것입니다.</p>
</div>
<!-- 2. 탭 패널 -->
<div role="tablist" aria-label="콘텐츠 탭">
<button role="tab" aria-selected="true"
aria-controls="tab-panel-1" id="tab-1">
개요
</button>
<button role="tab" aria-selected="false"
aria-controls="tab-panel-2" id="tab-2"
tabindex="-1">
상세
</button>
</div>
<div role="tabpanel" id="tab-panel-1"
aria-labelledby="tab-1">
<p>개요 내용입니다.</p>
</div>
<div role="tabpanel" id="tab-panel-2"
aria-labelledby="tab-2" hidden>
<p>상세 내용입니다.</p>
</div>
<!-- 3. 실시간 알림 (스크린 리더가 즉시 읽음) -->
<div aria-live="polite" aria-atomic="true" id="status">
<!-- JavaScript로 동적 메시지 삽입 -->
</div>
<!-- 4. 모달 다이얼로그 -->
<dialog aria-labelledby="modal-title" aria-modal="true">
<h2 id="modal-title">삭제 확인</h2>
<p>정말 삭제하시겠습니까?</p>
<button>확인</button>
<button>취소</button>
</dialog>
키보드 네비게이션
모든 인터랙티브 요소는 키보드로 접근 가능해야 합니다.
// 탭 패널 키보드 네비게이션 구현
document.querySelector("[role='tablist']").addEventListener("keydown", (e) => {
const tabs = [...e.currentTarget.querySelectorAll("[role='tab']")];
const currentIndex = tabs.indexOf(e.target);
let newIndex;
switch (e.key) {
case "ArrowRight": // 오른쪽 화살표 → 다음 탭
newIndex = (currentIndex + 1) % tabs.length;
break;
case "ArrowLeft": // 왼쪽 화살표 → 이전 탭
newIndex = (currentIndex - 1 + tabs.length) % tabs.length;
break;
case "Home": // Home → 첫 번째 탭
newIndex = 0;
break;
case "End": // End → 마지막 탭
newIndex = tabs.length - 1;
break;
default:
return; // 다른 키는 무시
}
// 포커스 이동 및 탭 활성화
tabs[newIndex].focus();
tabs[newIndex].click();
e.preventDefault();
});
/* 포커스 스타일 — 키보드 사용자를 위한 시각적 표시 */
/* 잘못됨: 포커스 스타일 제거 */
*:focus { outline: none; } /* 절대 금지! */
/* 올바름: 명확한 포커스 표시 */
:focus-visible {
outline: 3px solid #4f46e5; /* 인디고 색상 아웃라인 */
outline-offset: 2px; /* 요소로부터 2px 떨어져서 */
border-radius: 4px;
}
/* 마우스 클릭 시에는 아웃라인 숨김, 키보드 탐색 시에만 표시 */
:focus:not(:focus-visible) {
outline: none;
}
/* 건너뛰기 링크 — 키보드 사용자가 반복 네비게이션 건너뛰기 */
.skip-link {
position: absolute;
top: -100%;
left: 0;
z-index: 9999;
padding: 1rem;
background: #1e1b4b;
color: white;
}
.skip-link:focus {
top: 0; /* 포커스 시 화면에 표시 */
}
<!-- 건너뛰기 링크 사용 -->
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header>...</header>
<nav>...</nav>
<main id="main-content">
<!-- 메인 콘텐츠 -->
</main>
</body>
이미지와 대체 텍스트
<!-- 정보를 전달하는 이미지 — 구체적 설명 -->
<img src="chart.png" alt="2025년 분기별 매출 — 1분기 100억, 2분기 150억, 3분기 200억" />
<!-- 장식용 이미지 — 빈 alt (스크린 리더가 무시) -->
<img src="decoration.svg" alt="" />
<!-- 링크 안의 이미지 — 링크 목적 설명 -->
<a href="/profile">
<img src="avatar.jpg" alt="내 프로필로 이동" />
</a>
<!-- 복잡한 이미지 — 상세 설명 연결 -->
<figure>
<img src="architecture.png"
alt="마이크로서비스 아키텍처 다이어그램"
aria-describedby="arch-desc" />
<figcaption id="arch-desc">
API Gateway가 3개의 서비스(인증, 상품, 주문)로 요청을 라우팅하며,
각 서비스는 독립된 데이터베이스를 사용합니다.
</figcaption>
</figure>
폼 접근성
<!-- 올바른 폼 구조 -->
<form aria-labelledby="form-title">
<h2 id="form-title">회원가입</h2>
<!-- label과 input 연결 필수 -->
<div>
<label for="user-email">이메일 (필수)</label>
<input
type="email"
id="user-email"
name="email"
required
aria-required="true"
aria-describedby="email-hint email-error"
autocomplete="email" />
<p id="email-hint">예: user@example.com</p>
<p id="email-error" role="alert" hidden>
유효한 이메일을 입력해주세요.
</p>
</div>
<!-- 비밀번호 — 요구사항 안내 -->
<div>
<label for="user-pw">비밀번호 (필수)</label>
<input
type="password"
id="user-pw"
name="password"
required
aria-required="true"
aria-describedby="pw-requirements"
minlength="8"
autocomplete="new-password" />
<ul id="pw-requirements">
<li>8자 이상</li>
<li>영문, 숫자, 특수문자 포함</li>
</ul>
</div>
<button type="submit">가입하기</button>
</form>
접근성 테스트 도구
| 도구 | 유형 | 용도 |
|---|---|---|
| axe DevTools | 브라우저 확장 | 자동화된 접근성 검사 |
| Lighthouse | Chrome 내장 | 접근성 점수 측정 |
| NVDA | 스크린 리더 (Windows) | 실제 스크린 리더 테스트 |
| VoiceOver | 스크린 리더 (macOS) | 실제 스크린 리더 테스트 |
| Colour Contrast Analyser | 데스크톱 앱 | 색상 대비 확인 |
# axe-core CLI로 자동 테스트
npx @axe-core/cli https://example.com
# jest-axe로 컴포넌트 테스트에 접근성 검사 통합
npm install --save-dev jest-axe
실전 팁
- 시맨틱 HTML을 먼저 사용하세요: 대부분의 접근성 문제는 올바른 HTML 요소를 사용하는 것만으로 해결됩니다.
div와span남용을 피하세요. - 키보드만으로 사이트를 사용해보세요: Tab, Shift+Tab, Enter, Space, Escape, 화살표 키로 모든 기능을 사용할 수 있는지 직접 확인하세요.
:focus-visible스타일을 제거하지 마세요: 포커스 표시를 없애면 키보드 사용자는 현재 위치를 알 수 없습니다.- 색상에만 의존하지 마세요: 에러를 빨간색으로만 표시하면 색각 이상 사용자는 인지할 수 없습니다. 아이콘이나 텍스트를 함께 제공하세요.
- ARIA는 최후의 수단입니다: ARIA를 잘못 사용하면 접근성이 오히려 나빠집니다. “No ARIA is better than bad ARIA”라는 규칙을 기억하세요.
- 실제 스크린 리더로 테스트하세요: Windows에서는 NVDA(무료), macOS에서는 VoiceOver(내장)로 직접 듣고 확인하는 것이 가장 확실합니다.