iBetter Books
수정

도입 스토리

"접근성 점수가 올라갔어요!" 김개발이 흥분해서 말했습니다. "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 컴포넌트를 접근 가능하게 만들어 봅니다.