Flutter 튜토리얼 37편: 접근성 구현

요약#

핵심 요지#

  • 문제 정의: 시각, 청각, 운동 장애가 있는 사용자가 앱을 사용할 수 없으면 법적, 도덕적 문제가 발생하고 사용자 기반이 제한된다.
  • 핵심 주장: Flutter는 기본 위젯에서 접근성을 자동 지원하고, Semantics1 위젯으로 커스텀 접근성 정보를 추가할 수 있으며, AccessibilityGuideline2 API로 테스트할 수 있다.
  • 주요 근거: WCAG 2 표준은 4.5<1> 색상 대비, 48x48dp 터치 타겟 크기를 권장하고, UN 장애인권리협약은 정보 시스템 접근성을 요구한다.
  • 실무 기준: 스크린 리더로 테스트하고, 대비율과 터치 타겟 크기를 확인하며, Semantics 위젯으로 명확한 레이블을 제공한다.

문서가 설명하는 범위#

  • 접근성 규정과 표준(WCAG, ADA, EAA)
  • UI 디자인과 스타일링(폰트 크기, 대비율, 터치 타겟)
  • Semantics 위젯을 활용한 스크린 리더 지원
  • TalkBack, VoiceOver로 접근성 테스트
  • AccessibilityGuideline API로 자동화 테스트

읽는 시간: 18분 | 난이도: 중급


참고 자료#


문제 상황#

앱을 출시했는데 시각 장애가 있는 사용자로부터 “스크린 리더가 버튼을 읽어주지 않는다”는 피드백을 받았습니다. 아이콘만 있는 버튼에 레이블이 없어서 스크린 리더가 “버튼”이라고만 읽어줍니다. 저시력 사용자는 텍스트 색상이 배경과 비슷해서 읽기 어렵다고 합니다.

// 접근성 문제가 있는 코드
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => deleteItem(),
// 레이블 없음 - 스크린 리더가 "버튼"이라고만 읽음
)
Container(
color: Colors.grey.shade300,
child: Text(
'중요한 정보',
style: TextStyle(color: Colors.grey.shade500), // 대비율 낮음
),
)

문제는 다음과 같습니다.

  • 스크린 리더가 UI 요소를 제대로 설명하지 못한다.
  • 색상 대비가 낮아 저시력 사용자가 읽기 어렵다.
  • 터치 타겟이 너무 작아 운동 장애가 있는 사용자가 탭하기 어렵다.
  • 시스템 폰트 크기 설정을 무시하고 고정 크기를 사용한다.

해결 방법#

Flutter는 접근성을 기본으로 지원합니다. Material과 Cupertino 위젯은 자동으로 접근성 정보를 생성하고, Semantics 위젯으로 커스텀 정보를 추가할 수 있습니다.

챕터 1: 접근성 규정 이해하기#

Why#

NOTE

접근성은 도덕적 의무이자 법적 요구사항입니다. UN 장애인권리협약은 정보 시스템 접근성을 요구하고, 많은 국가에서 이를 법으로 강제합니다.

미국의 ADA(Americans with Disabilities Act), EU의 EAA(European Accessibility Act) 등 법률을 위반하면 법적 문제가 발생할 수 있습니다.

What#

NOTE

주요 접근성 표준

표준설명
WCAG 2Web Content Accessibility Guidelines - 국제 표준
EN 301 549EU ICT 제품/서비스 접근성 표준
VPATVoluntary Product Accessibility Template - 자체 평가 도구

주요 법률

법률지역요구사항
ADA미국공공 시설 차별 금지
Section 508미국연방 기관 ICT WCAG 준수
EAAEU공공/민간 서비스 접근성

Watch out#

WARNING

앱 출시 전 접근성 체크리스트

  • 모든 인터랙티브 요소가 동작해야 합니다. 빈 콜백 대신 SnackBar라도 표시하세요.
  • 스크린 리더가 모든 컨트롤을 설명할 수 있어야 합니다.
  • 색상 대비율이 4.5<1> 이상이어야 합니다.
  • 터치 타겟이 48x48dp 이상이어야 합니다.
  • 중요한 동작은 실행 취소가 가능해야 합니다.
  • 색맹 모드와 흑백 모드에서도 사용 가능해야 합니다.
  • 큰 폰트 설정에서도 레이아웃이 유지되어야 합니다.

결론: 접근성 표준을 이해하고 앱 출시 전에 체크리스트를 확인해야 합니다.


챕터 2: 큰 폰트와 텍스트 크기 지원#

Why#

NOTE

사용자는 시스템 설정에서 폰트 크기를 조정할 수 있습니다. 저시력 사용자는 더 큰 폰트가 필요하고, Flutter는 이 설정을 자동으로 존중합니다.

하지만 개발자가 레이아웃을 테스트하지 않으면 큰 폰트에서 UI가 깨질 수 있습니다.

What#

NOTE

Flutter의 Text 위젯은 시스템 폰트 크기 설정을 자동으로 반영합니다. MediaQuery.textScalerOf(context)3로 현재 텍스트 배율을 확인할 수 있습니다.

설정Android 경로iOS 경로
폰트 크기설정 > 디스플레이 > 글꼴 크기설정 > 손쉬운 사용 > 디스플레이 및 텍스트 크기

How#

TIP

레이아웃이 큰 폰트에서도 유지되도록 설계

class AccessibleLayout extends StatelessWidget {
const AccessibleLayout({super.key});
@override
Widget build(BuildContext context) {
// 텍스트 배율 확인
final textScaler = MediaQuery.textScalerOf(context);
return Scaffold(
appBar: AppBar(title: const Text('Accessible App')),
body: SingleChildScrollView(
// 큰 폰트에서 오버플로우 방지를 위해 스크롤 가능하게
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Flexible 위젯으로 텍스트가 줄바꿈되도록
Text(
'이 텍스트는 시스템 폰트 크기 설정을 따릅니다.',
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
// Wrap으로 버튼들이 줄바꿈되도록
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: () {},
child: const Text('버튼 1'),
),
ElevatedButton(
onPressed: () {},
child: const Text('버튼 2'),
),
],
),
],
),
),
),
);
}
}

텍스트 배율 제한 (필요한 경우에만)

// 특정 위젯에서 텍스트 크기 제한이 필요한 경우
Text(
'최대 1.5배까지만 확대',
textScaler: MediaQuery.textScalerOf(context).clamp(
maxScaleFactor: 1.5,
),
)

Watch out#

WARNING

고정 높이를 사용하면 텍스트가 잘립니다

// ❌ 고정 높이 - 큰 폰트에서 텍스트 잘림
Container(
height: 50,
child: Text('긴 텍스트...'),
)
// ✅ 최소 높이 또는 콘텐츠 기반 높이
Container(
constraints: BoxConstraints(minHeight: 50),
child: Text('긴 텍스트...'),
)

가장 큰 폰트 설정으로 앱을 테스트하세요.

결론: 시스템 폰트 크기 설정을 존중하고, 레이아웃이 유연하게 조정되도록 설계합니다.


챕터 3: 색상 대비율 확보#

Why#

NOTE

색상 대비가 낮으면 저시력 사용자와 색맹 사용자가 콘텐츠를 읽기 어렵습니다. 직사광선 아래나 밝기가 낮은 화면에서도 대비가 중요합니다.

WCAG는 텍스트 대비율 기준을 제시합니다.

What#

NOTE

WCAG 권장 대비율

텍스트 크기최소 대비율
작은 텍스트 (18pt 미만, 14pt 미만 볼드)4.5<1>
큰 텍스트 (18pt 이상, 14pt 이상 볼드)3.0<1>

대비율은 전경색과 배경색의 휘도 비율입니다. 흰색 배경에 검은색 텍스트는 21<1의> 최대 대비율을 가집니다.

How#

TIP

접근 가능한 색상 사용

class AccessibleColors extends StatelessWidget {
const AccessibleColors({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// ✅ 좋은 대비 - 4.5:1 이상
Container(
color: Colors.white,
padding: const EdgeInsets.all(16),
child: const Text(
'읽기 쉬운 텍스트',
style: TextStyle(
color: Colors.black87, // 높은 대비
fontSize: 16,
),
),
),
// ✅ 큰 텍스트는 3:1 이상
Container(
color: Colors.blue.shade900,
padding: const EdgeInsets.all(16),
child: const Text(
'헤드라인',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
// ✅ 비활성화 요소는 대비율 예외
const ElevatedButton(
onPressed: null, // 비활성화
child: Text('비활성화 버튼'),
),
],
);
}
}

테마에서 접근 가능한 색상 정의

ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
// Material 3은 접근 가능한 색상을 자동 생성
),
textTheme: const TextTheme(
bodyLarge: TextStyle(
color: Colors.black87, // 높은 대비
),
),
)

Watch out#

WARNING

색상만으로 정보를 전달하지 마세요

// ❌ 색상만으로 상태 표시 - 색맹 사용자 문제
Container(
color: isError ? Colors.red : Colors.green,
)
// ✅ 색상 + 아이콘/텍스트로 상태 표시
Container(
color: isError ? Colors.red : Colors.green,
child: Row(
children: [
Icon(isError ? Icons.error : Icons.check),
Text(isError ? '오류' : '성공'),
],
),
)

색맹 시뮬레이터로 앱을 테스트하세요.

결론: WCAG 대비율 기준을 따르고, 색상 외에도 아이콘이나 텍스트로 정보를 전달합니다.


챕터 4: 터치 타겟 크기#

Why#

NOTE

터치 타겟이 너무 작으면 운동 장애가 있는 사용자나 손 떨림이 있는 사용자가 탭하기 어렵습니다. 손가락으로 정확히 작은 영역을 탭하는 것은 모든 사용자에게 어렵습니다.

What#

NOTE

플랫폼별 터치 타겟 권장 크기

플랫폼최소 크기
Android48x48 dp
iOS44x44 pt
WCAG44x44 CSS 픽셀

Material 위젯(IconButton, ListTile 등)은 기본적으로 이 크기를 만족합니다.

How#

TIP

적절한 터치 타겟 크기 확보

class AccessibleTouchTargets extends StatelessWidget {
const AccessibleTouchTargets({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
// ✅ Material 버튼은 기본적으로 48x48 이상
IconButton(
icon: const Icon(Icons.add),
onPressed: () {},
),
// ✅ 작은 아이콘이지만 터치 영역은 충분히 큼
IconButton(
iconSize: 20, // 작은 아이콘
padding: const EdgeInsets.all(14), // 터치 영역 확보
icon: const Icon(Icons.close),
onPressed: () {},
),
// ✅ 커스텀 터치 타겟
GestureDetector(
onTap: () {},
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
child: const Icon(Icons.star, size: 24),
),
),
// ✅ InkWell로 터치 영역 확장
InkWell(
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(12),
child: Text('탭하세요'),
),
),
],
);
}
}

ListTile은 기본적으로 접근 가능

// ListTile은 기본 높이가 56dp로 충분한 터치 타겟
ListTile(
leading: const Icon(Icons.settings),
title: const Text('설정'),
onTap: () {},
)

Watch out#

WARNING

터치 타겟이 너무 가까우면 실수로 다른 버튼을 탭합니다

// ❌ 버튼들이 너무 가까움
Row(
children: [
IconButton(icon: Icon(Icons.edit), onPressed: () {}),
IconButton(icon: Icon(Icons.delete), onPressed: () {}),
],
)
// ✅ 충분한 간격 확보
Row(
children: [
IconButton(icon: Icon(Icons.edit), onPressed: () {}),
const SizedBox(width: 8), // 간격 추가
IconButton(icon: Icon(Icons.delete), onPressed: () {}),
],
)

결론: 모든 터치 타겟은 최소 48x48dp 이상이어야 하고, 서로 충분한 간격을 유지해야 합니다.


챕터 5: Semantics 위젯으로 스크린 리더 지원#

Why#

NOTE

스크린 리더(TalkBack, VoiceOver)는 UI 요소의 의미를 음성으로 읽어줍니다. Material 위젯은 자동으로 접근성 정보를 제공하지만, 커스텀 위젯은 직접 정보를 추가해야 합니다.

아이콘만 있는 버튼은 스크린 리더가 “버튼”이라고만 읽어서 사용자가 기능을 알 수 없습니다.

What#

NOTE

Flutter는 위젯 트리에서 자동으로 접근성 트리(Semantics Tree)를 생성합니다. Semantics 위젯으로 커스텀 접근성 정보를 추가할 수 있습니다.

속성설명
label스크린 리더가 읽어주는 텍스트
hint추가 설명 (예: “두 번 탭하여 활성화”)
button버튼임을 표시
image이미지임을 표시
excludeSemantics하위 위젯의 Semantics 무시

How#

TIP

IconButton에 레이블 추가

// 방법 1: tooltip 사용 (권장)
IconButton(
icon: const Icon(Icons.delete),
tooltip: '항목 삭제', // 스크린 리더가 읽음
onPressed: () => deleteItem(),
)
// 방법 2: Semantics 위젯 사용
Semantics(
label: '항목 삭제',
button: true,
child: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => deleteItem(),
),
)

이미지에 설명 추가

// 장식용 이미지 - 스크린 리더가 무시
Semantics(
image: true,
label: '', // 빈 레이블로 장식용 표시
child: Image.asset('assets/decoration.png'),
)
// 의미 있는 이미지 - 설명 제공
Semantics(
image: true,
label: '로그인 성공을 나타내는 체크 아이콘',
child: Image.asset('assets/success.png'),
)

복잡한 커스텀 위젯

class AccessibleRatingWidget extends StatelessWidget {
const AccessibleRatingWidget({
super.key,
required this.rating,
required this.onChanged,
});
final int rating;
final ValueChanged<int> onChanged;
@override
Widget build(BuildContext context) {
return Semantics(
label: '평점 $rating점',
hint: '좌우로 스와이프하여 변경',
value: '$rating / 5',
slider: true,
onIncrease: () => onChanged((rating + 1).clamp(1, 5)),
onDecrease: () => onChanged((rating - 1).clamp(1, 5)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) {
return GestureDetector(
onTap: () => onChanged(index + 1),
child: Icon(
index < rating ? Icons.star : Icons.star_border,
color: Colors.amber,
),
);
}),
),
);
}
}

그룹화된 요소

// 여러 요소를 하나의 Semantics로 그룹화
Semantics(
label: '사용자 정보: 홍길동, 서울시',
child: Row(
children: [
// 개별 Text의 Semantics 제외
ExcludeSemantics(child: Text('홍길동')),
ExcludeSemantics(child: Text('서울시')),
],
),
)

Watch out#

WARNING

TextSpan.locale로 다국어 음성 지원

앱에 다국어 텍스트가 있으면 스크린 리더가 올바른 발음으로 읽도록 locale을 지정하세요.

RichText(
text: TextSpan(
children: [
TextSpan(
text: '안녕하세요, ',
locale: const Locale('ko'),
),
TextSpan(
text: 'Hello!',
locale: const Locale('en'),
),
],
),
)

결론: Semantics 위젯과 tooltip으로 스크린 리더가 모든 UI 요소를 명확하게 설명하도록 합니다.


챕터 6: 스크린 리더로 테스트하기#

Why#

NOTE

실제 스크린 리더로 테스트해야 사용자 경험을 확인할 수 있습니다. 개발자가 직접 스크린 리더를 사용해보면 접근성 문제를 더 잘 이해할 수 있습니다.

What#

NOTE

플랫폼별 스크린 리더

플랫폼스크린 리더활성화 방법
AndroidTalkBack설정 > 손쉬운 사용 > TalkBack
iOSVoiceOver설정 > 손쉬운 사용 > VoiceOver

모빌리티 지원 도구

플랫폼도구설명
AndroidSwitch Access외부 스위치로 조작
AndroidVoice Access음성으로 조작
iOSSwitch Control외부 스위치로 조작
iOSVoice Control음성으로 조작
iOSAssistiveTouch멀티터치 제스처 대체

How#

TIP

TalkBack으로 테스트 (Android)

  1. 설정 > 손쉬운 사용 > TalkBack을 켭니다.
  2. 화면을 탭하면 TalkBack이 요소를 읽어줍니다.
  3. 두 번 탭하면 요소를 활성화합니다.
  4. 좌우로 스와이프하면 다음/이전 요소로 이동합니다.

VoiceOver로 테스트 (iOS)

  1. 설정 > 손쉬운 사용 > VoiceOver를 켭니다.
  2. 화면을 탭하면 VoiceOver가 요소를 읽어줍니다.
  3. 두 번 탭하면 요소를 활성화합니다.
  4. 좌우로 스와이프하면 다음/이전 요소로 이동합니다.

테스트 체크리스트

- [ ] 모든 버튼에 의미 있는 레이블이 있는가?
- [ ] 읽어주는 순서가 논리적인가?
- [ ] 화면 전환 시 포커스가 적절한 위치로 이동하는가?
- [ ] 에러 메시지가 자동으로 읽히는가?
- [ ] 로딩 상태가 알림되는가?

결론: 개발 중에 TalkBack과 VoiceOver로 정기적으로 테스트하세요.


챕터 7: AccessibilityGuideline API로 자동화 테스트#

Why#

NOTE

수동 테스트만으로는 모든 접근성 문제를 찾기 어렵습니다. 자동화 테스트를 추가하면 회귀를 방지하고 일관된 접근성을 유지할 수 있습니다.

What#

NOTE

Flutter의 AccessibilityGuideline API는 다음을 자동으로 테스트합니다.

가이드라인설명
androidTapTargetGuidelineAndroid 탭 타겟 최소 48x48dp
iOSTapTargetGuidelineiOS 탭 타겟 최소 44x44pt
labeledTapTargetGuideline탭 가능한 요소에 레이블 있음
textContrastGuideline텍스트 대비율 3<1> 이상

How#

TIP

접근성 테스트 작성

test/accessibility_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/main.dart';
void main() {
testWidgets('앱이 접근성 가이드라인을 따른다', (tester) async {
// Semantics 활성화
final SemanticsHandle handle = tester.ensureSemantics();
// 테스트할 위젯 렌더링
await tester.pumpWidget(const MyApp());
// Android 탭 타겟 크기 검사 (48x48dp)
await expectLater(
tester,
meetsGuideline(androidTapTargetGuideline),
);
// iOS 탭 타겟 크기 검사 (44x44pt)
await expectLater(
tester,
meetsGuideline(iOSTapTargetGuideline),
);
// 레이블 검사
await expectLater(
tester,
meetsGuideline(labeledTapTargetGuideline),
);
// 텍스트 대비율 검사 (3:1 이상)
await expectLater(
tester,
meetsGuideline(textContrastGuideline),
);
// 정리
handle.dispose();
});
}

특정 화면 테스트

testWidgets('로그인 화면 접근성', (tester) async {
final handle = tester.ensureSemantics();
await tester.pumpWidget(
const MaterialApp(home: LoginScreen()),
);
// 모든 가이드라인 검사
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});

테스트 실행

Terminal window
flutter test test/accessibility_test.dart

Watch out#

WARNING

AccessibilityGuideline은 모든 문제를 찾지 못합니다

자동화 테스트는 기본적인 검사만 수행합니다. 다음은 여전히 수동 테스트가 필요합니다.

  • 레이블이 의미 있는지 (있기만 한 것 vs 이해할 수 있는 것)
  • 읽는 순서가 논리적인지
  • 포커스 이동이 자연스러운지
  • 컨텍스트 변경이 예측 가능한지

자동화 테스트와 수동 스크린 리더 테스트를 함께 사용하세요.

결론: AccessibilityGuideline API로 기본 접근성 검사를 자동화하고, 수동 테스트로 보완합니다.


한계#

  • 자동화 테스트는 레이블의 품질을 검사하지 못합니다. “버튼 1”보다 “장바구니에 추가”가 더 좋지만, 둘 다 테스트를 통과합니다.
  • 색상 대비 테스트는 정적 색상만 검사합니다. 이미지 위의 텍스트는 수동으로 확인해야 합니다.
  • 웹 접근성은 별도 문서에서 다룹니다. ARIA 속성과 키보드 탐색은 추가 구현이 필요합니다.

Footnotes#

  1. Semantics(시맨틱스): 위젯의 의미와 역할을 접근성 도구(스크린 리더 등)에 전달하는 Flutter 위젯이다.

  2. AccessibilityGuideline(접근성 가이드라인): Flutter 테스트에서 접근성 기준 충족 여부를 자동으로 검사하는 API다.

  3. TextScaler(텍스트 스케일러): 시스템 폰트 크기 설정에 따른 텍스트 배율 정보를 제공하는 클래스다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 37편: 접근성 구현
https://moodturnpost.net/posts/flutter/flutter-accessibility/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차