도입 스토리
"장바구니에 상품 추가하면 스크린리더가 아무 말도 안 해요." 김개발이 말했습니다.
시각적으로는 작은 배지가 "3"으로 업데이트되었습니다. 하지만 스크린리더는 아무 알림도 보내지 않았습니다. 포커스도 이동하지 않았습니다. 마치 아무 일도 없었던 것처럼.
"JavaScript로 DOM을 업데이트했을 때 스크린리더가 자동으로 알아채지 못해요." 박멘토가 설명했습니다. "ARIA Live Region을 설정해줘야 해요. 동적 콘텐츠를 위한 '방송 채널' 같은 거예요."
핵심 개념 설명
ARIA Live Region
aria-live 속성이 있는 요소는 내용이 변경될 때 스크린리더가 자동으로 읽어줍니다.
<!-- 상태 메시지 영역 (장바구니 알림, 저장 완료 등) --><div id="status-message" aria-live="polite" aria-atomic="true" class="sr-only"> <!-- JavaScript로 내용을 추가/변경하면 스크린리더가 읽어줌 --></div>
aria-live 값:
polite: 현재 읽던 것이 끝난 후 읽음 (대부분 사용)assertive: 즉시 읽음, 기존 읽기를 중단 (오류, 긴급 알림에만 사용)off: 변경 사항 알리지 않음 (기본값)
function announceToScreenReader(message, priority = 'polite') { const announcer = document.getElementById('status-message'); // 같은 메시지를 연속으로 읽히려면 초기화 후 재설정 announcer.textContent = ''; // 짧은 지연 후 메시지 설정 (같은 메시지도 변경으로 인식) requestAnimationFrame(() => { announcer.textContent = message; });}// 장바구니 추가 시function addToCart(productName) { // 카트 업데이트 로직... cartCount++; updateCartBadge(cartCount); // 스크린리더에 알림 announceToScreenReader(`${productName}이(가) 장바구니에 추가되었습니다. 현재 ${cartCount}개`);}
role="status"와 role="alert"
역할(role)로도 동적 알림을 설정할 수 있습니다.
<!-- 일반 상태 메시지 (polite와 동일) --><div role="status" aria-live="polite"> 저장되었습니다.</div><!-- 오류/긴급 메시지 (assertive와 동일) --><div role="alert" aria-live="assertive"> 네트워크 오류가 발생했습니다. 다시 시도해 주세요.</div>
상태 메시지 — WCAG 4.1.3
포커스를 이동시키지 않고도 중요한 상태 변화를 스크린리더 사용자에게 알려야 합니다.
// 검색 결과 업데이트async function search(query) { const results = await fetchResults(query); // 결과 표시 renderResults(results); // 스크린리더에 결과 수 알림 (포커스 이동 없이) const statusEl = document.getElementById('search-status'); statusEl.textContent = results.length > 0 ? `"${query}"에 대한 검색 결과 ${results.length}건` : `"${query}"에 대한 검색 결과가 없습니다.`;}
<!-- 검색 결과 상태 --><div id="search-status" role="status" aria-live="polite" aria-atomic="true" class="sr-only"> <!-- 검색 후 결과 수 알림 --></div><div id="search-results" aria-label="검색 결과"> <!-- 결과 목록 --></div>
aria-atomic과 aria-relevant
<!-- aria-atomic="true": 영역 전체를 하나로 읽음 --><div aria-live="polite" aria-atomic="true"> 장바구니: 3개 항목, 총 150,000원 <!-- "장바구니: 3개 항목, 총 150,000원" 전체를 읽음 --></div><!-- aria-atomic="false" (기본): 변경된 부분만 읽음 --><div aria-live="polite" aria-atomic="false"> <span id="count">3</span>개 항목 <!-- "3" 부분만 변경되면 "3"만 읽음 --></div><!-- aria-relevant: 어떤 변경을 알릴지 --><div aria-live="polite" aria-relevant="additions removals"> <!-- additions: 추가된 노드, removals: 제거된 노드 알림 --></div>
로딩 상태 처리
비동기 작업 중 로딩 상태를 스크린리더에 알립니다.
<!-- 로딩 버튼 --><button id="save-btn" type="submit" aria-describedby="save-status"> 저장</button><span id="save-status" role="status" aria-live="polite" class="sr-only"></span>
async function saveData() { const btn = document.getElementById('save-btn'); const status = document.getElementById('save-status'); // 로딩 상태 btn.disabled = true; btn.setAttribute('aria-busy', 'true'); status.textContent = '저장 중입니다.'; try { await api.save(data); // 성공 status.textContent = '저장이 완료되었습니다.'; } catch (error) { // 실패 status.textContent = '저장에 실패했습니다. 다시 시도해 주세요.'; } finally { btn.disabled = false; btn.removeAttribute('aria-busy'); }}
단계별 실습
따라하기: Live Region 테스트
<!DOCTYPE html><html lang="ko"><head><meta charset="UTF-8"><title>Live Region 테스트</title></head><body><button onclick="addMessage()">메시지 추가</button><div id="messages" aria-live="polite" aria-atomic="false"> <!-- 스크린리더가 새 항목을 읽어줌 --></div><script>let count = 0;function addMessage() { count++; const p = document.createElement('p'); p.textContent = `메시지 ${count}: ${new Date().toLocaleTimeString()}`; document.getElementById('messages').appendChild(p);}</script></body></html>
NVDA나 VoiceOver를 켜고 버튼을 클릭하면 새 메시지가 자동으로 읽히는지 확인합니다.
변형하기: 장바구니 알림 구현
기존 장바구니 버튼에 Live Region 알림을 추가합니다.
// 스크린리더 전용 공지 유틸리티const Announcer = { polite: document.getElementById('polite-announcer'), assertive: document.getElementById('assertive-announcer'), say(message, priority = 'polite') { const el = priority === 'assertive' ? this.assertive : this.polite; el.textContent = ''; requestAnimationFrame(() => { el.textContent = message; }); }};document.querySelectorAll('.add-to-cart-btn').forEach(btn => { btn.addEventListener('click', () => { const name = btn.dataset.productName; addToCart(btn.dataset.productId); Announcer.say(`${name}이(가) 장바구니에 추가되었습니다.`); });});
정리와 확인
핵심 내용 요약
aria-live="polite": DOM 변경 시 다음 타이밍에 스크린리더 읽기aria-live="assertive": 즉시 읽기 — 오류나 긴급 상황에만 사용role="status": polite Live Region,role="alert": assertive Live Region- WCAG 4.1.3: 포커스 이동 없이 상태 메시지를 알림으로 제공
aria-busy="true": 로딩 중임을 보조기기에 알림
확인 문제
문제 1. 폼 제출 중 "처리 중..." 메시지를 즉시 읽혀야 할 때 어떤 aria-live 값을 써야 하는가?
"처리 중..."은 즉각적인 피드백이 필요하지만 긴급하지 않으므로 polite가 적절합니다.
assertive는 오류 메시지처럼 즉시 주의가 필요한 경우에만 써야 합니다.
문제 2. aria-atomic="true"와 aria-atomic="false"의 차이는?
true: Live Region 전체 내용을 하나의 단위로 읽음
false(기본): 변경된 부분만 읽음
예: "장바구니: 3개" → "4개"로 바뀌면
true: "장바구니: 4개" 전체 읽음
false: "4개"만 읽음
다음 챕터에서는 ARIA를 올바르게 사용하는 법을 다룹니다. No ARIA is better than bad ARIA — 잘못된 ARIA가 접근성을 오히려 망치는 이유를 알아봅니다.