웹 접근성을 위한 WAI-ARIA

아더에러·2026년 3월 27일

프론트엔드

목록 보기
10/15
post-thumbnail

WAI-ARIA란 무엇인가요?

WAI는 W3C에서 주도하는 웹 접근성 국제 표준 활동입니다. ARIA는 그 결과물 중 하나로, HTML만으로 표현하기 어려운 의미와 상태를 보완하기 위한 속성 집합입니다.

한 마디로 정리하면, WAI-ARIA는 "HTML만으로 부족한 접근성을 보완해주는 속성 규칙"입니다.

왜 프론트엔드 개발자가 WAI-ARIA를 알아야 하나요?

웹은 눈으로만 사용하는 것이 아닙니다. 시각 장애가 있는 사용자는 스크린 리더라는 보조 기술을 통해 웹을 탐색합니다. 스크린 리더는 화면을 읽어주는 프로그램으로, 마우스 대신 Tab 키와 방향키로 요소를 이동하면서 각 요소가 무엇인지, 어떤 상태인지를 음성으로 안내합니다.

HTML 기본 태그는 이미 어느 정도 접근성을 지원합니다. <button>을 사용하면 스크린 리더는 자동으로 "버튼"이라고 읽어줍니다.

<button>확인</button>

스크린 리더: "확인, 버튼"

하지만 React 등의 프레임워크로 개발하다 보면 divspan으로 버튼처럼 동작하는 요소를 만드는 경우가 생깁니다.

<div onClick={handleClick}>확인</div>

스크린 리더: "확인" (버튼인지 모름)

이 경우 스크린 리더는 해당 요소가 클릭 가능하다는 사실조차 알 수 없습니다. 키보드 사용자도 Tab으로 포커스가 가지 않아 아예 접근하지 못합니다. 이러한 격차를 메우는 것이 바로 WAI-ARIA입니다.

WAI-ARIA를 사용하면 얻을 수 있는 이점

보조 기술 사용자의 접근성 보장

스크린 리더 사용자뿐 아니라 키보드만으로 웹을 이용하는 사용자에게 영향을 미칩니다.

코드의 의도 명확화

rolearia-* 속성은 개발자 사이에서도 이 요소가 어떤 역할을 하는지 즉시 파악할 수 있게 해줍니다. 마크업만 봐도 UI의 동작 방식이 보입니다.

SEO 간접 기여

의미 있는 구조를 가진 HTML은 검색 엔진 크롤러도 더 잘 이해합니다.

role 속성

role은 이 요소가 어떤 UI 컴포넌트인지 정의하는 속성입니다. 스크린 리더 같은 보조 기기가 요소를 이해할 수 있도록 정보를 제공합니다.

<div role="button"></div>

스크린 리더가 div를 버튼처럼 인식합니다.

role만 붙인다고 끝이 아닙니다. 키보드 사용자도 접근할 수 있도록 tabIndex와 키보드 이벤트도 반드시 함께 구현해야 합니다.

자주 사용하는 role 목록

role의미
button버튼
dialog모달 / 팝업 대화 상자
alert즉시 안내해야 하는 메시지
navigation네비게이션 영역
main주요 콘텐츠 영역
list / listitem리스트 구조
tablist / tab / tabpanel탭 UI 구조
combobox자동완성 검색 입력

더 많은 role은 MDN ARIA Roles 문서에서 확인할 수 있습니다.

aria-* 속성

aria-* 속성은 요소의 상태, 속성, 관계를 설명합니다. aria-속성명="값" 형식으로 작성합니다.

aria-labelledby

다른 요소의 텍스트를 가져다 이름으로 사용합니다. id를 기준으로 연결하며, 여러 개를 동시에 연결할 수 있습니다.

<span id="label">닫기</span>
<button aria-labelledby="label">X</button>

스크린 리더: "닫기, 버튼"

<!-- 여러 요소 연결 -->
<span id="product">무선 이어폰</span>
<span id="price">89,000원</span>
<button aria-labelledby="product price">구매하기</button>

스크린 리더: "무선 이어폰 89,000원, 버튼"

aria-label

화면에 보이지 않는 텍스트를 직접 지정합니다. 아이콘 버튼처럼 시각적 텍스트가 없을 때 사용합니다.

<button aria-label="닫기">X</button>

스크린 리더: "닫기, 버튼" (화면에는 X만 보임)

aria-label은 요소 내부의 텍스트를 덮어씁니다. 따라서 화면의 텍스트와 aria-label이 다르면 시각 사용자와 스크린 리더 사용자가 서로 다른 정보를 받게 됩니다.

<!-- 위험한 사용: 화면에는 '삭제', 스크린 리더는 '닫기' -->
<button aria-label="닫기">삭제</button>

두 속성 모두 요소에 이름을 부여하지만, 사용 상황이 다릅니다.

  • 화면에 이미 텍스트가 있다면 → aria-labelledby
  • 화면에 텍스트가 없을 때만 → aria-label

폼에서는 <label> 태그를 우선 사용하고, 구조상 어려울 때만 aria-labelledby를 사용합니다.

<!-- 폼: <label> 우선 -->
<label for="email">이메일</label>
<input id="email" type="email" />

<!-- 구조상 label이 어려울 때 -->
<span id="email-label">이메일</span>
<input aria-labelledby="email-label" type="email" />

aria-describedby

이름이 "이게 뭐냐"를 정의한다면, aria-describedby는 "이걸 어떻게 써야 하냐"를 설명합니다. 부가 설명, 입력 안내, 에러 메시지 등을 연결할 때 사용합니다.

<label for="pw">비밀번호</label>
<p id="pw-desc">영문, 숫자 포함 8자 이상</p>
<input id="pw" type="password" aria-describedby="pw-desc" />

스크린 리더: "비밀번호, 수정 텍스트, 영문, 숫자 포함 8자 이상"

속성용도예시
aria-labelledby제목 / 이름 / 라벨로그인, 비밀번호, 닫기
aria-describedby부가 설명 / 안내 / 도움말8자 이상 입력하세요, 필수 입력입니다

aria-required

폼 필드가 필수 입력임을 나타냅니다. HTML의 required 속성과 유사하지만, 커스텀 폼 컴포넌트에서 브라우저 기본 유효성 검사를 사용하지 않을 때 접근성을 위해 aria-required를 사용합니다.

<label for="name">이름</label>
<input
  id="name"
  type="text"
  aria-required="true"
/>

스크린 리더: "이름, 수정 텍스트, 필수"

HTML required 속성과 aria-required="true"를 함께 사용하는 것이 권장됩니다. required는 브라우저 유효성 검사를 처리하고, aria-required는 스크린 리더에게 필수 여부를 명시적으로 전달합니다.

<input id="email" type="email" required aria-required="true" />

aria-invalid와 aria-errormessage

폼 입력 값이 유효하지 않을 때 사용합니다. aria-invalid로 현재 입력이 잘못되었음을 알리고, aria-errormessage로 에러 메시지 요소를 연결합니다.

<!-- 에러 없는 상태 -->
<input id="email" type="email" aria-invalid="false" />

<!-- 에러 발생 상태 -->
<input
  id="email"
  type="email"
  aria-invalid="true"
  aria-errormessage="email-error"
  aria-describedby="email-error"
/>
<p id="email-error" role="alert">
  올바른 이메일 형식을 입력해 주세요.
</p>

스크린 리더: "이메일, 수정 텍스트, 잘못된 입력, 올바른 이메일 형식을 입력해 주세요."

aria-errormessagearia-describedby를 함께 사용하는 이유는, 브라우저 및 스크린 리더마다 aria-errormessage 지원 수준이 다르기 때문입니다. 두 속성을 모두 붙이면 더 넓은 환경에서 안정적으로 동작합니다.

에러 메시지 요소에 role="alert"를 추가하면, 포커스 이동 없이도 스크린 리더가 에러를 즉시 읽어줍니다.

aria-hidden

시각적으로는 보이지만 스크린 리더에게는 숨겨야 하는 요소에 사용합니다.

<span aria-hidden="true">★★★★☆</span>
<span>별점 4점</span>

포커스를 받을 수 있는 요소에는 절대 사용하면 안 됩니다. 스크린 리더는 읽지 않는데 키보드 포커스는 이동하기 때문에, 사용자가 무엇을 누르는지 알 수 없는 상황이 됩니다.

<!-- 절대 이렇게 사용하면 안 됩니다 -->
<button aria-hidden="true">닫기</button>

부모 요소에 aria-hidden="true"를 붙이면 자식 요소도 모두 숨겨집니다.

<div aria-hidden="true">
  <button>닫기</button>  <!-- 이 버튼도 스크린 리더에서 사라집니다 -->
</div>

언제 써야 하는지 기준은 간단합니다. 눈에는 보이지만 읽을 필요가 없는 것만 숨깁니다. 장식용 아이콘, 중복 텍스트, 별점 시각화 등이 대표적인 예입니다.

aria-expanded

드롭다운, 아코디언, 메뉴 등 열림/닫힘 상태가 있는 요소에 사용합니다. 트리거 버튼에 붙이며, 상태가 변할 때마다 값을 JavaScript로 업데이트해야 합니다.

<!-- 닫힌 상태 -->
<button aria-expanded="false" aria-controls="menu">
  메뉴 열기
</button>
<ul id="menu" hidden>...</ul>

스크린 리더: "메뉴 열기, 버튼, 닫힘"

<!-- 열린 상태 -->
<button aria-expanded="true" aria-controls="menu">
  메뉴 열기
</button>
<ul id="menu">...</ul>

스크린 리더: "메뉴 열기, 버튼, 열림"

aria-controls

어떤 요소를 제어하는지 연결하는 속성입니다. aria-expanded와 함께 사용하면 스크린 리더가 버튼과 제어 대상 사이의 관계를 이해할 수 있습니다.

<button aria-controls="content" aria-expanded="false">
  더보기
</button>
<div id="content">...</div>

aria-pressed

버튼이 토글 상태를 가질 때 사용합니다. 클릭하면 상태가 바뀌고 유지되는 경우에 적합합니다.

<button aria-pressed="false">좋아요</button>

스크린 리더: "좋아요, 버튼, 안 눌림"

<button aria-pressed="true">좋아요</button>

스크린 리더: "좋아요, 버튼, 눌림"

aria-live

스크린 리더는 기본적으로 포커스가 이동한 요소만 읽습니다. 그러나 토스트 메시지, 에러 알림, 실시간 업데이트처럼 포커스 이동 없이 내용이 바뀌는 경우에는 aria-live를 사용해 스크린 리더에게 변경 사항을 알립니다.

<div aria-live="polite" id="toast"></div>
// JavaScript로 내용을 바꾸면 스크린 리더가 자동으로 읽습니다
document.getElementById('toast').textContent = '장바구니에 추가되었습니다.';

스크린 리더: "장바구니에 추가되었습니다."

aria-live는 두 가지 값을 가집니다.

동작적합한 상황
polite현재 읽는 것이 끝난 후 읽음성공 메시지, 일반 알림, 안내 문구
assertive현재 읽는 것을 중단하고 즉시 읽음에러 메시지, 중요 경고

주의할 점은 aria-live는 텍스트가 변경될 때만 트리거됩니다. 처음부터 내용이 있으면 읽지 않으므로, JavaScript로 내용을 새로 추가하거나 교체해야 합니다.

더 많은 aria-* 속성은 MDN ARIA Attributes 문서에서 확인할 수 있습니다.

스크린 리더가 이름을 읽는 순서

스크린 리더는 요소의 접근 가능한 이름을 계산할 때 다음 순서를 따릅니다. 더 높은 순위가 존재하면 낮은 순위는 완전히 무시됩니다.

aria-labelledby (최우선)

aria-label (두 번째)

요소 내부 텍스트 (세 번째)

별도의 ARIA 없이 가장 자연스러운 방법입니다. 가능하면 이 방법을 우선 사용하는 것이 좋습니다.

<button>닫기</button>

스크린 리더: "닫기, 버튼"

alt, title 속성 (네 번째)

이미지의 alt 속성이나 title 속성도 이름으로 사용됩니다.

<button><img src="close.png" alt="닫기" /></button>

스크린 리더: "닫기, 버튼"

단, 의미 없는 장식용 이미지에는 반드시 alt=""를 사용해야 스크린 리더가 무시합니다.

<img src="decoration.png" alt="" />

WAI-ARIA 사용 시 지켜야 할 규칙

ARIA는 보완용입니다

가장 중요한 원칙입니다. HTML 기본 태그를 사용할 수 있을 때는 반드시 기본 태그를 먼저 사용해야 합니다.

<!-- 잘못된 방법 -->
<div role="button" tabIndex={0} onClick={fn}>확인</div>

<!-- 올바른 방법 -->
<button onClick={fn}>확인</button>

<button>은 키보드 이벤트, 포커스 관리, 접근성을 기본으로 지원합니다. divrole="button"을 붙이면 이 모든 것을 직접 구현해야 합니다. 불필요한 ARIA 사용은 오히려 스크린 리더 사용자에게 혼란을 줄 수 있습니다.

의미 없는 태그에 aria-label만 단독으로 붙이면 안 됩니다

<div aria-label="메뉴">...</div>  <!-- 잘못된 사용 -->

스크린 리더는 요소를 읽을 때 "이름 + 역할" 형태로 안내합니다. 그런데 divspan처럼 의미 없는 태그는 role이 없으므로, aria-label을 붙여도 스크린 리더가 읽어줄 "역할"이 없어 실질적으로 아무 의미가 없거나 예측 불가능한 방식으로 동작합니다.

따라서 aria-label은 반드시 의미 있는 역할을 가진 요소에만 사용해야 합니다.

aria-label 사용 가능aria-label 사용 불가
<button><div> (role 없을 때)
<a><span> (role 없을 때)
<input><p>
<img>단순 레이아웃 div
role="button" 붙은 div

시맨틱 태그(<header>, <nav>, <main> 등)는 이미 자체적인 의미를 가지고 있으므로 aria-label로 덮어쓰는 것은 피해야 합니다. 단, 동일한 시맨틱 태그가 페이지에 여러 개 있어 구분이 필요할 때는 예외적으로 사용할 수 있습니다.

<!-- 예외 허용: 같은 태그가 여러 개일 때 -->
<nav aria-label="주 메뉴">...</nav>
<nav aria-label="푸터 메뉴">...</nav>

잘못된 role은 오히려 해롭습니다

role을 실제 구조와 다르게 붙이면 스크린 리더 사용자에게 혼란을 줍니다.

<!-- 실제 테이블 구조가 아닌데 role="table"을 붙인 경우 -->
<div role="table">...그냥 div 목록...</div>

이 경우 스크린 리더는 테이블로 인식하고 행/열 안내를 시도하지만, 실제 구조가 테이블이 아니므로 사용자는 전혀 다른 정보를 전달받게 됩니다.

마치며

WAI-ARIA는 잘 사용하면 보조 기술 사용자에게 큰 도움이 되지만, 잘못 사용하면 오히려 혼란을 줍니다. 항상 다음 세 가지를 기억하는 것이 좋습니다.

  • HTML 기본 태그로 해결할 수 있다면 ARIA보다 기본 태그를 먼저 사용합니다.
  • 화면을 보지 않고 읽히는 구조를 기준으로 설계합니다. 화면이 멀쩡해도 스크린 리더는 완전히 다르게 해석할 수 있습니다.
  • role이 없는 의미 없는 태그에 aria-label 등의 속성을 단독으로 붙이는 것은 피합니다.

0개의 댓글