Ray Book
웹 접근성

스크린 리더와 ARIA

스크린 리더는 페이지를 어떻게 읽는가? 랜드마크, role, aria-label, aria-live를 시각화합니다

accessibilityscreen-readerarialandmarksaria-live

스크린 리더는 페이지를 어떻게 읽는가

눈이 보이지 않는 사용자에게 웹 페이지는 "화면"이 아닙니다. 음성으로 전달되는 텍스트 스트림입니다. 스크린 리더 (VoiceOver, NVDA, JAWS 등) 는 브라우저의 접근성 트리 를 순회하면서 각 요소의 역할, 이름, 상태를 음성으로 읽어줍니다.

시각 사용자가 페이지를 "스캔"하듯이, 스크린 리더 사용자는 랜드마크 , 헤딩 , 링크 목록 같은 단축키를 사용해 페이지를 탐색합니다. 이 탐색이 효과적으로 동작하려면, HTML이 올바른 의미 구조를 가지고 있어야 합니다.

랜드마크: 페이지의 지도

랜드마크는 페이지의 주요 영역을 표시하는 접근성 역할입니다. 스크린 리더 사용자는 랜드마크 단축키로 영역 간을 빠르게 이동할 수 있습니다.

HTML 요소암묵적 랜드마크 역할의미
header (최상위)banner사이트 헤더, 로고, 전역 네비게이션
navnavigation네비게이션 링크 그룹
mainmain페이지의 주요 콘텐츠 (하나만)
footer (최상위)contentinfo사이트 푸터, 저작권, 연락처
asidecomplementary보조 콘텐츠, 사이드바
form (name 있을 때)form이름이 지정된 폼 영역
section (name 있을 때)region이름이 지정된 콘텐츠 영역

주의할 점이 있습니다. headerfooter최상위에 있을 때만 banner와 contentinfo 역할을 가집니다. article이나 section 안에 중첩된 header/footer는 랜드마크가 아닙니다.

<body>
  <header>...</header>     <!-- banner 랜드마크 ✓ -->
  <nav>...</nav>           <!-- navigation 랜드마크 ✓ -->
  <main>                   <!-- main 랜드마크 ✓ -->
    <article>
      <header>...</header> <!-- 랜드마크 아님 ✗ -->
      <footer>...</footer> <!-- 랜드마크 아님 ✗ -->
    </article>
  </main>
  <footer>...</footer>     <!-- contentinfo 랜드마크 ✓ -->
</body>

같은 유형의 랜드마크가 여러 개일 때는 aria-label로 구분해야 합니다. 예를 들어 네비게이션이 두 개라면:

<nav aria-label="메인 메뉴">...</nav>
<nav aria-label="푸터 링크">...</nav>

레이블을 지정할 때 "navigation"이라는 단어를 포함하지 마세요. 스크린 리더가 "메인 메뉴, navigation"이라고 읽어야지, "메인 메뉴 네비게이션, navigation"이라고 중복되면 안 됩니다.

시각화: 스크린 리더 탐색 방식

아래 시각화에서 스크린 리더가 페이지를 탐색하는 다섯 가지 방식을 확인해 보세요. 각 단계에서 스크린 리더가 무엇을 읽어주는지 (announcement) 에 주목합니다.

Landmarks랜드마크 탐색
<header>role=banner
<nav>role=navigation
<main>role=main
<footer>role=contentinfo
Screen Reader Announcement
"banner" → "navigation" → "main" → "contentinfo"
스크린 리더는 랜드마크 단위로 페이지를 탐색할 수 있습니다. header는 banner, nav는 navigation, main은 main, footer는 contentinfo 역할을 자동으로 가집니다. 사용자는 단축키 하나로 원하는 영역에 바로 접근합니다.

ARIA의 첫 번째 규칙, 다시 한 번

이전 글에서 강조했지만 다시 반복할 가치가 있습니다.

네이티브 HTML 요소로 원하는 시맨틱과 동작을 구현할 수 있다면, ARIA를 사용하지 마라.

ARIA는 HTML의 접근성 정보를 덮어씁니다 . role="button"div에 추가하면 접근성 트리에서 버튼으로 인식되지만, 키보드 동작 (Enter/Space) 이나 폼 제출 같은 네이티브 기능은 따라오지 않습니다. ARIA는 "정보"만 바꿀 뿐, "동작"은 바꾸지 않습니다.

이 구분이 중요합니다.

  • 시맨틱 HTML = 정보 + 동작 + 접근성 (한 번에 해결)
  • div + ARIA = 정보만 변경 (동작은 직접 구현해야 함)

필수 ARIA 속성들

시맨틱 HTML만으로 해결할 수 없는 경우, ARIA가 필요합니다. 가장 자주 사용하는 속성들을 정리합니다.

aria-label과 aria-labelledby

요소에 접근 가능한 이름을 부여합니다.

<!-- aria-label: 텍스트를 직접 지정 -->
<button aria-label="닫기">
  <svg>...</svg>  <!-- 아이콘만 있는 버튼 -->
</button>

<!-- aria-labelledby: 다른 요소의 텍스트를 참조 -->
<h2 id="section-title">검색 결과</h2>
<section aria-labelledby="section-title">
  ...
</section>

aria-labelledbyaria-label보다 우선순위가 높습니다. 화면에 이미 텍스트가 있다면 aria-labelledby로 참조하는 것이 좋습니다, 텍스트가 변경될 때 자동으로 반영되기 때문입니다.

aria-hidden

접근성 트리에서 요소를 숨깁니다. 장식용 아이콘이나 시각적으로만 의미 있는 요소에 사용합니다.

<!-- 장식용 아이콘 숨기기 -->
<button>
  <span aria-hidden="true">★</span>
  즐겨찾기에 추가
</button>

주의: aria-hidden="true"를 포커스 가능한 요소나 그 부모에 적용하면, 스크린 리더가 요소를 무시하지만 키보드로는 포커스할 수 있는 상태가 됩니다. 이것은 WCAG 위반입니다. 포커스 가능한 요소를 숨기려면 aria-hidden과 함께 tabindex="-1"을 사용하거나, inert 속성을 사용하세요.

aria-live

동적으로 변경되는 콘텐츠를 스크린 리더에 알립니다.

<!-- polite: 현재 읽기가 끝난 후 알림 -->
<div aria-live="polite" role="status">
  저장되었습니다
</div>

<!-- assertive: 즉시 알림 (긴급한 경우만) -->
<div aria-live="assertive" role="alert">
  세션이 만료되었습니다. 다시 로그인해 주세요.
</div>

aria-live 영역은 페이지 로드 시점에 DOM에 존재해야 합니다. JavaScript로 나중에 영역 자체를 추가하면 일부 스크린 리더가 인식하지 못합니다. 빈 상태로 미리 마크업해 두고, 내용만 동적으로 변경하는 것이 안전합니다.

동작사용 사례
polite현재 읽기 완료 후 알림저장 확인, 검색 결과 수 변경
assertive즉시 알림 (현재 읽기 중단)에러 메시지, 세션 만료 경고
off알리지 않음 (기본값)-

aria-expanded

토글 가능한 요소의 열림/닫힘 상태를 전달합니다.

<button aria-expanded="false" aria-controls="faq-1">
  자주 묻는 질문 1
</button>
<div id="faq-1" hidden>
  답변 내용입니다...
</div>

JavaScript에서 토글할 때 aria-expanded 값을 "true"/"false"로 변경합니다. 스크린 리더는 "자주 묻는 질문 1, button, collapsed" 또는 "expanded"로 상태를 읽어줍니다.

aria-controls

aria-controls는 버튼이 제어하는 대상 요소의 ID를 가리킵니다. 일부 스크린 리더 (특히 JAWS) 에서 "이 버튼이 어떤 영역을 제어하는지" 알려줍니다. aria-expanded와 함께 사용하는 것이 일반적입니다.

자주 하는 실수

빈 링크와 버튼

<!-- Bad: 스크린 리더가 "link"라고만 읽음 -->
<a href="/profile">
  <img src="avatar.png">
</a>

<!-- Good: 접근 가능한 이름 제공 -->
<a href="/profile">
  <img src="avatar.png" alt="내 프로필">
</a>

의미 없는 alt 텍스트

<!-- Bad: 파일명을 그대로 사용 -->
<img src="chart.png" alt="chart.png">

<!-- Bad: 장식용 이미지에 설명 추가 -->
<img src="divider.png" alt="구분선 이미지">

<!-- Good: 의미 전달 -->
<img src="chart.png" alt="2025년 매출 추이: 1분기 대비 4분기 23% 증가">

<!-- Good: 장식용 이미지는 빈 alt -->
<img src="divider.png" alt="">

role의 잘못된 사용

<!-- Bad: div에 role만 추가하고 키보드 지원 누락 -->
<div role="button">삭제</div>

<!-- Bad: 이미 의미가 있는 요소에 중복 역할 -->
<button role="button">저장</button>

<!-- Bad: role로 시맨틱 HTML을 대체 -->
<div role="navigation">
  <div role="link" onclick="goto('/')">Home</div>
</div>

<!-- Good: 시맨틱 HTML 사용 -->
<nav>
  <a href="/">Home</a>
</nav>

aria-live 남용

<!-- Bad: 너무 많은 영역을 assertive로 설정 -->
<div aria-live="assertive">
  검색 결과 42개  <!-- 매 키 입력마다 읽어줌 = 매우 시끄러움 -->
</div>

<!-- Good: polite로 설정하고, 디바운스 적용 -->
<div aria-live="polite" role="status">
  검색 결과 42개
</div>

체크리스트

  • 페이지에 올바른 랜드마크 구조가 있는가? (header, nav, main, footer)
  • 같은 유형의 랜드마크가 여러 개일 때 aria-label로 구분했는가?
  • 헤딩 레벨이 건너뛰지 않고 계층 구조를 유지하는가?
  • 아이콘만 있는 버튼/링크에 aria-label이 있는가?
  • 장식용 이미지와 아이콘에 aria-hidden="true" 또는 alt=""를 사용했는가?
  • 동적 콘텐츠 변경에 aria-live를 사용하고 있는가?
  • aria-live 영역이 페이지 로드 시 DOM에 존재하는가?
  • 토글 요소에 aria-expanded를 사용하고 있는가?
  • VoiceOver/NVDA로 실제 테스트해 보았는가?

다음 글에서는 색상, 대비, 모션 을 다룹니다. 색각 이상 사용자를 위한 대비 기준, 색상만으로 정보를 전달하지 않는 방법, 그리고 prefers-reduced-motion을 시각화합니다.