Velog
99%의 개발자가 모르는 ARIA 속성
ARIA 의 첫 번째 규칙은 *ARIA 를 쓰지 마세요* — 의미론적 HTML 이 항상 우선. 그 위에 *역할(role) / 위젯 속성 / 라이브 영역 / 관계* 네 축으로 정리하면 헷갈리지 않는다.
핵심 요약
ARIA 4축: (1) Role — button, dialog, tab, tabpanel, combobox, tree 등 요소가 무엇인지. 랜드마크(banner/main/navigation/complementary), 문서 구조(article/list/table), 위젯(button/checkbox/...), 라이브 영역(alert/status/log) 으로 분류. (2) 위젯 속성 — aria-checked, aria-pressed, aria-expanded, aria-selected, aria-disabled, aria-required, aria-invalid, aria-valuenow/min/max. 상태가 변하면 반드시 동기화. (3) 라이브 영역 속성 — aria-live="polite|assertive", aria-atomic, aria-busy, aria-relevant — 동적 콘텐츠 알림의 긴급도. (4) 관계 속성 — aria-labelledby, aria-describedby, aria-controls, aria-owns, aria-activedescendant — 요소들을 의미적으로 연결. 가장 중요한 규칙은 변하지 않는다 — <button> 이 있으면 <div role="button"> 을 쓰지 말 것. ARIA 는 HTML 의 대체 가 아니라 보강.
ARIA 를 '장식적 a11y 속성 모음' 이 아니라 '(1) role: 요소가 무엇인가, (2) 위젯 속성: 사용자가 어떻게 상호작용하는가, (3) 라이브 영역: 동적 변화 알림, (4) 관계 속성: 요소 간 연결 — 이 네 축으로 보조 기술과 대화하는 표준 어휘' 로 본다. 첫 번째 규칙은 변치 않는다 — 기본 HTML 요소가 있으면 그것이 우선. ARIA 는 기본 HTML 로는 부족할 때만 의미를 보강한다.
시각적으로 멀쩡한 사이트 가 스크린 리더 사용자에게는 사용 불가 인 경우가 흔하다.
<div onClick>으로 만든 버튼은 키보드 포커스/Enter/Space 가 다 깨진 상태- 모달이 열렸는데
aria-modal="true"가 없어 바깥 콘텐츠가 여전히 읽힌다 - 폼 에러 메시지가
role="alert"없이 조용히 화면에만 뜬다 - 토글 버튼의
aria-pressed가 상태 변화 시 업데이트되지 않아 항상 false 로 읽힌다
전 세계 10억 명 이상이 보조 기술에 의존한다. 또한 키보드 사용성/SEO/구조적 명확성도 함께 좋아지므로, ARIA 를 정확히 쓰는 것은 추가 비용 이 아니라 기본 품질 이다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
First rule of ARIA: *don't use ARIA*
기본 HTML 요소가 있으면 그게 정답. ARIA 는 그 표준 요소로 표현 불가능할 때만 등장한다.
<!-- ❌ 키보드/포커스/Enter/Space 모두 직접 처리해야 함 -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>
<!-- ✅ 모든 게 무료 — 키보드, AT, 포커스 링, 비활성, 폼 제출 -->
<button onclick="submit()">Submit</button>
ARIA 위젯 패턴 은 키보드 인터랙션을 모두 직접 구현 해야 동작한다. 가능하면 표준 요소로 가는 것이 접근성·유지보수·버그 모두 이익.
디자인이 더 자유롭다는 이유로 <button> 대신 <div> 를 쓰는 것. 키보드 사용자/스크린 리더 사용자 모두를 떼어낸다.
상태 속성은 *반드시 동기화* 해야 의미가 있다
aria-expanded, aria-pressed, aria-checked, aria-selected 는 상태가 변할 때 함께 업데이트 되어야 한다. 그렇지 않으면 처음 값 만 영원히 보고된다.
// ❌ 시각만 바뀌고 a11y 트리는 그대로
const [open, setOpen] = useState(false);
<button onClick={() => setOpen(!open)} aria-expanded="false">메뉴</button>
// ✅ 상태 변화에 따라 ARIA 도 동기화
<button onClick={() => setOpen(!open)} aria-expanded={open}>메뉴</button>
자동화 도구(axe, Lighthouse) 는 초기 렌더만 검사하므로 이 버그는 수동 테스트나 단위 테스트 로만 잡힌다.
정적 속성으로 박아두고 클래스만 토글 하는 것. 자동화 도구는 통과하지만 스크린 리더는 영원히 "접힘" 으로 읽는다.
라이브 영역으로 *동적 변화* 를 알려라
폼 에러, 검색 결과 갱신, 토스트 알림 같은 DOM 변화 는 보조 기술에 자동으로 전달되지 않는다. aria-live 또는 그에 해당하는 role 이 필요.
<!-- 폼 검증: 즉시 알림 -->
<span id="email-error" role="alert">올바른 이메일을 입력하세요</span>
<!-- 검색 결과: 적절한 시점에 알림 -->
<div id="results" aria-live="polite" aria-atomic="false">
<span>12개 결과</span>
</div>
<!-- 로딩 표시 -->
<div role="status" aria-busy="true">불러오는 중...</div>
polite 는 사용자가 한가할 때, assertive 는 즉시 끼어든다. 남용하면 알림이 산만해 도리어 사용성을 해친다.
모든 변화 에 aria-live="assertive" 를 박는 것. 사용자는 작업을 끊임없이 방해받는다.
읽는 순서
- 1이론
WAI-ARIA Authoring Practices 의 Combobox, Dialog, Disclosure 패턴 세 가지를 정독.
- 2구현
표준
<details>/<dialog>로 다시 만들 수 있는지 먼저 시도하고, 안 되는 경우에만 ARIA 위젯 패턴으로 구현한다. - 3실무
axe DevTools + 키보드 only 탐색 + 스크린 리더(NVDA/VoiceOver) 로 본인 사이트의 핵심 페이지 3곳을 검증.
- 4설명
팀에 'ARIA 의 첫 번째 규칙과 5가지 흔한 실수' 5분 발표.
면접 연결 질문
[감점 답변] 'aria-label 만 붙이면 된다'. [좋은 답변] (1) <button role="switch" aria-checked={on}> 또는 <input type="checkbox" role="switch"> — 키보드 포커스/Space 키 가 동작하는 표준 요소 위에. (2) aria-checked 를 상태 변화에 따라 동기화. (3) 라벨은 <label> 또는 aria-labelledby. (4) 시각적 상태 외에 aria 라벨이 상태를 분명히 전달 — "on"/"off" 같은 텍스트.
[감점 답변] '역할만 dialog'. [좋은 답변] (1) role="dialog" + aria-modal="true". (2) 제목과 본문을 aria-labelledby/aria-describedby 로 연결. (3) 열릴 때 포커스를 다이얼로그 내부 첫 인터랙티브 요소로 이동. (4) 포커스 트랩 — 닫힐 때까지 외부로 못 나가도록 Tab 순환. (5) ESC 키 닫기 + 닫힐 때 원래 트리거 버튼 으로 포커스 복귀.
자기 점검
'CSS 가 더 쉬워서'. 표준 요소도 appearance: none 으로 자유롭게 스타일 가능하다.
'완전히 같다'. role="status" 는 기본적으로 polite + atomic=true 를 가진 의미적 영역. 명시적으로 의도를 드러내고 싶을 때 role 을 쓴다.