웹 접근성 가이드 - 모두를 위한 웹사이트 만들기
웹사이트를 만들고 나서 키보드로 탐색해본 적 있으신가요?
저는 처음 만든 포트폴리오 사이트를 친구에게 자랑하다가 충격을 받은 적이 있습니다. 친구가 “Tab 키를 눌러봐”라고 하더군요. 눌러보니… 어디에 포커스가 있는지 전혀 보이지 않았습니다. 마우스 없이는 아무것도 할 수 없는 사이트였던 거죠.
그때 깨달았습니다. “멋지게 보이는 것”과 “모두가 사용할 수 있는 것”은 다르다는 걸요.
웹 접근성(Web Accessibility)은 장애가 있는 사람을 포함한 모든 사람이 웹사이트를 이용할 수 있도록 만드는 것입니다. 이는 단순히 법적 요구사항을 넘어, 더 많은 사용자에게 다가갈 수 있는 방법입니다.
왜 접근성이 중요한가요?
1. 더 많은 사용자에게 도달
전 세계 인구의 약 15%가 어떤 형태로든 장애를 가지고 있습니다. 이는:
- 시각 장애 (맹인, 저시력, 색맹)
- 청각 장애 (청각 손실)
- 운동 장애 (키보드만 사용, 음성 제어 필요)
- 인지 장애 (난독증, ADHD, 자폐 스펙트럼)
하지만 접근성은 장애인만을 위한 것이 아닙니다.
- 노트북에서 마우스가 고장났을 때
- 밝은 햇빛 아래에서 화면을 볼 때
- 시끄러운 카페에서 비디오를 볼 때
- 한 손으로 휴대폰을 들고 있을 때
우리 모두가 때때로 “일시적인 장애”를 경험합니다.
2. 더 나은 SEO
검색 엔진도 웹사이트를 “읽는” 사용자입니다. 접근성이 좋은 사이트는:
- 시맨틱 HTML로 구조가 명확함
- 이미지에 대체 텍스트가 있음
- 콘텐츠 계층이 명확함
즉, 접근성 개선 = SEO 개선입니다.
3. 법적 준수
많은 국가에서 웹 접근성이 법적 요구사항입니다.
- 한국: 장애인차별금지법
- 미국: ADA (Americans with Disabilities Act)
- EU: European Accessibility Act
WCAG란 무엇인가요?
WCAG (Web Content Accessibility Guidelines)는 W3C에서 만든 웹 접근성 국제 표준입니다.
WCAG의 4가지 원칙 (POUR)
1. Perceivable (인지 가능)
→ 사용자가 콘텐츠를 인지할 수 있어야 합니다
2. Operable (운용 가능)
→ 사용자가 인터페이스를 조작할 수 있어야 합니다
3. Understandable (이해 가능)
→ 콘텐츠와 조작법을 이해할 수 있어야 합니다
4. Robust (견고함)
→ 다양한 기술(보조 기술 포함)에서 작동해야 합니다
적합성 레벨
- Level A: 최소 요구사항
- Level AA: 권장 수준 (대부분의 법률 준수)
- Level AAA: 가장 높은 수준 (모든 사이트에 필요하진 않음)
이 가이드는 Level AA를 목표로 합니다.
실전 접근성 체크리스트
1. 키보드 접근성 ⌨️
모든 기능이 키보드만으로 사용 가능해야 합니다.
✅ 해야 할 것
<!-- 모든 interactive 요소는 포커스 가능 -->
<button type="button">클릭</button>
<a href="/page">링크</a>
<!-- 포커스 순서 논리적으로 -->
<nav>
<a href="#main">본문으로 건너뛰기</a> <!-- 첫 번째 -->
</nav>
<main id="main">
<!-- 메인 콘텐츠 -->
</main>
❌ 하지 말아야 할 것
<!-- ❌ div에 클릭 이벤트 (키보드 접근 불가) -->
<div onclick="handleClick()">클릭하세요</div>
<!-- ❌ 포커스 불가능한 요소 -->
<span onclick="doSomething()">버튼처럼 보임</span>
CSS: 포커스 표시
/* ✅ 좋은 예: 명확한 포커스 표시 */
:focus-visible {
outline: 3px solid #ff230a;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(255, 35, 10, 0.4);
}
/* ❌ 나쁜 예: 포커스 제거 */
:focus {
outline: none; /* 절대 이렇게 하지 마세요! */
}
왜 outline: none이 문제일까요?
키보드 사용자는 포커스가 어디에 있는지 알 수 없어서 사이트를 탐색할 수 없습니다.
2. 시맨틱 HTML 🏗️
올바른 HTML 태그를 사용하면 의미가 명확해집니다.
✅ 좋은 예
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">홈</a></li>
<li><a href="/about">소개</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>제목</h1>
<p>내용...</p>
</article>
</main>
<footer>
<p>© 2025 회사명</p>
</footer>
❌ 나쁜 예
<!-- ❌ 모든 게 div -->
<div class="header">
<div class="nav">
<div class="link">홈</div>
<div class="link">소개</div>
</div>
</div>
<div class="content">
<div class="title">제목</div>
<div class="text">내용...</div>
</div>
왜 시맨틱 HTML이 중요할까요?
스크린 리더는 HTML 태그로 페이지 구조를 파악합니다. <nav>를 만나면 “내비게이션 영역”이라고 읽어주고, <main>을 만나면 “메인 콘텐츠”라고 알려줍니다.
3. Heading 계층 🗂️
제목 태그는 순서대로 사용해야 합니다.
✅ 좋은 예
<h1>페이지 제목</h1>
<h2>섹션 1</h2>
<h3>섹션 1.1</h3>
<h3>섹션 1.2</h3>
<h2>섹션 2</h2>
<h3>섹션 2.1</h3>
❌ 나쁜 예
<!-- ❌ 계층 건너뛰기 -->
<h1>페이지 제목</h1>
<h3>섹션 제목</h3> <!-- h2를 건너뜀! -->
<h2>다음 섹션</h2> <!-- 순서가 뒤바뀜! -->
시각적으로만 크게 만들고 싶다면 CSS를 사용하세요:
/* h3를 h2처럼 보이게 */
h3.large {
font-size: 2rem;
font-weight: 600;
}
4. 이미지 대체 텍스트 🖼️
모든 의미 있는 이미지에 alt 속성을 추가하세요.
✅ 좋은 예
<!-- 의미 있는 이미지 -->
<img src="chart.png" alt="2025년 매출 증가 그래프: 1분기 30%, 2분기 45% 증가">
<!-- 장식용 이미지 -->
<img src="decoration.png" alt="" role="presentation">
<!-- 링크 안의 이미지 -->
<a href="/profile">
<img src="avatar.png" alt="김철수 프로필">
</a>
❌ 나쁜 예
<!-- ❌ alt 없음 -->
<img src="chart.png">
<!-- ❌ 의미 없는 alt -->
<img src="chart.png" alt="이미지">
<img src="graph.png" alt="graph.png">
<!-- ❌ 너무 긴 alt -->
<img src="product.png" alt="이 제품은 매우 훌륭한 제품으로서 우리 회사의 최신 기술을 활용하여...">
alt 텍스트 작성 팁:
- 간결하게: 125자 이내
- 맥락 고려: 주변 텍스트와 중복 피하기
- 키워드 채우기 금지: SEO를 위해 키워드만 나열하지 마세요
5. 색상 대비 🎨
텍스트와 배경 간 충분한 대비가 필요합니다.
WCAG 색상 대비 기준
- 일반 텍스트: 최소 4.5:1
- 큰 텍스트 (18pt+ 또는 14pt+ bold): 최소 3:1
- UI 요소: 최소 3:1
✅ 좋은 예
/* 대비 비율: 7.1:1 (충분함) */
.text {
color: #000000;
background: #ffffff;
}
/* 대비 비율: 12.6:1 (매우 좋음) */
[data-theme="dark"] .text {
color: #ffffff;
background: #000000;
}
❌ 나쁜 예
/* ❌ 대비 비율: 1.9:1 (불충분) */
.text {
color: #cccccc;
background: #ffffff;
}
/* ❌ 대비 비율: 2.1:1 (불충분) */
.text {
color: #ff6b6b;
background: #ffffff;
}
대비 확인 도구:
- WebAIM Contrast Checker
- Chrome DevTools의 Contrast Ratio 표시
- Colour Contrast Analyser
색상만으로 정보 전달하지 마세요:
<!-- ❌ 색상만 사용 -->
<p style="color: red;">오류가 발생했습니다</p>
<!-- ✅ 아이콘과 텍스트 함께 -->
<p>
<svg aria-hidden="true"><!-- 오류 아이콘 --></svg>
<span class="error-text">오류가 발생했습니다</span>
</p>
6. ARIA 속성 🏷️
ARIA (Accessible Rich Internet Applications)는 HTML만으로 표현하기 어려운 의미를 추가합니다.
ARIA의 5가지 원칙
- No ARIA is better than Bad ARIA: 잘못 사용하느니 안 쓰는게 나음
- Native HTML First: HTML로 가능하면 ARIA 불필요
- Keyboard Accessible: ARIA만으로 키보드 접근성 해결 안 됨
- No Override: 네이티브 의미를 덮어쓰지 마세요
- Interactive Elements: 모든 interactive 요소는 role과 name 필요
자주 사용하는 ARIA 속성
<!-- 버튼 상태 -->
<button aria-pressed="false">구독</button>
<button aria-pressed="true">구독 중</button>
<!-- 확장 가능한 영역 -->
<button aria-expanded="false" aria-controls="menu">
메뉴
</button>
<div id="menu" hidden>
<!-- 메뉴 내용 -->
</div>
<!-- 탭 UI -->
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel1">
탭 1
</button>
<button role="tab" aria-selected="false" aria-controls="panel2">
탭 2
</button>
</div>
<div role="tabpanel" id="panel1">내용 1</div>
<div role="tabpanel" id="panel2" hidden>내용 2</div>
<!-- 라이브 리전 (동적 콘텐츠) -->
<div aria-live="polite" aria-atomic="true">
<p>3개의 새 메시지가 있습니다</p>
</div>
<!-- 로딩 상태 -->
<button aria-busy="true">
저장 중...
</button>
<!-- 레이블 연결 -->
<button aria-label="닫기">
<svg><!-- X 아이콘 --></svg>
</button>
<!-- 설명 추가 -->
<input
type="email"
aria-describedby="email-hint"
>
<span id="email-hint">
example@email.com 형식으로 입력하세요
</span>
❌ ARIA 남용 사례
<!-- ❌ 네이티브 버튼을 div로 만들지 마세요 -->
<div role="button" tabindex="0" onclick="...">
클릭
</div>
<!-- ✅ 그냥 button 사용 -->
<button type="button">클릭</button>
<!-- ❌ 불필요한 role -->
<nav role="navigation"> <!-- nav는 이미 navigation role -->
<ul role="list"> <!-- ul은 이미 list role -->
<li role="listitem"> <!-- li는 이미 listitem role -->
항목
</li>
</ul>
</nav>
<!-- ✅ 간결하게 -->
<nav aria-label="Main navigation">
<ul>
<li>항목</li>
</ul>
</nav>
7. 폼 접근성 📝
폼은 가장 중요한 인터랙션 지점입니다.
✅ 좋은 예
<!-- 명확한 레이블 -->
<label for="email">이메일</label>
<input
type="email"
id="email"
name="email"
required
aria-required="true"
aria-describedby="email-hint"
>
<span id="email-hint">
example@email.com 형식으로 입력하세요
</span>
<!-- 오류 표시 -->
<label for="password">비밀번호</label>
<input
type="password"
id="password"
aria-invalid="true"
aria-describedby="password-error"
>
<span id="password-error" role="alert">
비밀번호는 최소 8자 이상이어야 합니다
</span>
<!-- 그룹화 -->
<fieldset>
<legend>배송 방법</legend>
<label>
<input type="radio" name="shipping" value="standard">
일반 배송
</label>
<label>
<input type="radio" name="shipping" value="express">
빠른 배송
</label>
</fieldset>
❌ 나쁜 예
<!-- ❌ 레이블 없음 -->
<input type="text" placeholder="이름 입력">
<!-- ❌ 레이블과 연결 안 됨 -->
<label>이메일</label>
<input type="email">
<!-- ❌ 오류 표시가 명확하지 않음 -->
<input type="password" style="border-color: red;">
8. 동적 콘텐츠 🔄
JavaScript로 콘텐츠가 변경될 때 스크린 리더에 알려야 합니다.
ARIA Live Regions
<!-- 중요하지 않은 업데이트 -->
<div aria-live="polite">
<p>10초 전에 업데이트됨</p>
</div>
<!-- 중요한 업데이트 (즉시 알림) -->
<div aria-live="assertive">
<p>오류: 파일을 저장할 수 없습니다</p>
</div>
<!-- 상태 메시지 -->
<div role="status" aria-live="polite">
<p>저장되었습니다</p>
</div>
<!-- 경고 메시지 -->
<div role="alert" aria-live="assertive">
<p>연결이 끊어졌습니다</p>
</div>
JavaScript로 동적 콘텐츠 추가
// ✅ 좋은 예: 스크린 리더에 알림
function showNotification(message) {
const notification = document.createElement('div');
notification.setAttribute('role', 'status');
notification.setAttribute('aria-live', 'polite');
notification.textContent = message;
document.body.appendChild(notification);
// 3초 후 제거
setTimeout(() => {
notification.remove();
}, 3000);
}
// ❌ 나쁜 예: 스크린 리더가 모름
function showNotificationBad(message) {
const notification = document.createElement('div');
notification.textContent = message;
document.body.appendChild(notification);
}
9. 모션과 애니메이션 🎬
일부 사용자는 애니메이션에 어지러움을 느낍니다.
prefers-reduced-motion
/* 기본 애니메이션 */
.element {
transition: transform 0.3s ease;
}
.element:hover {
transform: scale(1.1);
}
/* 모션 줄이기 선호 시 */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
JavaScript에서 확인
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (!prefersReducedMotion) {
// 애니메이션 적용
element.classList.add('animated');
}
10. 터치 타겟 크기 📱
터치 타겟은 충분히 커야 합니다.
WCAG 권장 사항
- 최소 크기: 44 x 44 픽셀
- 권장 크기: 48 x 48 픽셀
/* ✅ 모바일 터치 타겟 */
@media (max-width: 768px) {
button,
a,
input[type="button"],
input[type="submit"] {
min-height: 44px;
min-width: 44px;
padding: var(--space-3) var(--space-4);
}
}
/* ✅ 클릭 영역 확장 (실제 크기는 작게) */
.icon-button {
position: relative;
width: 24px;
height: 24px;
}
.icon-button::before {
content: '';
position: absolute;
top: -12px;
left: -12px;
right: -12px;
bottom: -12px;
}
접근성 테스트 방법 🧪
1. 키보드 테스트
- Tab 키로 모든 interactive 요소 탐색
- Enter/Space로 버튼 클릭
- 화살표 키로 라디오 버튼, 드롭다운 탐색
- Esc로 모달 닫기
체크리스트:
- 모든 interactive 요소에 도달 가능?
- 포커스가 명확하게 보이는가?
- 포커스 순서가 논리적인가?
- 포커스 트랩이 없는가?
2. 스크린 리더 테스트
무료 스크린 리더:
- Windows: NVDA (무료, 오픈소스)
- Mac: VoiceOver (내장)
- iOS: VoiceOver (내장)
- Android: TalkBack (내장)
기본 명령어 (NVDA):
Ctrl: 읽기 중지Insert + ↓: 모두 읽기H: 다음 제목으로 이동Tab: 다음 링크/버튼으로 이동
3. 자동화 도구
# Lighthouse (Chrome DevTools)
npm install -g lighthouse
lighthouse https://yoursite.com --only-categories=accessibility
# axe DevTools (Chrome Extension 설치)
# https://www.deque.com/axe/devtools/
# Pa11y
npm install -g pa11y
pa11y https://yoursite.com
4. 브라우저 개발자 도구
Chrome DevTools:
- Elements 탭 → Accessibility 패널
- Lighthouse 탭 → Accessibility 감사
- Rendering 탭 → Emulate vision deficiencies
Firefox DevTools:
- Accessibility Inspector
- 색맹 시뮬레이션
실전 체크리스트 ✅
페이지 레벨
- 모든 페이지에 고유한
<title> <html lang="ko">설정- “본문으로 건너뛰기” 링크
- Landmark 역할 (
<header>,<nav>,<main>,<footer>) - Heading 계층 (H1 → H2 → H3)
콘텐츠
- 모든 이미지에
alt속성 - 비디오에 자막/대본
- 색상만으로 정보 전달하지 않음
- 텍스트 대비 최소 4.5:1
- 텍스트 크기 조절 가능 (200%까지)
인터랙션
- 모든 기능 키보드로 사용 가능
- 포커스 표시 명확
- 터치 타겟 최소 44x44px
- 타임아웃 경고 및 연장 옵션
- 오류 메시지 명확
폼
- 모든 입력 필드에
<label> - 필수 필드 표시 (
required,aria-required) - 오류 메시지 (
aria-invalid,aria-describedby) - 도움말 텍스트 연결
- Submit 후 성공/실패 피드백
동적 콘텐츠
- ARIA live regions
- 로딩 상태 표시 (
aria-busy) - 모달 포커스 트랩
- 모달 닫기 가능 (Esc, X 버튼)
함정과 주의사항 ⚠️
❌ 함정 1: div/span 남용
<!-- ❌ div를 버튼처럼 사용 -->
<div class="button" onclick="submit()">
제출
</div>
<!-- ✅ 네이티브 button 사용 -->
<button type="button" onclick="submit()">
제출
</button>
왜 문제일까요?
- 키보드로 포커스 불가
- 스크린 리더가 버튼으로 인식 못함
- Enter/Space로 작동 안 함
❌ 함정 2: placeholder를 label 대신 사용
<!-- ❌ placeholder만 사용 -->
<input type="text" placeholder="이름">
<!-- ✅ label 사용 -->
<label for="name">이름</label>
<input type="text" id="name" placeholder="홍길동">
왜 문제일까요?
- Placeholder는 입력 시 사라짐
- 스크린 리더가 읽지 못하는 경우 있음
- 대비가 낮아 읽기 어려움
❌ 함정 3: 아이콘만 사용
<!-- ❌ 아이콘만 -->
<button>
<svg><!-- X 아이콘 --></svg>
</button>
<!-- ✅ aria-label 추가 -->
<button aria-label="닫기">
<svg aria-hidden="true"><!-- X 아이콘 --></svg>
</button>
❌ 함정 4: 자동 재생 미디어
<!-- ❌ 자동 재생 -->
<video autoplay>
<source src="video.mp4">
</video>
<!-- ✅ 사용자 제어 -->
<video controls>
<source src="video.mp4">
<track kind="captions" src="captions.vtt" srclang="ko" label="한국어">
</video>
마치며
접근성은 “한 번에 완벽하게”가 아니라 지속적인 개선 과정입니다.
저도 처음에는 “접근성은 복잡하고 어렵다”고 생각했습니다. 하지만 하나씩 적용하다 보니, 대부분은 간단한 습관이더군요:
<button>대신<div>쓰지 않기- 이미지에
alt추가하기 - 색상 대비 확인하기
- 키보드로 테스트하기
작은 것부터 시작하세요. 오늘 하나만 고쳐도 누군가에겐 큰 차이가 됩니다.
참고 자료
- WCAG 2.1 Guidelines
- MDN Web Accessibility
- WebAIM Resources
- The A11Y Project
- Inclusive Components
- ARIA Authoring Practices Guide
도구 및 확장 프로그램
브라우저 확장
- axe DevTools - 자동 접근성 테스트
- WAVE - 시각적 접근성 평가
- Lighthouse - Chrome 내장
댓글