ARIA 접근성 가이드

적용 방법

자주 하는 ARIA 실수

실제 서비스에서 자주 발견되는 ARIA 오용 22가지를 문제·영향·고친 코드로 정리합니다. 잘못된 ARIA는 ARIA가 없는 것보다 나쁜 결과를 만들 수 있습니다.

WebAIM이 매년 상위 100만 개 홈페이지를 자동 검사한 결과(WebAIM Million)를 보면, ARIA를 사용한 페이지가 그렇지 않은 페이지보다 평균적으로 더 많은 접근성 오류를 보입니다. ARIA 자체가 나쁜 것이 아니라, 브라우저의 기본 의미를 덮어쓰거나 상태를 갱신하지 않는 등 잘못 쓰기 쉽기 때문입니다. 아래 실수는 대부분 ARIA 사용 규칙 다섯 가지 중 하나를 어긴 경우입니다.

읽는 방법

각 항목은 문제(무엇이 잘못되었는지), 영향(사용자에게 어떤 일이 생기는지), 비권장·권장 코드 순서로 구성됩니다. 화면낭독기 안내 문구는 NVDA 한국어 기준의 예시이며, 화면낭독기와 브라우저 조합에 따라 다를 수 있습니다.

역할을 잘못 쓰는 실수

1. 네이티브 요소에 같은 역할을 또 지정

문제 <button role="button">, <nav role="navigation">처럼 HTML 요소가 이미 가진 암묵적 역할을 다시 지정합니다.

영향 동작에는 문제가 없지만 코드가 장황해지고, 이후 요소를 바꿀 때(예: <button>을 <a>로) 역할이 잘못 남는 원인이 됩니다. 검사 도구는 “중복 역할”로 경고합니다.

비권장비권장
<button role="button">저장</button>
<nav role="navigation">...</nav>
권장권장
<button>저장</button>
<nav>...</nav>

알려진 예외

Safari(WebKit)는 list-style: none을 적용한 목록의 목록 의미를 제거합니다. 목록 개수 정보가 꼭 필요하면 <ul role="list">를 쓰는 것이 관행으로 허용됩니다.

2. 네이티브 의미를 덮어쓰기

문제 제목 요소에 role="button"을 주는 식으로 기존 의미를 다른 역할로 바꿉니다.

영향 <h2 role="button">은 더 이상 제목이 아닙니다. 화면낭독기의 제목 목록과 제목 단축키(H)로 찾을 수 없게 되고, 버튼의 키보드 동작도 생기지 않습니다(WCAG 1.3.1).

비권장비권장
<h2 role="button" onclick="toggle()">배송 안내</h2>
권장권장
<h2>
  <button type="button" aria-expanded="false" aria-controls="ship">배송 안내</button>
</h2>
<div id="ship" hidden>...</div>

3. 역할만 주고 키보드 동작은 만들지 않음

문제 <div role="button">에 클릭 이벤트만 붙이고 tabindex와 키보드 이벤트를 넣지 않습니다.

영향 화면낭독기는 “버튼”이라고 읽지만 Tab으로 갈 수 없고, Enter·Space로 실행되지 않습니다. 키보드 사용자는 기능을 쓸 수 없습니다(WCAG 2.1.1). ARIA는 역할을 알릴 뿐 동작을 만들어 주지 않습니다.

비권장비권장
<div role="button" onclick="save()">저장</div>
권장권장
<button type="button" onclick="save()">저장</button>
<!-- 꼭 div를 써야 한다면 tabindex="0" + Enter·Space 처리까지 직접 구현 -->

4. 사이트 내비게이션에 menu 역할 사용

문제 상단 메뉴(GNB)에 데스크톱 애플리케이션용 menu/menuitem/menubar를 붙입니다.

영향 menu 역할은 Tab이 아닌 화살표 키로 이동하는 애플리케이션 메뉴를 뜻합니다. 화면낭독기가 메뉴 모드로 바뀌어 링크 목록으로 탐색하던 사용자가 혼란을 겪고, 화살표 키 동작을 구현하지 않으면 아예 조작할 수 없습니다.

비권장비권장
<ul role="menubar">
  <li role="menuitem"><a href="/shop">쇼핑</a></li>
  <li role="menuitem"><a href="/event">이벤트</a></li>
</ul>
권장권장
<nav aria-label="주 메뉴">
  <ul>
    <li><a href="/shop" aria-current="page">쇼핑</a></li>
    <li><a href="/event">이벤트</a></li>
  </ul>
</nav>

하위 메뉴가 있는 내비게이션은 디스클로저 패턴(aria-expanded 버튼)으로 만드는 것이 일반적입니다. menu는 메뉴 버튼 패턴처럼 실제 명령 목록에만 씁니다.

5. 필수 상위·하위 역할 누락

문제 role="tab"을 role="tablist" 밖에 두거나, role="listbox" 안에 role="option" 없이 일반 요소를 넣습니다.

영향 역할 사이의 소유 관계가 깨지면 “3개 중 1번째” 같은 위치 정보가 나오지 않고, 화면낭독기가 위젯을 인식하지 못하거나 항목을 건너뜁니다.

비권장비권장
<div class="tabs">
  <button role="tab" aria-selected="true">소개</button>
  <button role="tab" aria-selected="false">후기</button>
</div>
권장권장
<div role="tablist" aria-label="상품 정보">
  <button role="tab" id="t1" aria-selected="true" aria-controls="p1">소개</button>
  <button role="tab" id="t2" aria-selected="false" aria-controls="p2" tabindex="-1">후기</button>
</div>
<div role="tabpanel" id="p1" aria-labelledby="t1">...</div>

각 역할 문서의 “필수 상위 역할(Required Context)”·“필수 하위 역할(Required Owned Elements)” 항목을 확인하세요(예: tab, listbox).

6. 링크와 버튼을 바꿔 쓰기

문제 동작을 실행하는 데 <a href="#">를 쓰거나, 페이지를 이동하는 데 <button>을 씁니다.

영향 링크는 “이동”, 버튼은 “실행”으로 기대됩니다. href="#" 링크는 주소 표시줄에 #이 붙고 페이지 맨 위로 스크롤되며, Space 키로 실행되지 않습니다. 화면낭독기 링크 목록에 동작용 항목이 섞입니다.

비권장비권장
<a href="#" onclick="openDialog()">쿠폰 받기</a>
권장권장
<button type="button" onclick="openDialog()">쿠폰 받기</button>
<a href="/coupons">쿠폰함으로 이동</a>

이름을 잘못 주는 실수

7. 아이콘만 있는 버튼에 이름이 없음

문제 텍스트 없이 아이콘 글꼴·SVG·배경 이미지만 있는 버튼에 이름을 주지 않습니다.

영향 화면낭독기는 “버튼”만 읽거나 아이콘 글꼴 문자, 파일명을 읽습니다. 사용자는 무엇을 하는 버튼인지 알 수 없습니다(WCAG 4.1.2).

비권장비권장
<button class="btn-close"><i class="icon-x"></i></button>
권장권장
<button type="button" class="btn-close" aria-label="닫기">
  <i class="icon-x" aria-hidden="true"></i>
</button>

8. div·span에 aria-label 지정

문제 역할이 없는 <div>, <span>(generic)에 aria-label을 붙여 정보를 전달하려 합니다.

영향 generic은 이름 금지 역할입니다. 화면낭독기마다 무시하거나 일부만 읽어 결과가 일정하지 않습니다.

비권장비권장
<span class="badge" aria-label="새 알림 3개">3</span>
권장권장
<span class="badge">3<span class="sr-only">개의 새 알림</span></span>

9. 보이는 글자와 다른 aria-label

문제 화면에는 “검색”이라고 보이는데 aria-label="찾기 실행"을 줍니다.

영향 aria-label이 콘텐츠보다 우선하므로 화면낭독기 사용자는 다른 이름을 듣고, 음성 인식 사용자가 “검색 클릭”이라고 말해도 동작하지 않습니다(WCAG 2.5.3).

비권장비권장
<button aria-label="찾기 실행">검색</button>
권장권장
<button>검색</button>

10. 이름에 역할이나 상태를 중복해 넣기

문제 aria-label="닫기 버튼", alt="로고 이미지"처럼 역할을 이름에 넣거나, 토글 버튼 이름을 상태에 따라 바꾸면서 aria-pressed도 함께 씁니다.

영향 “닫기 버튼, 버튼”처럼 중복해 읽힙니다. 이름과 상태가 동시에 바뀌면 “재생 중지, 전환 버튼, 눌림”처럼 의미가 모순됩니다.

비권장비권장
<button aria-label="닫기 버튼">✕</button>
<button aria-pressed="true">음소거 해제</button>
권장권장
<button aria-label="닫기">✕</button>
<button aria-pressed="true">음소거</button>
<!-- 상태는 aria-pressed가, 이름은 고정 -->

11. placeholder를 레이블 대신 사용

문제 입력 필드에 <label> 없이 placeholder만 둡니다.

영향 입력을 시작하면 안내가 사라져 무엇을 입력하던 중인지 잊기 쉽고, 기본 placeholder는 대비가 낮습니다. 일부 보조기술은 placeholder를 이름으로 읽지 않습니다(WCAG 3.3.2).

비권장비권장
<input type="tel" placeholder="휴대폰 번호">
권장권장
<label for="tel">휴대폰 번호</label>
<input type="tel" id="tel" autocomplete="tel" placeholder="010-1234-5678">

상태를 잘못 다루는 실수

12. 상태 값을 갱신하지 않음

문제 aria-expanded="false"를 처음에 넣어 두고, 패널을 열고 닫을 때 값을 바꾸지 않습니다. CSS 클래스만 바꾸는 경우가 대부분입니다.

영향 화면은 펼쳐졌는데 화면낭독기는 계속 “축소됨”이라고 읽습니다. 잘못된 상태 정보는 상태 정보가 없는 것보다 사용자를 더 혼란스럽게 합니다.

비권장비권장 (JavaScript)
btn.addEventListener('click', function () {
  panel.classList.toggle('is-open');
});
권장권장 (JavaScript)
btn.addEventListener('click', function () {
  var open = btn.getAttribute('aria-expanded') !== 'true';
  btn.setAttribute('aria-expanded', String(open));
  panel.hidden = !open;
});

상태를 하나의 출처로 관리하는 방법은 상태 동기화 문서를 참고하세요.

13. aria-disabled만 주고 동작은 막지 않음

문제 aria-disabled="true"를 지정하고 클릭·키보드 이벤트는 그대로 둡니다.

영향 aria-disabled는 “사용 불가”라고 알리기만 합니다. 화면낭독기는 “사용 불가”라고 읽는데 실제로 눌러지면 사용자는 상태를 믿을 수 없습니다.

비권장비권장
<button aria-disabled="true" onclick="submit()">결제하기</button>
권장권장
<button aria-disabled="true" id="pay">결제하기</button>
<script>
  pay.addEventListener('click', function (e) {
    if (pay.getAttribute('aria-disabled') === 'true') { e.preventDefault(); return; }
    submit();
  });
</script>
<!-- 포커스가 필요 없다면 HTML disabled 속성이 가장 간단합니다 -->

14. 상태 속성을 맞지 않는 역할에 사용

문제 탭에 aria-pressed, 체크박스에 aria-selected, 일반 링크에 aria-checked처럼 해당 역할이 지원하지 않는 상태를 씁니다.

영향 지원하지 않는 상태는 보조기술에 전달되지 않거나 엉뚱하게 해석됩니다. 상태마다 쓸 수 있는 역할이 정해져 있습니다(예: aria-selected는 tab, option, gridcell 등).

비권장비권장
<button role="tab" aria-pressed="true">소개</button>
<div role="checkbox" aria-selected="true" tabindex="0">동의</div>
권장권장
<button role="tab" aria-selected="true">소개</button>
<input type="checkbox" id="agree" checked>
<label for="agree">동의</label>

숨김과 포커스 실수

15. 포커스 가능한 요소를 aria-hidden으로 숨김

문제 버튼·링크 자체나 그 조상 요소에 aria-hidden="true"를 줍니다. 슬라이드 캐러셀의 보이지 않는 슬라이드, 닫힌 메뉴에서 자주 생깁니다.

영향 Tab으로 포커스는 가는데 화면낭독기는 아무것도 읽지 않거나 이름 없이 읽습니다. ARIA 규칙 4 위반입니다.

비권장비권장
<div class="slide" aria-hidden="true">
  <a href="/event/1">이벤트 보기</a>
</div>
권장권장
<div class="slide" inert>
  <a href="/event/1">이벤트 보기</a>
</div>
<!-- 또는 보이지 않는 슬라이드는 hidden / display:none -->

숨김 방법별 차이는 콘텐츠 숨기기 문서의 비교 표를 참고하세요.

16. 양수 tabindex 사용

문제 tabindex="1", tabindex="5"처럼 양수를 주어 Tab 순서를 강제로 정합니다.

영향 양수 tabindex 요소가 문서의 다른 모든 요소보다 먼저 포커스를 받아, 화면 순서와 Tab 순서가 어긋납니다. 요소가 추가될 때마다 순서가 깨집니다(WCAG 2.4.3).

비권장비권장
<input tabindex="2" name="id">
<input tabindex="1" name="pw">
권장권장
<!-- DOM 순서를 화면 순서와 맞춤 -->
<input name="id">
<input name="pw">

tabindex 값별 동작은 키보드와 포커스 문서를 참고하세요.

17. 포커스 표시 제거

문제 디자인을 이유로 outline: none을 전역에 적용하고 대체 스타일을 주지 않습니다.

영향 키보드 사용자는 현재 위치를 알 수 없어 페이지를 사용할 수 없습니다(WCAG 2.4.7).

비권장비권장 (CSS)
*:focus { outline: none; }
권장권장 (CSS)
:focus-visible {
  outline: 3px solid #1a5fd0;
  outline-offset: 2px;
}

18. 모달에 aria-modal만 지정하고 포커스는 관리하지 않음

문제 role="dialog" aria-modal="true"를 넣었지만 열 때 포커스를 옮기지 않고, 닫을 때 원래 위치로 돌려주지 않으며, Esc 키도 처리하지 않습니다.

영향 aria-modal은 보조기술에 “바깥 콘텐츠는 비활성”이라고 알릴 뿐, 포커스를 가두지 않습니다. 키보드 사용자는 모달 뒤의 페이지를 헤매고, 닫은 뒤에는 포커스가 문서 맨 앞으로 가 버립니다.

비권장비권장
<div role="dialog" aria-modal="true" class="modal">
  <h2>쿠폰</h2> ...
</div>
<!-- 열기: modal.style.display = 'block' 만 실행 -->
권장권장
<dialog id="coupon" aria-labelledby="coupon-h">
  <h2 id="coupon-h">쿠폰</h2> ...
  <button type="button" onclick="coupon.close()">닫기</button>
</dialog>
<!-- 열기: coupon.showModal()
     → 포커스 이동, 바깥 inert 처리, Esc 닫기, 닫은 뒤 포커스 복귀를 브라우저가 처리 -->

자세한 구현은 대화상자 패턴을 참고하세요.

라이브 리전 실수

19. 라이브 리전을 알릴 때 새로 만들기

문제 알림이 필요한 순간 role="status" 요소를 메시지와 함께 DOM에 새로 추가합니다.

영향 보조기술은 이미 존재하는 라이브 리전의 변화를 감지합니다. 요소와 내용이 동시에 생기면 알림이 누락되는 경우가 많습니다(alert는 비교적 잘 읽히지만 보장되지 않습니다).

비권장비권장 (JavaScript)
function notify(msg) {
  var el = document.createElement('div');
  el.setAttribute('role', 'status');
  el.textContent = msg;
  document.body.appendChild(el);
}
권장권장 (JavaScript)
// 페이지 로드 시 빈 영역을 미리 둠: <div id="msg" role="status"></div>
function notify(msg) {
  document.getElementById('msg').textContent = msg;
}

20. assertive와 alert 남용

문제 저장 완료, 장바구니 추가 같은 일반 안내에도 role="alert"나 aria-live="assertive"를 씁니다. 페이지를 열 때부터 내용이 들어 있는 배너에 role="alert"를 주기도 합니다.

영향 assertive 알림은 사용자가 듣던 내용을 끊습니다. 자주 쓰면 읽던 정보를 놓치고, 정말 급한 오류가 묻힙니다.

비권장비권장
<div role="alert">장바구니에 담았습니다.</div>
권장권장
<div role="status">장바구니에 담았습니다.</div>
<!-- 즉시 대응이 필요한 오류·경고에만 role="alert" -->

자세한 내용은 라이브 리전 문서를 참고하세요.

참조와 구조 실수

21. 없는 ID를 참조하거나 ID가 중복됨

문제 aria-labelledby, aria-describedby, aria-controls가 존재하지 않는 ID를 가리키거나, 컴포넌트를 반복 렌더링하면서 같은 ID가 여러 번 생깁니다.

영향 없는 ID는 무시되어 이름·설명이 사라지고, ID가 중복되면 첫 번째 요소만 참조되어 다른 항목의 이름이 엉뚱하게 계산됩니다.

비권장비권장
<!-- 목록의 모든 카드가 같은 id -->
<h3 id="title">무선 이어폰</h3>
<button aria-describedby="title">담기</button>
<h3 id="title">블루투스 스피커</h3>
<button aria-describedby="title">담기</button>
권장권장
<h3 id="p-101">무선 이어폰</h3>
<button aria-describedby="p-101">담기</button>
<h3 id="p-102">블루투스 스피커</h3>
<button aria-describedby="p-102">담기</button>
<!-- 프레임워크에서는 React useId(), Vue의 고유 키 등으로 ID 생성 -->

22. 제목 역할에 수준을 빠뜨리거나 시각 스타일로만 제목 표현

문제 굵고 큰 <div>로 제목을 만들거나, role="heading"에 aria-level을 빠뜨립니다.

영향 화면낭독기 사용자는 제목 목록으로 페이지 구조를 파악하는데, 시각 스타일만 있는 제목은 목록에 나오지 않습니다. WAI-ARIA 1.2에서 aria-level은 heading의 필수 속성입니다.

비권장비권장
<div class="title-lg">회원 정보</div>
<div role="heading">배송지</div>
권장권장
<h2>회원 정보</h2>
<h3>배송지</h3>
<!-- HTML 제목을 쓸 수 없을 때만: <div role="heading" aria-level="3">배송지</div> -->

제목 계층은 랜드마크와 제목 구조 문서에서 다룹니다.

실수를 찾아내는 방법

위 실수 중 상당수는 자동 검사 도구로 찾을 수 있지만, 상태 갱신·포커스 관리·라이브 리전처럼 동작과 관련된 실수는 직접 조작해 봐야 드러납니다.

실수 유형별 점검 방법
점검 방법잘 찾는 실수
자동 검사(axe DevTools, Lighthouse, WAVE)이름 없는 버튼(7), 이름 금지 역할(8), 없는 ID 참조·중복 ID(21), 필수 하위 역할 누락(5), 포커스 가능한 aria-hidden(15), 지원하지 않는 속성(14)
브라우저 접근성 트리 확인보이는 글자와 다른 이름(9), 역할 덮어쓰기(2), 계산된 이름·상태 값(10, 12)
키보드만으로 조작키보드 동작 누락(3), 양수 tabindex(16), 포커스 표시(17), 모달 포커스 관리(18)
화면낭독기로 듣기상태 미갱신(12), 라이브 리전 누락·남용(19, 20), menu 역할 혼란(4)
음성 인식으로 조작Label in Name 위반(9)

다음 단계

도구 사용법은 테스트 도구, 화면낭독기 조작은 화면낭독기로 확인하기, 전체 점검 항목은 점검 체크리스트를 참고하세요. 코드를 직접 고쳐 보려면 실습실을 이용하세요.