이 글은 누구를 위한 것인가
- 앱 접근성 검수를 받아야 하는 팀
- VoiceOver나 TalkBack을 켰을 때 앱이 제대로 작동하지 않는 문제를 해결하려는 개발자
- WCAG 모바일 가이드라인을 구현해야 하는 엔지니어
접근성이 중요한 이유
WHO에 따르면 전 세계 인구의 약 15%가 어떤 형태로든 장애를 가지고 있다. 시각 장애인은 스크린 리더(iOS: VoiceOver, Android: TalkBack)로 앱을 사용한다. 한국에서도 장애인 차별 금지법에 따라 일정 규모 이상의 앱은 접근성 기준을 충족해야 한다.
접근성은 장애인만을 위한 것이 아니다. 밝은 야외에서 화면이 잘 안 보이는 상황, 한 손을 사용할 수 없는 상황, 고령 사용자 모두가 접근성 개선의 수혜자다.
1. 스크린 리더 지원: 시맨틱 레이블링
스크린 리더는 화면의 텍스트를 읽어주고, 이미지나 아이콘 등 비텍스트 요소는 개발자가 의미를 제공해야 한다.
Flutter: Semantics 위젯
// 아이콘 버튼에 레이블 추가
Semantics(
label: '좋아요',
hint: '더블 탭으로 좋아요를 표시합니다',
button: true,
child: IconButton(
icon: Icon(Icons.favorite_border),
onPressed: onLike,
),
),
// 이미지에 대체 텍스트
Semantics(
label: '나이키 에어맥스 270 흰색 운동화',
image: true,
child: Image.network(productImageUrl),
),
// 장식용 이미지는 스크린 리더에서 제외
ExcludeSemantics(
child: Image.asset('assets/decorative_background.png'),
),
// 여러 위젯을 하나의 시맨틱 단위로 묶기
Semantics(
label: '상품명: 나이키 에어맥스. 가격: 139,000원. 재고 있음',
child: Column(
children: [
Text('나이키 에어맥스'),
Text('139,000원'),
Text('재고 있음'),
],
),
),
React Native: accessibility Props
// 아이콘 버튼
<TouchableOpacity
accessible={true}
accessibilityLabel="장바구니에 추가"
accessibilityHint="상품을 장바구니에 추가합니다"
accessibilityRole="button"
onPress={addToCart}
>
<Icon name="cart-plus" />
</TouchableOpacity>
// 이미지
<Image
source={{ uri: productImage }}
accessible={true}
accessibilityLabel="나이키 에어맥스 270 흰색 운동화 측면 사진"
accessibilityRole="image"
/>
// 장식용 요소 숨기기
<Image
source={decorativeImage}
accessible={false}
importantForAccessibility="no-hide-descendants"
/>
// 상태를 전달하는 컴포넌트
<TouchableOpacity
accessibilityLabel={`${productName}, ${isFavorite ? '즐겨찾기 등록됨' : '즐겨찾기 해제됨'}`}
accessibilityState={{ selected: isFavorite }}
onPress={toggleFavorite}
>
<Icon name={isFavorite ? 'heart' : 'heart-outline'} />
</TouchableOpacity>
2. 포커스 관리
스크린 리더 사용자는 포커스 순서로 앱을 탐색한다. 논리적이지 않은 포커스 순서는 혼란을 준다.
포커스 순서
UI에서 시각적으로 왼쪽 위에서 오른쪽 아래로 읽히는 순서가 포커스 순서여야 한다. 복잡한 레이아웃에서 이 순서가 깨지는 경우:
Flutter:
// FocusTraversalGroup으로 포커스 순서 그룹화
FocusTraversalGroup(
policy: OrderedTraversalPolicy(),
child: Column(
children: [
FocusTraversalOrder(
order: NumericFocusOrder(1),
child: ProductTitle(),
),
FocusTraversalOrder(
order: NumericFocusOrder(2),
child: ProductPrice(),
),
FocusTraversalOrder(
order: NumericFocusOrder(3),
child: AddToCartButton(),
),
],
),
),
화면 전환 시 포커스 이동
새 화면이 열릴 때 포커스가 적절한 위치로 이동해야 한다.
// React Native
import { AccessibilityInfo, findNodeHandle } from 'react-native';
const ModalContent = () => {
const titleRef = useRef(null);
useEffect(() => {
// 모달이 열리면 제목으로 포커스 이동
const timeout = setTimeout(() => {
const node = findNodeHandle(titleRef.current);
if (node) {
AccessibilityInfo.setAccessibilityFocus(node);
}
}, 300); // 애니메이션 완료 후
return () => clearTimeout(timeout);
}, []);
return (
<View>
<Text ref={titleRef} accessibilityRole="header">
상품 상세 정보
</Text>
{/* ... */}
</View>
);
};
3. 터치 타겟 크기
손가락으로 터치할 수 있는 최소 크기 기준:
- WCAG 2.5.5: 44×44 CSS 픽셀 (권장)
- Apple HIG: 44×44 포인트
- Material Design: 48×48dp
시각적으로 작은 요소도 터치 타겟은 크게 만들 수 있다.
// Flutter: 시각적 크기와 터치 타겟 분리
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
width: 44,
height: 44,
alignment: Alignment.center,
child: Icon(Icons.close, size: 20), // 아이콘은 20, 타겟은 44
),
),
// React Native: hitSlop으로 터치 영역 확장
<TouchableOpacity
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
onPress={onClose}
style={{ width: 20, height: 20 }}
>
<Icon name="close" size={20} />
</TouchableOpacity>
4. 색상 대비
시각 장애나 색약이 있는 사용자를 위해 충분한 색상 대비가 필요하다.
WCAG AA 기준:
- 일반 텍스트: 4.5:1 이상
- 큰 텍스트 (18pt 이상 또는 굵은 14pt): 3:1 이상
- UI 컴포넌트, 그래픽: 3:1 이상
색상만으로 정보를 전달하지 않는다:
// 나쁜 예: 빨강=에러, 초록=성공을 색상만으로 구분
Text(
message,
style: TextStyle(color: isError ? Colors.red : Colors.green),
),
// 좋은 예: 아이콘 + 색상 조합
Row(
children: [
Icon(
isError ? Icons.error : Icons.check_circle,
color: isError ? Colors.red : Colors.green,
semanticLabel: isError ? '오류' : '성공',
),
Text(message),
],
),
5. 접근성 테스트
VoiceOver (iOS) 테스트
- 설정 → 손쉬운 사용 → VoiceOver 켜기
- 또는 사이드 버튼 3번 탭 (접근성 단축키 설정 시)
- 화면을 탐색하며 포커스 순서와 레이블이 올바른지 확인
- Simulator에서도 테스트 가능 (단축키: Cmd+F5)
TalkBack (Android) 테스트
- 설정 → 접근성 → TalkBack 켜기
- 또는 볼륨 양쪽 3초 동시 누르기
- Android Studio Accessibility Scanner 플러그인으로 자동 검사
자동화 도구
# Flutter: flutter_test의 accessibility 검사
testWidgets('접근성 검사', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// 접근성 가이드라인 위반 검사
await expectLater(
tester,
meetsGuideline(androidTapTargetGuideline), // 터치 타겟 크기
);
await expectLater(
tester,
meetsGuideline(labeledTapTargetGuideline), // 레이블 누락
);
await expectLater(
tester,
meetsGuideline(textContrastGuideline), // 색상 대비
);
});
맺으며
접근성은 나중에 추가하는 기능이 아니라 처음부터 설계에 포함되어야 한다. 이미 완성된 앱에 접근성을 추가하는 것은 몇 배의 비용이 든다.
시작점은 두 가지다: 모든 인터랙티브 요소에 레이블 추가와 터치 타겟 크기 44×44 확인. 이 두 가지만 해도 스크린 리더 사용자의 기본 탐색 경험이 크게 개선된다. 직접 VoiceOver를 켜고 자신의 앱을 사용해보는 것이 어떤 문서보다 빠른 학습법이다.