iBetter Books
수정

도입 스토리

"장바구니에 상품 추가하면 스크린리더가 아무 말도 안 해요." 김개발이 말했습니다.

시각적으로는 작은 배지가 "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가 접근성을 오히려 망치는 이유를 알아봅니다.