1. 웹 접근성이란 ?
장애 유무와 관계없이 모든 사용자가 웹을 동등하게 이용할 수 있도록 보장하는 것이다.
대상 장애 유형
- 시각 장애 (전맹, 저시력, 색각 이상)
- 청각 장애
- 운동 장애 (마우스 사용 불가 등)
- 인지 / 학습 장애
2. WCAG 2.1 -- 4대 원칙 (POUR)
| 원칙 | 설명 |
| Perceivable | 인식 가능해야 한다. |
| Operable | 조작 가능해야 한다. |
| Understandable | 이해 가능해야 한다. |
| Robust | 다양한 환경에서 동작 |
원칙별 실무 적용 예시
<!-- Perceivable: 이미지에 대체 텍스트 제공 -->
<img src="sale-banner.png" alt="여름 세일 최대 50% 할인">
<!-- Operable: div에 click만 달면 키보드 접근 불가 → button 태그 사용 -->
❌ <div onclick="doSomething()">클릭</div>
✅ <button type="button" onclick="doSomething()">클릭</button>
<!-- Understandable: 오류 메시지는 구체적으로 안내 -->
❌ <span>입력 오류</span>
✅ <span role="alert">이메일 형식이 올바르지 않습니다 (예: name@email.com)</span>
<!-- Robust: 표준 시맨틱 태그 사용 -->
❌ <div class="header">...</div>
✅ <header>...</header>
3. 대체 텍스트 (alt)
개념 이미지 정보를 스크린리더 사용자에게 전달한다.
| 유형 | 처리 |
| 의미 있는 이미지 | alt="설명" |
| 장식용 이미지 | alt=""(빈 값,속성 자체 생략X) |
| 링크 이미지 | 링크 목적을 설명하는 텍스트 |
| 복잡한 이미지 | alt + aria-describedby or 본문 내 추가 설명 |
주의
- alt 속성 자체가 없으면 스크린리더가 파일명을 그대로 읽음
- 의미 없는 alt (alt="이미지") 금지
<!-- ✅ 의미 있는 이미지 -->
<img src="sale.png" alt="여름 세일 최대 50% 할인">
<!-- ✅ 장식용 이미지 — alt="" 빈 값으로 처리 (속성 생략 X) -->
<img src="divider.png" alt="">
<!-- ✅ 링크 안의 이미지 — 링크 목적을 설명 -->
<a href="/home">
<img src="logo.png" alt="지마켓 홈으로 이동">
</a>
<!-- ✅ 복잡한 이미지 (차트 등) — aria-describedby로 본문 설명 연결 -->
<img src="chart.png" alt="2024년 분기별 매출 차트" aria-describedby="chart-desc">
<p id="chart-desc">1분기 120억, 2분기 150억, 3분기 180억, 4분기 210억으로 꾸준히 증가</p>
<!-- ❌ alt 속성 없음 — 스크린리더가 파일명을 그대로 읽음 -->
<img src="banner_2024_v3_final.png">
<!-- ❌ 의미 없는 alt -->
<img src="sale.png" alt="이미지">
4.키보드 접근성
기본 원칙 모든 기능은 키보드로 조작 가능해야된다.
주요 키
| 키 | 동작 |
| Tab / Shift+Tab | 포커스 이동 |
| Enter / Space | 실행 |
| Esc | 닫기 |
포커스 규칙
- DOM 순서 = 시각적 순서
- 모달이 열리면 포커스를 모달 내부로 이동
- 모달이 닫히면 트리거(열기 버튼)로 포커스 복원
- 모달 내부는 포커스 트랩 적용 (Tab이 모달 밖으로 나가지 않게)
- 단, 일반 페이지 흐름에서 포커스 트랩은 절대 금지 (모달 / 다이얼로그 한정)
/* 마우스 사용 시에만 아웃라인 숨기기 */
:focus:not(:focus-visible) { outline: none; }
:focus-visible { outline: 2px solid #0066cc; outline-offset: 2px; }
# outline: none 전체 적용 금지 - 키보드 사용자가 현재 위치를 알 수 없게 된다.
# display: none / visibility : hidden 은 접근성 텍스트 숨김 용도로 사용 금지
6. wai-aria
원칙 시맨틱 HTML로 해결 가능하면 aria 사용 금지 aria는 보완재
<!-- ❌ div에 role 붙이는 것보다 -->
<div role="button" tabindex="0">확인</div>
<!-- ✅ 네이티브 button 쓰는 게 낫다 -->
<!-- 네이티브 요소는 키보드 접근, Enter/Space 실행이 기본 내장 -->
<button type="button">확인</button>
| 종류 | 설명 | 예시 |
| role | 역할 정의 | role="dialog" |
| property | 요소의 속성/관계 | aria-label aria-labelledby |
| state | 현재 상태(동적 변경) | aria-expanded aria-selected |
7. 주요 aria 속성
aria-label vs aria-labelledby
| 속성 | 언제 쓰나 |
| aria-label | 레이블 텍스트가 화면에 없을 때 (문자열 직접 제공) |
| aria-labelledby | 레이블이 화면에 이미 있을 때 (요소 id 참조) |
<!-- aria-label: 화면에 텍스트가 없는 아이콘 버튼 -->
<button aria-label="검색">
<svg aria-hidden="true">...</svg>
</button>
<!-- aria-labelledby: 모달 제목이 이미 화면에 있을 때 -->
<dialog aria-labelledby="modal-title">
<h2 id="modal-title">배송지 변경</h2>
<p>...</p>
</dialog>
<!-- 스크린리더: "배송지 변경 대화상자" -->
<!-- ❌ 텍스트가 이미 있는데 aria-label 중복 사용 -->
<button aria-label="닫기">닫기</button>
<!-- 스크린리더에 따라 "닫기 닫기" 이중으로 읽힐 수 있음 -->
aria-expanded
열림 / 닫힘이 있는 컴포넌트 (드롭다운, 아코디언, 탭 등)에 필수다.
<!-- 초기값은 반드시 false (닫혀 있는 상태가 기본) -->
<button aria-expanded="false" aria-controls="menu">카테고리</button>
<ul id="menu" hidden>
<li><a href="/fashion">패션</a></li>
<li><a href="/beauty">뷰티</a></li>
</ul>
btn.addEventListener('click', () => {
const isExpanded = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!isExpanded));
menu.hidden = isExpanded;
});
# 초기값을 true로 쓰는 실수 주의 - 닫혀 있는 상태가 기본이면 반드시 false로 시작한다.
aria-hidden
스크린리더에서 해당 요소와 하위 요소 전체를 숨김
<!-- ✅ 장식용 아이콘 -->
<i class="icon-star" aria-hidden="true"></i>
<!-- ✅ 텍스트 옆 보조 아이콘 -->
<span>3개 선택됨</span>
<svg aria-hidden="true" focusable="false">...</svg>
<!-- ❌ 포커스 가능한 요소에 사용 금지 -->
<!-- 키보드로 포커스는 가는데 스크린리더가 읽지 않아 혼란 야기 -->
<button aria-hidden="true">저장</button>
<!-- ❌ aria-hidden 부모 안에 포커스 가능한 자식 포함 금지 -->
<div aria-hidden="true">
<button>클릭</button> <!-- 이 버튼도 스크린리더에서 사라짐 -->
</div>
role="alert" / role="status"
동적으로 변하는 메시지를 스크린리더에 자동으로 알릴 때 사용
| role | 특징 | 용도 |
| alert | 즉시 인터럽트해서 읽음 | 오류, 경고 |
| status | 자연스럽게 읽음 | 저장 완료, 로딩 중 |
html
<!-- 오류 메시지 영역 -->
<div role="alert" id="error-msg"></div>
<!-- 저장 완료 안내 영역 -->
<div role="status" id="status-msg"></div>
js
// JS로 텍스트를 삽입하는 순간 스크린리더가 자동으로 읽음
document.getElementById('error-msg').textContent = '비밀번호가 일치하지 않습니다.';
document.getElementById('status-msg').textContent = '저장되었습니다.';
** role="alert"는 aria-live="assertive"를 내장하고 있어서 따로 선언하지 않아도 된다.
<!-- ❌ 중복 선언 — 불필요 -->
<div role="alert" aria-live="assertive">...</div>
<!-- ✅ 이렇게만 해도 충분 -->
<div role="alert">...</div>
# role="alert" 남발 금지 - 사소한 안내 메시지에 쓰면 UX 흐름이 깨진다.
8. 폼 접근성
label 연결
<!-- ✅ for-id 연결 -->
<label for="email">이메일</label>
<input type="email" id="email">
<!-- ✅ 감싸는 방식도 가능 -->
<label>
이메일
<input type="email">
</label>
<!-- ❌ placeholder만 사용 금지 — 포커스 시 사라져서 인지 부담 큼 -->
<input type="email" placeholder="이메일 입력">
<!-- ✅ placeholder는 힌트 용도로만, label과 함께 사용 -->
<label for="email">이메일</label>
<input type="email" id="email" placeholder="name@email.com">
필수 항목 표시
<!-- ✅ * 기호는 장식이므로 aria-hidden, 필수 여부는 aria-required로 전달 -->
<label for="name">이름 <span aria-hidden="true">*</span></label>
<input type="text" id="name" aria-required="true">
오류 처리
html
<label for="phone">전화번호</label>
<input
type="tel"
id="phone"
aria-describedby="phone-error"
aria-invalid="true"
>
<span id="phone-error" role="alert">
전화번호 형식이 올바르지 않습니다 (예: 010-0000-0000)
</span>
js
// 유효성 검사 후 상태 동적으로 변경
input.setAttribute('aria-invalid', 'true');
errorMsg.textContent = '전화번호 형식이 올바르지 않습니다';
// 올바르게 입력 시 초기화
input.setAttribute('aria-invalid', 'false');
errorMsg.textContent = '';
9. 아이콘 / 이미지 버튼
<!-- ✅ SVG + 숨김 텍스트 -->
<button type="button">
<svg aria-hidden="true" focusable="false">...</svg>
<span class="for-a11y">장바구니 담기</span>
</button>
<!-- ✅ img 태그 -->
<button type="button">
<img src="cart.svg" alt="장바구니 담기">
</button>
<!-- ✅ background-image로 처리할 때 -->
<button type="button" aria-label="장바구니 담기">
<span aria-hidden="true"></span>
</button>
<!-- ❌ 텍스트 대안 없는 아이콘 버튼 — "버튼"만 읽힘 -->
<button type="button">
<svg>...</svg>
</button>