ARIA 접근성 가이드

적용 방법

모바일 웹과 터치 환경

모바일 화면낭독기(VoiceOver, TalkBack)에서 ARIA가 동작하는 방식의 차이, 터치 제스처 대안, 터치 대상 크기, 화면 방향, 그리고 네이티브 앱 접근성 API와 ARIA 개념의 대응을 정리합니다.

모바일 화면낭독기의 동작 방식

모바일 기기에는 운영체제에 화면낭독기가 내장되어 있습니다. iOS·iPadOS는 VoiceOver, Android는 TalkBack(제조사에 따라 삼성 기기는 TalkBack 기반의 음성 지원)이 기본입니다. 웹 페이지는 iOS에서는 Safari 등 WebKit 엔진, Android에서는 주로 Chrome이 접근성 트리를 만들어 화면낭독기에 전달합니다.

데스크톱과 가장 큰 차이는 조작 방식입니다.

  • 스와이프 탐색: 오른쪽·왼쪽으로 쓸어 넘기면 다음·이전 요소로 이동합니다. 데스크톱의 화살표·Tab 이동에 해당합니다.
  • 터치 탐색: 손가락이 닿은 위치의 요소를 읽습니다. 그래서 화면에 보이는 위치와 접근성 트리의 요소 크기가 일치해야 합니다.
  • 두 번 탭으로 현재 요소를 실행합니다. 이때 브라우저는 일반적으로 click 이벤트를 보냅니다. keydown 이벤트는 오지 않습니다.
  • 단위 이동: VoiceOver는 로터, TalkBack은 읽기 컨트롤(읽기 옵션)로 제목·링크·컨트롤 등 단위를 고른 뒤 위·아래 스와이프로 이동합니다. 제목 구조가 데스크톱만큼 중요합니다(랜드마크와 제목 구조).

키보드 이벤트에만 의존하지 마세요

모바일 화면낭독기 사용자는 대부분 물리 키보드 없이 제스처로 조작합니다. 화살표 키로만 값을 바꾸는 커스텀 슬라이더, keydown에서만 동작하는 메뉴는 모바일에서 쓸 수 없습니다. 실행 동작은 click으로 처리하고, 가능하면 네이티브 HTML 컨트롤을 씁니다.

VoiceOver와 TalkBack의 지원 차이

두 화면낭독기 모두 WAI-ARIA를 지원하지만, 세부 동작은 운영체제·브라우저·버전에 따라 다릅니다. 아래는 흔히 차이가 나는 부분입니다. 실제 지원 여부는 대상 기기에서 직접 확인해야 합니다.

모바일 화면낭독기에서 자주 차이가 나는 동작
항목고려할 점권장 대응
설명(aria-describedby)VoiceOver는 이름·역할을 읽은 뒤 잠시 쉬었다가 힌트처럼 읽으며, 설정에서 힌트를 끄면 읽지 않습니다.필수 정보는 이름이나 보이는 텍스트에 넣고, 설명에는 보조 정보만 둡니다.
커스텀 sliderVoiceOver는 위·아래 스와이프, TalkBack은 볼륨 키·스와이프로 값을 조절하지만, 커스텀 구현은 조절 동작이 전달되지 않는 경우가 많습니다.<input type="range">를 쓰거나 “−”·“+” 버튼을 함께 제공합니다.
라이브 리전두 화면낭독기 모두 지원하지만, 포커스 이동과 동시에 발생하면 안내가 끊기거나 누락되기도 합니다.알림과 포커스 이동을 동시에 하지 않고, 짧은 문장으로 알립니다(라이브 리전).
모달(aria-modal)배경 콘텐츠 차단 지원이 브라우저마다 다릅니다. 스와이프로 대화상자 밖으로 나가는 경우가 있습니다.HTML <dialog>의 showModal() 또는 배경에 inert를 함께 씁니다.
title 속성마우스 오버가 없어 화면에 툴팁이 보이지 않습니다.정보를 보이는 텍스트나 레이블로 제공합니다.
랜드마크 이동VoiceOver 로터는 랜드마크를 지원합니다. TalkBack의 랜드마크 이동 지원은 버전·브라우저에 따라 다릅니다.랜드마크와 함께 제목 구조를 반드시 갖춥니다.
숨김 요소 위치sr-only로 화면 밖에 둔 요소는 터치 탐색으로 찾을 수 없고 스와이프로만 도달합니다.중요한 컨트롤을 시각적으로 숨기지 않습니다(콘텐츠 숨기기).

모바일에서 확인하는 방법

iOS는 설정 > 손쉬운 사용 > VoiceOver, Android는 설정 > 접근성 > TalkBack에서 켭니다. 손쉬운 사용 단축키(측면 버튼 세 번 누르기, 볼륨 키 두 개 길게 누르기)를 설정해 두면 쉽게 켜고 끌 수 있습니다. 자세한 제스처는 화면낭독기 사용법을 참고하세요.

터치 제스처의 대안 (WCAG 2.5.x)

터치 환경의 복잡한 제스처는 운동 장애가 있는 사용자, 한 손이나 스타일러스·헤드 포인터를 쓰는 사용자, 화면낭독기 사용자가 수행하기 어렵습니다. WCAG 2.2 지침 2.5(입력 방식)는 다음을 요구합니다.

입력 방식 관련 WCAG 성공 기준
성공 기준수준요구 내용
2.5.1 포인터 제스처A두 손가락 확대, 스와이프 같은 다중 포인트·경로 기반 제스처 기능은 한 번 탭·클릭으로도 할 수 있어야 합니다(예: 지도 확대 버튼, 캐러셀 이전·다음 버튼).
2.5.2 포인터 취소A누르는 순간(down)이 아니라 손을 뗄 때(up) 실행해, 잘못 눌렀을 때 손가락을 밖으로 끌어 취소할 수 있게 합니다. click 이벤트를 쓰면 대부분 충족됩니다.
2.5.3 이름에 레이블 포함A보이는 레이블 텍스트가 접근 가능한 이름에 포함되어야 합니다. 음성 제어 사용자에게 중요합니다(접근 가능한 이름).
2.5.4 동작 기반 작동A기기를 흔들거나 기울이는 동작으로 실행하는 기능은 일반 컨트롤로도 할 수 있어야 하고, 동작 반응을 끌 수 있어야 합니다.
2.5.7 드래그 동작AA (2.2 신규)드래그해야 하는 기능(순서 바꾸기, 슬라이더, 지도 이동)은 드래그 없이 한 번의 포인터 조작으로도 할 수 있어야 합니다.
비권장스와이프로만 넘기는 캐러셀
<div class="slides" ontouchmove="handleSwipe(event)">
  ...
</div>
권장버튼으로도 넘길 수 있음
<section aria-roledescription="캐러셀" aria-label="이벤트">
  <button type="button" aria-label="이전 슬라이드">‹</button>
  <button type="button" aria-label="다음 슬라이드">›</button>
  <div class="slides">...</div>
</section>
비권장드래그로만 순서 변경
<li draggable="true">장바구니 상품 A</li>
권장드래그 대신 버튼 제공
<li>
  장바구니 상품 A
  <button type="button" aria-label="상품 A 위로 이동">▲</button>
  <button type="button" aria-label="상품 A 아래로 이동">▼</button>
</li>

캐러셀의 전체 구조는 캐러셀 패턴, 슬라이더는 슬라이더 패턴을 참고하세요.

터치 대상 크기

작은 버튼은 손 떨림이 있거나 손가락이 굵은 사용자, 움직이는 차 안에서 쓰는 사용자 모두에게 누르기 어렵습니다.

  • 2.5.8 대상 크기(최소)(AA, WCAG 2.2 신규): 포인터 대상은 최소 24×24 CSS 픽셀이어야 합니다. 더 작다면, 각 대상의 중심에 지름 24px 원을 그렸을 때 이웃 대상(이나 그 원)과 겹치지 않을 만큼 간격을 두어야 합니다. 문장 속 링크, 브라우저 기본 컨트롤, 크기가 법적·본질적으로 정해진 경우, 같은 기능을 하는 충분히 큰 다른 컨트롤이 있는 경우는 예외입니다.
  • 2.5.5 대상 크기(향상)(AAA): 최소 44×44 CSS 픽셀을 요구합니다. Apple Human Interface Guidelines의 44×44pt, Android Material Design의 48×48dp 권장과 비슷한 수준이므로 실무 목표로 삼기 좋습니다.
터치 영역 확보
/* 아이콘은 작아도 누를 수 있는 영역은 넉넉하게 */
.icon-btn {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.icon-btn svg { width: 20px; height: 20px; }

확대를 막지 마세요

<meta name="viewport" content="..., user-scalable=no">나 maximum-scale=1로 확대를 막으면 저시력 사용자가 화면을 키울 수 없습니다(1.4.4 텍스트 크기 조정). width=device-width, initial-scale=1만 지정하고, 320 CSS 픽셀 너비에서도 가로 스크롤 없이 읽히도록 만듭니다(1.4.10 리플로우).

화면 방향

WCAG 1.3.4 화면 방향(AA)은 콘텐츠를 세로 또는 가로 한 방향으로만 볼 수 있게 제한하지 않도록 요구합니다. 휠체어 등에 기기를 고정해 쓰는 사용자는 기기를 돌릴 수 없습니다. 피아노 앱, 수표 촬영처럼 특정 방향이 본질적인 경우만 예외입니다.

비권장가로 모드에서 콘텐츠를 가림
@media (orientation: landscape) {
  .app { display: none; }
  .rotate-msg { display: block; }  /* "세로로 돌려 주세요" */
}
권장두 방향 모두 레이아웃 제공
@media (orientation: landscape) {
  .app { flex-direction: row; }   /* 배치만 바꿈 */
}

네이티브 앱 API와 ARIA 개념 대응

하이브리드 앱이나 웹·앱을 함께 만드는 팀은 ARIA 개념이 네이티브 플랫폼에서 어떻게 표현되는지 알아 두면 좋습니다. 개념은 같지만(이름·역할·상태·값), API 이름과 세부 동작은 다릅니다. 아래는 대표적인 대응이며 완전히 같은 의미는 아닙니다.

ARIA와 Android·iOS 접근성 API 대응
개념 (ARIA/HTML)Android (View / Jetpack Compose)iOS (UIKit / SwiftUI)
이름: aria-label, altandroid:contentDescription / Modifier.semantics { contentDescription = "..." }accessibilityLabel / .accessibilityLabel("...")
레이블 연결: <label for>, aria-labelledbyandroid:labelFor(레이블 뷰에 지정)컨테이너로 묶어 하나의 요소로 만들거나 accessibilityLabel 지정
설명: aria-describedbyandroid:hint(입력 필드), tooltipTextaccessibilityHint / .accessibilityHint("...")
역할: role위젯 클래스(Button, CheckBox 등)가 역할을 결정. AccessibilityNodeInfo의 className / Compose Role.Button, Role.Checkbox, Role.Switch, Role.Tab 등accessibilityTraits(.button, .link, .image, .searchField 등) / .accessibilityAddTraits(.isButton)
역할 설명: aria-roledescriptionAccessibilityNodeInfoCompat.setRoleDescription()공개 API 없음(트레잇·레이블로 표현)
제목: <h1>~<h6>, role="heading"android:accessibilityHeading(API 28+) / Modifier.semantics { heading() }.header 트레잇 / .accessibilityAddTraits(.isHeader)
상태: aria-checked, aria-pressed, aria-expandedisCheckable/isChecked, 사용자 정의 상태는 stateDescription(API 30+) / Compose toggleableState, stateDescriptionaccessibilityValue(예: "켬"), .selected 트레잇, iOS 17 이상 .toggleButton 트레잇
선택: aria-selectedisSelected / Compose selected.selected 트레잇
비활성: disabled, aria-disabledisEnabled = false / Compose disabled().notEnabled 트레잇
값: aria-valuenow, aria-valuetextRangeInfo, stateDescription / Compose progressBarRangeInfoaccessibilityValue, .adjustable 트레잇과 accessibilityIncrement()/accessibilityDecrement()
숨김: aria-hidden="true"android:importantForAccessibility="no" 또는 하위까지 "noHideDescendants" / Compose Modifier.clearAndSetSemantics {}isAccessibilityElement = false, 하위까지 accessibilityElementsHidden = true / .accessibilityHidden(true)
그룹으로 읽기(카드 전체를 한 번에)android:screenReaderFocusable(API 28+), focusable 컨테이너 / Modifier.semantics(mergeDescendants = true)컨테이너에 isAccessibilityElement = true / .accessibilityElement(children: .combine)
모달: aria-modal, inert대화상자(Dialog) 창 사용, 화면 제목은 accessibilityPaneTitle(API 28+) / Compose paneTitleaccessibilityViewIsModal = true / .accessibilityAddTraits(.isModal)
라이브 리전: aria-liveandroid:accessibilityLiveRegion="polite" / Compose liveRegion = LiveRegionMode.PoliteUIAccessibility.post(notification: .announcement, argument: "..."), 자주 바뀌는 값은 .updatesFrequently 트레잇
포커스 이동: element.focus()sendAccessibilityEvent(TYPE_VIEW_FOCUSED) 등 / Compose FocusRequesterUIAccessibility.post(notification: .screenChanged 또는 .layoutChanged, argument: 대상뷰)
언어: langLocaleSpan 등 텍스트 스팬accessibilityLanguage
Kotlin (Jetpack Compose)
// Jetpack Compose: 즐겨찾기 토글 아이콘
IconToggleButton(
  checked = isFav,
  onCheckedChange = { isFav = it },
  modifier = Modifier.semantics {
    stateDescription = if (isFav) "추가됨" else "추가 안 됨"
  }
) {
  Icon(Icons.Default.Star, contentDescription = "즐겨찾기")
}
Swift (SwiftUI)
// SwiftUI: 같은 기능
Button(action: { isFav.toggle() }) {
  Image(systemName: isFav ? "star.fill" : "star")
}
.accessibilityLabel("즐겨찾기")
.accessibilityValue(isFav ? "추가됨" : "추가 안 됨")

즉시 안내 API는 아껴 쓰세요

Android의 announceForAccessibility(), iOS의 .announcement 알림은 ARIA의 aria-live="assertive"처럼 사용자가 듣던 내용을 끊을 수 있습니다. Android 공식 문서는 즉시 안내 대신 라이브 리전과 창 제목(pane title)을 쓰도록 권장합니다. 모바일 앱 지침은 모바일 앱 접근성 지침을 참고하세요.

모바일 점검 체크리스트

  • VoiceOver와 TalkBack에서 스와이프만으로 모든 콘텐츠와 컨트롤에 순서대로 도달하는지 확인합니다.
  • 두 번 탭으로 모든 버튼·링크·커스텀 위젯이 실행되는지 확인합니다(keydown에만 의존하지 않는지).
  • 로터·읽기 컨트롤의 제목 이동으로 페이지 구조가 파악되는지 확인합니다.
  • 스와이프·핀치·드래그·흔들기로 하는 기능에 버튼 같은 대안이 있는지 확인합니다.
  • 터치 대상이 24×24 CSS 픽셀 이상(권장 44×44)이거나 충분한 간격이 있는지 확인합니다.
  • 세로·가로 방향 모두에서 콘텐츠를 쓸 수 있고, 확대가 막혀 있지 않은지 확인합니다.
  • 대화상자가 열렸을 때 스와이프로 배경 콘텐츠에 가지 않는지 확인합니다.