도입 스토리
"접근성 점수가 올라갔어요!" 김개발이 흥분해서 말했습니다. "ARIA 속성을 잔뜩 추가했더니 axe 오류가 줄었어요."
박멘토가 NVDA로 페이지를 열었습니다.
"버튼. 버튼. 버튼. 버튼. 버튼. 버튼. 텍스트 입력. 버튼. 버튼."
"이게 더 좋아진 건가요?"
김개발이 당황했습니다. "무슨 일이에요?"
"role="button"을 span 태그에 붙였는데, 이미 <button> 태그를 감싸고 있어요. 중첩 버튼이 된 거예요. 그리고 이 링크에는 aria-label이 있는데 안에 텍스트도 있어서 '홈으로 이동. 홈'이라고 두 번 읽혀요."
"ARIA를 쓰면 쓸수록 좋은 게 아닌가요?"
"No ARIA is better than bad ARIA — ARIA를 안 쓰는 게 잘못 쓰는 것보다 낫습니다."
핵심 개념 설명
ARIA 5가지 규칙
W3C는 ARIA 사용을 위한 5가지 규칙을 제시합니다.
규칙 1 — 시맨틱 요소를 먼저 사용하라.
ARIA role을 추가하기 전에, 같은 의미를 가진 HTML 요소가 있는지 확인합니다.
<!-- 하지 말 것: div에 ARIA role --><div role="button" tabindex="0">제출</div><!-- 올바름: 시맨틱 요소 사용 --><button type="submit">제출</button>
규칙 2 — 시맨틱을 변경하지 마라.
<!-- 하지 말 것: 버튼의 의미를 변경 --><button role="heading">상품명</button><!-- 올바름: 각 요소를 목적에 맞게 --><h3>상품명</h3><button>구매하기</button>
규칙 3 — 모든 ARIA 컨트롤은 키보드로 접근 가능해야 한다.
<!-- 하지 말 것: role="button"인데 keyboard 접근 불가 --><span role="button" onclick="act()">클릭</span><!-- 올바름: tabindex와 키 이벤트 추가 --><span role="button" tabindex="0" onclick="act()" onkeydown="if(event.key==='Enter'||event.key===' ')act()"> 클릭</span>
규칙 4 — 포커스 가능한 요소를 숨기지 마라.
<!-- 하지 말 것: 포커스 가능 요소를 aria-hidden으로 숨김 --><a href="/home" aria-hidden="true">홈</a><!-- 올바름: 시각적으로 숨기면서 포커스 유지 또는 완전히 제거 --><a href="/home" class="sr-only">홈</a><!-- 또는 tabindex="-1"과 aria-hidden 함께 --><a href="/home" aria-hidden="true" tabindex="-1">홈</a>
규칙 5 — 인터랙티브 요소에는 접근 가능한 이름이 있어야 한다.
<!-- 하지 말 것: 이름 없는 버튼 --><button> <svg><!-- 아이콘 --></svg></button><!-- 올바름: aria-label로 이름 제공 --><button aria-label="검색"> <svg aria-hidden="true"><!-- 아이콘 --></svg></button>
자주 하는 ARIA 실수
실수 1 — aria-label과 텍스트 중복:
<!-- 잘못된 예: "홈으로 이동. 홈"이라고 두 번 읽힘 --><a href="/" aria-label="홈으로 이동">홈</a><!-- 올바른 예: 텍스트가 충분하면 aria-label 불필요 --><a href="/">홈</a><!-- 또는 텍스트를 시각적으로 숨기고 aria-label 사용 --><a href="/" aria-label="홈으로 이동"> <svg aria-hidden="true"><!-- 집 아이콘 --></svg></a>
실수 2 — aria-hidden으로 포커스 가능 요소 숨기기:
<!-- 잘못된 예: Tab은 이동되지만 스크린리더는 무시 --><button aria-hidden="true">저장</button><!-- 올바른 예: 둘 다 숨기거나 둘 다 보이기 --><button aria-hidden="true" tabindex="-1">저장</button><!-- 또는 --><button>저장</button>
실수 3 — 잘못된 role 사용:
<!-- 잘못된 예: img에 link role (이미지가 링크라면 <a>로 감싸야) --><img src="logo.png" role="link" onclick="goHome()"><!-- 올바른 예 --><a href="/"> <img src="logo.png" alt="홈으로 이동"></a>
ARIA 접근 가능한 이름 계산 순서
스크린리더가 요소의 이름을 결정하는 순서입니다.
| 우선순위 | 방법 | 예시 |
|---|---|---|
| 1 | aria-labelledby |
다른 요소의 텍스트 참조 |
| 2 | aria-label |
직접 이름 지정 |
| 3 | 연결된 <label> |
<label for="id"> |
| 4 | title 속성 |
마우스 hover 팁 |
| 5 | 요소 내용 | 버튼 텍스트, 링크 텍스트 |
| 6 | alt 속성 |
이미지의 경우 |
<!-- aria-labelledby가 가장 높은 우선순위 --><h2 id="dialog-title">로그인</h2><dialog aria-labelledby="dialog-title"> <!-- 스크린리더: "로그인 대화상자" --></dialog><!-- aria-label은 내부 텍스트보다 우선 --><button aria-label="검색">Search</button><!-- 스크린리더: "검색 버튼" (aria-label 우선) -->
단계별 실습
따라하기: ARIA 감사하기
axe DevTools에서 ARIA 관련 오류를 확인합니다.
주요 ARIA 규칙:
aria-allowed-attr: 해당 role에 허용되지 않는 aria 속성aria-required-children: 필수 자식 요소 누락aria-required-parent: 필수 부모 요소 누락aria-valid-attr: 존재하지 않는 aria 속성
변형하기: ARIA 제거하기
불필요한 ARIA를 제거하고 시맨틱 HTML로 대체합니다.
// ARIA 속성이 과도하게 사용된 요소 찾기const ariaElements = document.querySelectorAll('[role], [aria-label], [aria-labelledby]');ariaElements.forEach(el => { console.log(el.tagName, el.getAttribute('role'), el.outerHTML.substring(0, 80));});
응용하기: 커스텀 셀렉트 컴포넌트 감사
커스텀 드롭다운 컴포넌트가 올바른 ARIA 패턴을 사용하는지 확인합니다.
<!-- 올바른 커스텀 셀렉트 예시 (Listbox 패턴) --><div class="select-wrapper"> <button id="select-btn" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="select-label select-btn" > <span id="selected-option">옵션 선택</span> <span aria-hidden="true">▼</span> </button> <ul role="listbox" aria-labelledby="select-label" hidden > <li role="option" aria-selected="false">옵션 1</li> <li role="option" aria-selected="true">옵션 2</li> <li role="option" aria-selected="false">옵션 3</li> </ul></div>
정리와 확인
핵심 내용 요약
- "No ARIA is better than bad ARIA": 잘못된 ARIA가 접근성을 오히려 해침
- ARIA 5규칙: 시맨틱 우선 → 의미 변경 금지 → 키보드 접근 → hidden 주의 → 이름 필수
- 이름 계산 순서:
aria-labelledby>aria-label><label>> 내용 aria-hidden주의: 포커스 가능 요소와 함께 사용하면 안 됨
확인 문제
문제 1. <button aria-label="닫기">X</button>에서 스크린리더는 무엇을 읽는가?
"닫기 버튼" — aria-label이 내부 텍스트("X")보다 우선순위가 높습니다.
문제 2. <img src="close.svg" aria-hidden="true">를 버튼 안에 넣는 이유는?
버튼 안의 아이콘이 aria-hidden="true"이면 스크린리더가 이미지를 무시합니다.
버튼의 aria-label이나 내부 텍스트(sr-only)로 이름을 제공하면
아이콘이 중복 읽히지 않아 깔끔한 경험이 됩니다.
PART 05를 마쳤습니다. 이제 WCAG 4대 원칙 전체를 배웠습니다. 다음 PART에서는 실제 웹 컴포넌트에 이 원칙들을 적용하는 실습을 시작합니다. WAI-ARIA 디자인 패턴을 따라 버튼, 모달, 탭, 드롭다운 등 자주 쓰는 UI 컴포넌트를 접근 가능하게 만들어 봅니다.