ARIA 접근성 가이드

적용 방법

라이브 리전으로 변화 알리기

포커스가 없는 곳에서 내용이 바뀔 때 화면낭독기가 자동으로 읽도록 하는 라이브 리전(live region)의 종류, 속성, 만드는 방법과 흔한 실패를 정리합니다.

라이브 리전이란

화면낭독기는 기본적으로 포커스가 있는 곳과 사용자가 탐색하는 곳만 읽습니다. 그래서 “저장되었습니다” 메시지, 장바구니 수량, 검색 결과 건수처럼 페이지를 새로 고치지 않고 다른 곳에서 바뀌는 내용은 시각장애인 사용자가 놓치기 쉽습니다.

라이브 리전은 “이 영역의 내용이 바뀌면 사용자에게 알려 달라”고 표시한 영역입니다. 브라우저는 영역 안의 변화를 감지해 보조기술에 전달하고, 화면낭독기는 포커스를 옮기지 않고 바뀐 내용을 읽습니다. WCAG 4.1.3 상태 메시지(수준 AA)는 포커스를 받지 않는 상태 메시지를 보조기술이 알 수 있게 하라고 요구하며, 라이브 리전이 대표적인 충족 방법입니다.

가장 기본적인 라이브 리전
<!-- 1. 빈 영역을 미리 만들어 둠 -->
<div id="save-msg" role="status"></div>

<script>
  // 2. 나중에 내용만 넣으면 화면낭독기가 읽음
  document.getElementById('save-msg').textContent = '저장되었습니다.';
</script>

aria-live: polite와 assertive

aria-live는 변화를 얼마나 급하게 알릴지 정합니다.

aria-live 값
값동작쓰는 곳
off (기본값)변화를 자동으로 알리지 않습니다. 사용자가 그 영역에 가야 들을 수 있습니다.자주 바뀌지만 매번 알릴 필요가 없는 정보
polite지금 읽고 있는 내용을 마친 뒤, 사용자가 쉬는 틈에 알립니다.저장 완료, 검색 결과 건수, 장바구니 담김 등 대부분의 상태 메시지
assertive읽던 내용을 끊고 바로 알립니다.세션 만료 직전 경고, 전송 실패처럼 즉시 알아야 하는 오류

assertive는 아껴 쓰세요

assertive는 사용자가 듣던 내용을 끊습니다. 일반적인 안내에까지 쓰면 정보를 놓치게 되고 사용자가 피로를 느낍니다. 망설여지면 polite를 고릅니다.

라이브 리전 역할: status, alert, log

자주 쓰는 라이브 리전에는 전용 역할이 있습니다. 역할에는 알맞은 aria-live·aria-atomic 값이 암묵적으로 들어 있어, 역할만 지정해도 됩니다.

라이브 리전 역할과 암묵적 값
역할암묵적 aria-live암묵적 aria-atomic용도
statuspolitetrue작업 결과, 진행 상황 등 급하지 않은 상태 메시지. HTML <output> 요소도 이 역할입니다.
alertassertivetrue즉시 알려야 하는 중요한 메시지(오류·경고). 포커스는 옮기지 않습니다.
logpolite(기본 false)채팅, 작업 기록처럼 새 항목이 순서대로 추가되는 영역
timeroff—남은 시간 표시. 매초 읽히지 않도록 기본적으로 알리지 않습니다.
marqueeoff—주가 전광판처럼 중요도가 낮고 계속 바뀌는 정보

사용자의 응답이 필요한 경고라면 alert 대신 포커스를 받는 alertdialog를 씁니다(대화상자 패턴 참고). 알림 UI 전반은 알림 패턴에서 다룹니다.

역할과 aria-live를 함께 쓰는 경우

일부 오래된 브라우저·화면낭독기 조합을 위해 role="status" aria-live="polite"처럼 같은 의미를 중복 지정하는 코드를 자주 볼 수 있습니다. 값이 서로 같다면 문제는 없지만, role="alert" aria-live="polite"처럼 서로 다른 값을 주면 명시한 aria-live가 우선하므로 의도를 헷갈리게 만듭니다.

aria-atomic, aria-relevant, aria-busy

세 속성은 영역이 바뀌었을 때 무엇을, 언제 읽을지를 조정합니다.

aria-atomic — 바뀐 부분만, 아니면 전체를

aria-atomic가 false(기본값)이면 바뀐 노드만 읽고, true이면 영역 전체를 하나로 다시 읽습니다. “장바구니 3개”에서 숫자만 바뀔 때 숫자만 읽히면 의미를 알 수 없으므로 true를 줍니다.

HTML
<p aria-live="polite" aria-atomic="true">
  장바구니 <span id="cart-count">3</span>개
</p>
<!-- 숫자가 4로 바뀌면 "장바구니 4개"를 통째로 읽음 -->

aria-relevant — 어떤 변화를 알릴지

aria-relevant는 알릴 변화의 종류를 공백으로 나열합니다. 값은 additions(노드 추가), removals(노드 제거), text(텍스트 변경), all(모두)이며, 기본값은 additions text입니다. 대부분은 기본값으로 충분하고, 지원도 고르지 않으므로 바꿀 일이 드뭅니다. 접속자 목록에서 나간 사람을 알리는 경우처럼 제거가 의미 있을 때만 removals를 추가합니다.

aria-busy — 갱신이 끝날 때까지 기다리기

여러 단계에 걸쳐 내용을 채우는 동안 중간 상태가 하나씩 읽히지 않도록, 영역에 aria-busy="true"를 주었다가 갱신이 끝나면 false로 바꿉니다. 보조기술은 true인 동안 변화를 모아 두었다가 알릴 수 있습니다. 끝난 뒤 반드시 false로 돌려 놓아야 합니다.

JavaScript
region.setAttribute('aria-busy', 'true');
renderItems(items);            // 여러 노드를 차례로 추가
region.setAttribute('aria-busy', 'false');

영역은 미리 만들어 두기

브라우저는 이미 접근성 트리에 등록된 라이브 리전의 변화를 감지합니다. 알릴 때 aria-live가 붙은 요소를 새로 만들어 내용과 함께 한 번에 넣으면, 등록과 변경이 동시에 일어나 읽히지 않는 경우가 많습니다. 페이지를 불러올 때 빈 영역을 만들어 두고, 나중에는 내용만 바꿉니다.

비권장알릴 때 영역을 새로 만듦
function notify(msg) {
  var el = document.createElement('div');
  el.setAttribute('role', 'status');
  el.textContent = msg;          // 등록과 동시에 내용 → 놓치기 쉬움
  document.body.appendChild(el);
}
권장미리 만든 영역에 내용만 넣음
<div id="live" role="status" class="sr-only"></div>
<script>
  function notify(msg) {
    document.getElementById('live').textContent = msg;
  }
</script>
  • 같은 문장을 연속으로 넣으면 “변화가 없다”고 판단해 다시 읽지 않을 수 있습니다. 먼저 비운 뒤 잠시(예: 100ms) 후에 넣거나, 문장을 조금 다르게 만듭니다.
  • 화면에 보일 필요가 없는 안내는 시각적으로만 숨긴 영역(.sr-only)에 둡니다. display: none이나 aria-hidden="true"로 숨긴 영역은 읽히지 않습니다.
  • alert는 내용과 함께 새로 삽입해도 읽히도록 대부분의 브라우저가 처리하지만, 일관된 결과를 위해 다른 리전과 같이 미리 두는 편이 안전합니다.

알림 데모

버튼을 누르면 각 라이브 리전에 내용이 들어갑니다. 화면낭독기를 켜고 버튼에 포커스를 둔 채로 눌러 보면, 포커스가 그대로 있어도 메시지가 읽히는 것을 확인할 수 있습니다.

role="status"

role="alert"

role="log"

화면낭독기 안내 예시 (NVDA 한국어 기준)
동작안내 예시
“저장” 버튼을 누름(현재 읽던 내용을 마친 뒤) “저장되었습니다. 오후 3:12”
“전송 실패” 버튼을 누름(즉시) “전송에 실패했습니다. 네트워크 연결을 확인하세요.”
“채팅 수신” 버튼을 누름새로 추가된 메시지 한 줄만 “김민지: 회의 자료 올렸어요”

사례: 검색어 추천 결과 건수 안내

검색창에 글자를 입력하면 추천 검색어 목록이 나타나는 UI에서, 목록을 화면에만 그리면 화면낭독기 사용자는 목록이 생겼다는 사실도, 추천어가 몇 개인지도 알 수 없습니다. 입력하는 동안 포커스는 입력 필드에 있고, 목록은 라이브 리전이 아니기 때문입니다.

해결 방법은 세 가지를 함께 적용하는 것입니다.

  1. 입력 필드를 combobox로 만들고 aria-controls로 목록(listbox)과 연결합니다. 목록이 열리면 aria-expanded를 true로 바꿉니다.
  2. 추천 결과 건수를 알리는 status 영역을 미리 만들어 두고, 결과가 바뀔 때 “추천 검색어 5개”처럼 내용만 넣습니다.
  3. 위·아래 화살표로 추천어를 이동할 때 aria-activedescendant로 현재 항목을 알려 줍니다.
검색어 추천 구조
<label for="q">검색</label>
<input id="q" type="text" role="combobox"
       aria-expanded="false" aria-controls="suggest" aria-autocomplete="list">
<ul id="suggest" role="listbox" aria-label="추천 검색어" hidden></ul>

<!-- 결과 건수 안내용 라이브 리전 (처음부터 비어 있는 상태로 존재) -->
<div id="suggest-count" role="status" class="sr-only"></div>
JavaScript
var timer;
function showSuggestions(items) {
  // ... 목록 항목을 그림 ...
  input.setAttribute('aria-expanded', String(items.length > 0));
  list.hidden = items.length === 0;

  // 타이핑할 때마다 읽히지 않도록 잠시 기다렸다가 건수만 안내
  clearTimeout(timer);
  timer = setTimeout(function () {
    status.textContent = items.length
      ? '추천 검색어 ' + items.length + '개. 위아래 화살표로 이동하세요.'
      : '추천 검색어가 없습니다.';
  }, 500);
}

건수를 알려 주면 사용자는 목록으로 이동할지, 계속 입력할지 판단할 수 있습니다. 결과가 바뀔 때마다 즉시 읽으면 입력한 글자의 음성과 겹치므로 위 예처럼 짧은 지연(디바운스)을 두는 것이 좋습니다. 전체 구현은 콤보박스 패턴을 참고하세요.

흔한 실패

  • 영역을 알릴 때 새로 만듦 — 앞에서 설명한 대로 읽히지 않을 수 있습니다. 미리 만들어 둡니다.
  • 영역을 숨겼다가 보이면서 내용을 넣음 — display: none에서 보이게 바꾸는 것은 노드 추가로 처리되지 않는 경우가 있습니다. 영역은 늘 표시된 상태(필요하면 .sr-only)로 두고 내용만 바꿉니다.
  • 너무 큰 영역을 라이브 리전으로 지정 — 상품 목록 전체나 본문 전체에 aria-live를 주면 작은 변화에도 긴 내용이 읽힙니다. 알릴 문장만 담는 작은 영역을 따로 둡니다.
  • 모든 메시지에 assertive/alert — 읽던 내용이 계속 끊깁니다. 오류처럼 급한 것만 alert로 씁니다.
  • 포커스 이동과 라이브 리전을 동시에 — 오류 요약으로 포커스를 옮기면서 같은 내용을 alert로도 알리면 두 번 읽힙니다. 하나만 고릅니다.
  • 메시지가 금방 사라짐 — 몇 초 뒤 사라지는 토스트는 읽기 전에 지워지거나, 저시력 사용자가 다 읽지 못할 수 있습니다(WCAG 2.2.1 시간 조절). 충분히 오래 두거나 사용자가 닫게 합니다.
  • 타이머·진행률을 매번 읽힘 — 1초마다 바뀌는 값에 polite를 주면 끊임없이 읽힙니다. timer(기본 off)를 쓰고, 중요한 시점(“1분 남음”)에만 별도 영역으로 알립니다.

화면낭독기마다 차이가 있습니다

라이브 리전은 브라우저와 화면낭독기 조합에 따라 동작 차이가 큰 기능입니다. NVDA, JAWS, 센스리더, VoiceOver, TalkBack에서 각각 확인하세요. 테스트 방법은 화면낭독기 사용법과 테스트 도구 문서를 참고하세요.