탄생 배경
웹의 초기 — 문서 중심
웹은 처음부터 문서를 공유하기 위한 매체로 설계되었습니다. HTML 태그들은 제목, 단락, 링크, 이미지 같은 문서 구성 요소를 표현하는 데 최적화되어 있었고, 스크린 리더 같은 보조 기술도 이 구조를 기반으로 콘텐츠를 해석했습니다.
h1 = 제목이다
a = 링크다
img = 이미지다
태그 자체가 의미를 가지고 있었기 때문에 별도의 보완이 필요 없었습니다.
Ajax와 RIA의 등장 — 문제의 시작
2000년대 중반, Ajax(Asynchronous JavaScript and XML) 가 등장하면서 웹이 바뀌었습니다. 페이지 전체를 새로고침하지 않고 일부만 동적으로 업데이트하는 방식이 가능해졌고, 웹은 "문서"에서 "애플리케이션"으로 진화하기 시작했습니다.
Gmail(2004), Google Maps(2005) 같은 서비스가 대표적입니다. 이런 서비스들은 데스크탑 애플리케이션 수준의 UI를 웹에서 구현했습니다 — 드래그, 드롭다운, 탭, 모달, 자동완성 등.
문제는 이 모든 UI가 div, span 같은 의미 없는 태그로 만들어졌다는 것입니다.
<!-- 시각적으로는 완벽한 드롭다운이지만 -->
<div class="dropdown">
<div class="dropdown-toggle">선택하세요</div>
<ul class="dropdown-menu">
<li>옵션 1</li>
<li>옵션 2</li>
</ul>
</div>
스크린 리더 입장에서는 그냥 텍스트 덩어리일 뿐이었습니다. 시각 장애인은 드롭다운이 열렸는지, 어떤 항목이 선택됐는지 전혀 알 수 없었습니다.
WAI-ARIA의 탄생
이 문제를 해결하기 위해 W3C 산하의 WAI(Web Accessibility Initiative) 가 2008년 ARIA 1.0 명세를 발표했습니다.
“동적 웹 콘텐츠와 커스텀 UI 위젯에 접근성 의미를 부여하는 표준”
핵심 아이디어는 단순했습니다. HTML 태그가 의미를 표현하지 못하면, 속성으로 의미를 보충하자.
<div class="dropdown" role="listbox" aria-expanded="false">
...
</div>
이후 2014년 HTML5가 등장하면서 <nav>, <main>, <article> 같은 시맨틱 태그가 추가됐고, 많은 케이스가 HTML만으로도 해결 가능해졌습니다. 하지만 커스텀 위젯, SPA의 동적 업데이트, 복잡한 인터랙션은 여전히 ARIA 없이는 접근성을 보장하기 어렵습니다.
ARIA란
ARIA(Accessible Rich Internet Applications) 는 HTML 요소에 추가 속성을 부여해 보조 기술(스크린 리더, 점자 디스플레이 등)이 UI를 올바르게 해석할 수 있도록 의미 정보를 제공하는 W3C 표준입니다.
ARIA는 세 가지 정보를 전달합니다.
- 이 요소가 무엇인가 (role) — 버튼인가, 다이얼로그인가, 탭인가
- 이 요소의 현재 상태가 무엇인가 (state) — 열려 있는가, 선택됐는가, 비활성인가
- 이 요소가 다른 요소와 어떤 관계인가 (property) — 어떤 레이블과 연결됐는가, 어떤 요소를 제어하는가
ARIA 속성은 시각적 표현에는 영향을 주지 않습니다. 오직 보조 기술이 읽는 접근성 트리(Accessibility Tree) 에만 영향을 줍니다.
ARIA의 3가지 구성 요소
1. Role (역할)
요소가 어떤 UI 컴포넌트인지 정의합니다.
<div role="button">클릭</div>
<div role="dialog">모달</div>
<div role="tablist">탭 목록</div>
<div role="tab">탭 1</div>
<div role="tabpanel">탭 내용</div>
<div role="alert">오류 메시지</div>
<div role="progressbar">로딩 중</div>
Role은 크게 4가지로 분류됩니다.
| 분류 | 설명 | 예시 |
|---|---|---|
| Widget | 인터랙티브 UI 요소 | button, checkbox, tab, slider |
| Structure | 콘텐츠 구조 | list, table, row, cell |
| Landmark | 페이지 주요 영역 | banner, navigation, main, contentinfo |
| Live Region | 동적으로 업데이트되는 영역 | alert, log, status |
2. Property (속성)
요소의 성격이나 관계를 나타냅니다. 상태와 달리 잘 변하지 않는 정적인 정보입니다.
<input aria-label="검색어">
<input aria-required="true">
<button aria-haspopup="true">메뉴 열기</button>
3. State (상태)
요소의 현재 상태를 나타냅니다. JS로 동적으로 변경되는 경우가 많습니다.
<button aria-pressed="false">좋아요</button>
<div aria-expanded="true">드롭다운</div>
<input aria-invalid="true">
<li aria-selected="true">옵션 1</li>
주요 속성 정리
레이블과 설명
| 속성 | 역할 |
|---|---|
aria-label |
요소에 직접 레이블 텍스트를 지정 |
aria-labelledby |
다른 요소의 id를 참조해 레이블로 사용 |
aria-describedby |
요소에 대한 추가 설명을 다른 요소와 연결 |
<!-- aria-label: 보이는 텍스트 없을 때 -->
<button aria-label="검색">
<svg>...</svg>
</button>
<!-- aria-labelledby: 보이는 텍스트를 레이블로 활용 -->
<h2 id="modal-title">비밀번호 변경</h2>
<div role="dialog" aria-labelledby="modal-title">...</div>
<!-- aria-describedby: 추가 설명 연결 -->
<input type="password" aria-describedby="pw-hint">
<p id="pw-hint">8자 이상, 특수문자 포함</p>
상태
| 속성 | 값 | 설명 |
|---|---|---|
aria-expanded |
true / false | 펼쳐짐 여부 (드롭다운, 아코디언) |
aria-pressed |
true / false / mixed | 토글 버튼 눌림 상태 |
aria-checked |
true / false / mixed | 체크 상태 (커스텀 체크박스) |
aria-selected |
true / false | 선택 상태 (탭, 리스트) |
aria-disabled |
true / false | 비활성 상태 |
aria-invalid |
true / false / grammar / spelling | 유효성 검사 실패 |
aria-hidden |
true / false | 보조 기술에서 숨김 |
관계
| 속성 | 설명 |
|---|---|
aria-controls |
이 요소가 제어하는 요소의 id |
aria-owns |
DOM 구조와 무관하게 부모-자식 관계 정의 |
aria-activedescendant |
포커스 대신 활성 자식 요소 지정 |
<button aria-controls="menu-list" aria-expanded="false">메뉴</button>
<ul id="menu-list">...</ul>
라이브 리전
페이지의 일부가 동적으로 업데이트될 때 스크린 리더에 알리는 속성입니다. Ajax로 콘텐츠가 바뀌어도 스크린 리더는 알 수 없기 때문에 이 속성이 필요합니다.
<!-- 중요한 메시지 즉시 읽기 -->
<div role="alert">로그인에 실패했습니다.</div>
<!-- 덜 긴급한 상태 알림 -->
<div aria-live="polite" aria-atomic="true">
저장 중...
</div>
| 값 | 동작 |
|---|---|
aria-live="polite" |
현재 읽는 것이 끝난 후 알림 |
aria-live="assertive" |
즉시 끊고 알림 (긴급한 오류 등) |
aria-live="off" |
알림 안 함 (기본값) |