웹 접근성 검사·수정 도우미 WCAG 2.1 진단 · 수정 코드 · 작업 지시서
← 접근성 아티클 목록

ARIA는 언제 쓰고 언제 쓰지 말아야 하나

ARIA 실무 가이드 · WCAG 2.1 성공기준 4.1.2 · 2.5.3 · 4.1.3 · 1.3.1 · 2026년 9월

ARIA(Accessible Rich Internet Applications)는 HTML만으로 표현할 수 없는 역할·상태·속성을 보조기기에 알려 주는 속성 묶음입니다. 그런데 접근성 연구·실무자 사이에는 오래된 격언이 있습니다. "ARIA가 없는 페이지가 ARIA가 잘못 쓰인 페이지보다 낫다." ARIA는 말만 바꿀 뿐 동작은 만들어 주지 않기 때문입니다. 이 글은 ARIA를 언제 써야 하고, 무엇을 함께 구현해야 하며, 어디서 흔히 틀리는지를 정리합니다.

1. ARIA의 첫 번째 규칙

W3C의 ARIA 사용 지침 첫 번째 규칙은 이렇게 요약됩니다. 같은 일을 하는 네이티브 HTML 요소가 있으면 ARIA 대신 그것을 쓴다. 네이티브 요소는 역할·이름·키보드 동작·포커스 처리를 브라우저가 공짜로 제공하기 때문입니다.

ARIA로 흉내 낸 버튼 (할 일이 많다)
<div role="button" tabindex="0"
     onclick="save()"
     onkeydown="if(event.key==='Enter'||event.key===' '){save()}">
  저장
</div>
네이티브 버튼 (그냥 된다)
<button type="button" onclick="save()">
  저장
</button>

왼쪽 코드는 role, tabindex, Enter 처리, Space 처리를 모두 직접 해야 하고, 하나라도 빠지면 키보드 사용자가 쓸 수 없는 버튼이 됩니다. 이 도구의 click-not-focusable, role-button-no-tabindex 규칙이 잡는 것이 바로 이 "반쯤 만든 버튼"입니다.

2. ARIA의 세 가지 재료

role은 요소의 의미를 덮어쓰고, state는 시각적으로 바뀐 것을 소리로도 바뀌게 합니다. 화면에서 메뉴가 펼쳐졌다면 aria-expanded="true"도 같이 바뀌어야 합니다. 하나만 바뀌면 화면과 소리가 어긋납니다.

3. 접근 가능한 이름 붙이기: label, labelledby, 보이는 텍스트

버튼·링크·입력창은 접근 가능한 이름(accessible name)이 있어야 합니다(4.1.2). 우선순위는 대체로 다음과 같습니다.

  1. aria-labelledby — 다른 요소의 텍스트를 이름으로 참조
  2. aria-label — 문자열을 직접 지정
  3. 네이티브 방식 — <label for>, 버튼·링크의 내부 텍스트, 이미지의 alt, <caption>
  4. title 속성 — 마지막 수단(모바일·키보드에서 잘 노출되지 않음)
<!-- 아이콘만 있는 버튼: aria-label로 이름 부여 -->
<button type="button" aria-label="장바구니 열기">
  <svg aria-hidden="true" focusable="false">…</svg>
</button>

<!-- 화면에 이미 제목이 있을 때: labelledby로 재사용 -->
<section aria-labelledby="sec-review">
  <h2 id="sec-review">고객 리뷰</h2>
  …
</section>

가능하면 눈에 보이는 텍스트를 이름으로 삼으세요. 음성으로 조작하는 사용자는 화면에 보이는 글자를 그대로 말합니다("검색 누르기"). 화면에는 "검색"이라고 보이는데 aria-label="찾기 버튼"이라면 음성 명령이 통하지 않습니다. WCAG 2.1의 성공기준 2.5.3(이름에 보이는 레이블 포함)이 정확히 이 문제를 다룹니다: 접근 가능한 이름은 보이는 레이블 텍스트를 포함해야 한다.

이름을 참조하는 aria-labelledby, aria-describedby없는 id를 가리키거나, 중복된 id 때문에 엉뚱한 요소를 가리키는 일이 의외로 많습니다. 이 경우 이름이 조용히 비어 버립니다. 이 도구의 aria-ref-broken, duplicate-id 규칙이 이를 잡습니다.

4. 숨기기: aria-hidden, hidden, display:none의 차이

방법화면화면낭독기키보드 포커스
display:none / hidden / visibility:hidden안 보임안 읽힘받지 않음
aria-hidden="true"보임안 읽힘그대로 받음(위험)
시각적으로만 숨김(화면 밖 배치, .visually-hidden)안 보임읽힘포커스 가능한 요소면 받음

aria-hidden="true"장식 아이콘처럼 화면낭독기에서 빼고 싶은 요소에만 씁니다. 그런데 그 안에 링크나 버튼이 들어 있으면, 탭으로는 도달하는데 화면낭독기는 무엇에 포커스가 갔는지 읽어 주지 않는 유령 상태가 됩니다. 팝업이 닫혀 있는데 aria-hidden만 걸고 내부 버튼이 그대로 탭 순서에 남아 있는 경우가 대표적입니다(aria-hidden-focusable). 닫힌 UI는 hidden 또는 inert 속성으로 완전히 빼는 편이 안전합니다.

5. 펼침/접힘(disclosure) 버튼

"더보기", 아코디언, 드롭다운 메뉴 등은 버튼 + aria-expanded 조합이 기본입니다.

<button type="button" id="faq-btn" aria-expanded="false" aria-controls="faq-1">
  배송은 얼마나 걸리나요?
</button>
<div id="faq-1" hidden>평일 오후 2시 이전 주문은 당일 출고됩니다.</div>

<script>
  const btn = document.getElementById('faq-btn');
  const panel = document.getElementById('faq-1');
  btn.addEventListener('click', () => {
    const open = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', String(!open));
    panel.hidden = open;
  });
</script>

네이티브 <details><summary>를 쓰면 이 스크립트 없이도 같은 결과를 얻습니다. 스타일 요구사항이 특별하지 않다면 먼저 고려해 보세요.

6. 모달 대화상자(dialog)

팝업이 열리면 세 가지가 함께 일어나야 합니다.

  1. 포커스 이동 — 열리는 즉시 팝업 안(제목 또는 첫 조작 요소)으로 포커스를 옮깁니다.
  2. 포커스 가두기와 배경 비활성Tab이 팝업 밖으로 나가지 않아야 하고, 뒤 화면은 읽히지도 눌리지도 않아야 합니다.
  3. 닫기와 복귀Esc로 닫히고, 닫히면 팝업을 연 버튼으로 포커스가 돌아갑니다.

가장 쉬운 방법은 네이티브 <dialog>showModal()입니다. 배경 비활성화(inert), Esc 처리, 포커스 이동을 브라우저가 처리해 줍니다.

<dialog id="confirm" aria-labelledby="confirm-title">
  <h2 id="confirm-title">주문을 취소할까요?</h2>
  <button type="button" id="ok">취소하기</button>
  <button type="button" id="close">돌아가기</button>
</dialog>
<script>
  const dlg = document.getElementById('confirm');
  openBtn.addEventListener('click', () => dlg.showModal());
  document.getElementById('close').addEventListener('click', () => dlg.close());
</script>

<div>로 직접 만든 모달이라면 role="dialog", aria-modal="true", 이름(aria-labelledby)을 붙이고 위의 세 가지를 자바스크립트로 직접 구현해야 합니다. 이름표만 붙이고 동작이 없으면 화면낭독기는 팝업 밖 내용을 계속 읽습니다 (dialog-not-marked 규칙이 역할 표시 누락을 잡는 이유입니다).

7. 상태 변화 알리기: 라이브 영역(live region)

"장바구니에 담았습니다", "저장 실패", "검색 결과 12건" 같은 안내는 화면 일부만 바뀌기 때문에 보이는 사람은 알지만 화면낭독기 사용자는 알 수 없습니다. 이럴 때 라이브 영역을 씁니다(4.1.3 상태 메시지).

<!-- 영역은 페이지가 로드될 때 이미 DOM에 있어야 한다 -->
<p id="cart-status" role="status"></p>

<script>
  addBtn.addEventListener('click', () => {
    // 나중에 텍스트만 바꿔 넣는다
    document.getElementById('cart-status').textContent = '장바구니에 담았습니다.';
  });
</script>

흔한 실수는 알림 영역 자체를 동적으로 만들어 붙이는 것입니다. 대부분의 보조기기는 이미 존재하던 영역 안의 변화를 감지하므로, 영역이 내용과 함께 새로 생기면 읽지 않을 수 있습니다. 이 사이트의 검사 진행 상태(role="status")도 같은 방식으로 만들었습니다.

8. 자주 보는 ARIA 안티패턴

패턴문제대안
<a role="button">에 href 없음링크인지 버튼인지 모호하고 키보드 동작 불완전동작이면 <button>, 이동이면 <a href>
<div aria-label="…">(role 없음)역할 없는 요소의 이름은 대부분 읽히지 않음의미 있는 요소·role을 함께 부여하거나 보이는 텍스트 사용
role 오타 (role="buton")알 수 없는 role은 무시되거나 잘못 전달유효한 role 값 확인(aria-role-invalid)
보이는 글자와 다른 aria-label음성 조작 실패(2.5.3)보이는 텍스트를 이름의 앞부분에 포함
모든 요소에 습관적으로 role 부착네이티브 의미를 덮어써 오히려 오동작먼저 시맨틱 HTML 요소 사용
탭·메뉴에 role만 부여하고 방향키 미구현role은 "방향키로 움직일 것"이라는 약속인데 지켜지지 않음WAI-ARIA 저작 관행(APG)의 키보드 규칙 따르기

9. ARIA를 다 쓴 뒤 확인할 것

  1. 개발자도구 접근성 트리에서 역할과 이름이 기대와 같은지 본다.
  2. 마우스를 치우고 키보드만으로 모든 동작을 해 본다. (키보드 접근성 글 참고)
  3. NVDA·VoiceOver로 상태 변화(펼침, 오류, 안내)가 실제로 들리는지 확인한다. (30분 입문 참고)
  4. W3C의 ARIA 저작 관행 가이드(APG)에서 해당 패턴(탭, 콤보박스, 메뉴 등)의 키보드 규칙과 예제를 대조한다.
자동 검사는 "id 참조가 깨졌는가, role 이름이 유효한가, 숨겨진 요소 안에 포커스 가능 요소가 있는가" 같은 문법적 오류를 잡습니다. ARIA가 의미상 맞게 쓰였는지, 상태가 실제와 동기화되는지는 사람이 조작해 봐야 알 수 있습니다.

이 도구는 ARIA 관련 6개 규칙(참조 깨짐, 잘못된 role, aria-hidden 내 포커스, role=button 포커스 누락, 모달 표시 누락, 중복 id)을 규칙 사전의 설명과 함께 점검합니다.

관련 글
· 키보드만으로 쓸 수 있는 사이트 만들기
· 접근 가능한 입력 양식 만들기
· 제목·랜드마크·페이지 제목으로 문서 구조 잡기
· 검사 규칙 사전 — 35개 검사 항목 해설
문의: dkdnj123@gmail.com