Flutter 튜토리얼 38편: 웹 접근성

Flutter 웹 접근성#

Flutter 웹 애플리케이션에서 접근성을 구현하는 방법을 알아봅니다. Semantics 트리가 HTML DOM으로 어떻게 변환되는지, 그리고 스크린 리더가 이를 어떻게 해석하는지 살펴보겠습니다.


웹 접근성의 특수성#

Why - 왜 웹에서는 별도의 접근성 처리가 필요한가요?#

Flutter는 모든 UI를 단일 캔버스(Canvas)에 렌더링합니다. 네이티브 앱에서는 운영체제가 제공하는 접근성 API와 직접 통신하지만, 웹에서는 브라우저가 HTML DOM 구조를 기반으로 접근성 정보를 해석합니다.

캔버스에 그려진 픽셀만으로는 스크린 리더가 UI의 의미와 구조를 파악할 수 없습니다. 따라서 Flutter 웹은 내부 Semantics 트리를 접근 가능한 HTML DOM 구조로 변환하는 특별한 레이어가 필요합니다.

What - Flutter 웹 접근성이란 무엇인가요?#

Flutter 웹 접근성은 Semantics 트리의 정보를 HTML 요소와 ARIA(Accessible Rich Internet Applications) 속성으로 변환하여 스크린 리더가 이해할 수 있게 만드는 시스템입니다.

주요 특징:

  • 성능 최적화를 위한 선택적 활성화: 기본적으로 비활성화 상태
  • Semantics → HTML DOM 변환: 위젯의 의미 정보를 HTML 구조로 매핑
  • ARIA 역할 지원: SemanticsRole을 통한 웹 표준 역할 정의

How - 어떻게 동작하나요?#

Flutter Widget Tree
Semantics Tree (의미 정보)
HTML DOM + ARIA 속성
브라우저 접근성 API
스크린 리더

Watch out - 주의사항#

Flutter 웹 접근성은 기본적으로 비활성화되어 있습니다. 성능상의 이유로 명시적으로 활성화해야 합니다.


웹 접근성 활성화#

Why - 왜 명시적으로 활성화해야 하나요?#

접근성 DOM을 생성하고 유지하는 것은 추가적인 메모리와 처리 비용이 발생합니다. 모든 사용자에게 이 비용을 부담시키기보다, 실제로 필요한 사용자만 활성화할 수 있도록 선택적으로 제공합니다.

What - 활성화 방법은 무엇인가요?#

두 가지 방법으로 접근성 모드를 활성화할 수 있습니다.

방법 1: 숨겨진 버튼 (사용자 활성화)

Flutter 웹 앱에는 aria-label="Enable accessibility" 속성을 가진 보이지 않는 버튼이 있습니다. 스크린 리더 사용자가 이 버튼을 누르면 접근성 모드가 활성화됩니다.

방법 2: 코드에서 직접 활성화

import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
void main() {
runApp(const MyApp());
if (kIsWeb) {
SemanticsBinding.instance.ensureSemantics();
}
}

How - 각 방법의 차이점은 무엇인가요?#

방법활성화 시점사용 사례
숨겨진 버튼사용자 요청 시일반 사용자를 위한 성능 유지
코드 활성화앱 시작 시접근성이 필수인 서비스

Watch out - 주의사항#

kIsWeb 상수는 package:flutter/foundation.dart에서 가져옵니다. 웹이 아닌 플랫폼에서 ensureSemantics()를 호출해도 문제없지만, 조건문으로 감싸는 것이 의도를 명확히 합니다.

// kIsWeb 사용 시 foundation.dart 임포트 필요
import 'package:flutter/foundation.dart';

SemanticsRole을 통한 역할 정의#

Why - 왜 역할 정의가 중요한가요?#

시맨틱 역할(Semantic Role)은 UI 요소의 목적을 정의합니다. 스크린 리더는 이 역할 정보를 바탕으로 사용자에게 요소의 유형과 기능을 알려줍니다.

예를 들어 역할이 정의되지 않은 커스텀 버튼은 스크린 리더에서 단순히 “텍스트”로 읽힐 수 있습니다. 하지만 버튼 역할이 정의되면 “버튼, 제출하기”처럼 명확하게 안내됩니다.

What - SemanticsRole이란 무엇인가요?#

SemanticsRole은 Flutter에서 제공하는 열거형으로, 위젯에 특정 역할을 할당합니다. 웹에서 렌더링될 때 이 역할은 해당하는 ARIA 역할로 변환됩니다.

주요 SemanticsRole 값:

SemanticsRoleARIA 역할설명
buttonbutton클릭 가능한 버튼
linklink하이퍼링크
headingheading제목
listlist목록 컨테이너
listItemlistitem목록 항목
tabtab
tabBartablist탭 목록
tabletable테이블
sliderslider슬라이더

How - SemanticsRole을 어떻게 사용하나요?#

표준 위젯 사용 (자동 역할 할당)

Flutter의 표준 위젯들은 적절한 시맨틱 정보를 자동으로 포함합니다.

// 자동으로 button 역할이 할당됨
ElevatedButton(
onPressed: () => print('클릭'),
child: Text('제출'),
)
// 자동으로 tablist, tab 역할이 할당됨
TabBar(
tabs: [
Tab(text: '홈'),
Tab(text: '설정'),
],
)

커스텀 위젯에 역할 명시

import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
class CustomButton extends StatelessWidget {
final VoidCallback onTap;
final Widget child;
const CustomButton({
required this.onTap,
required this.child,
super.key,
});
@override
Widget build(BuildContext context) {
return Semantics(
role: SemanticsRole.button,
button: true,
child: GestureDetector(
onTap: onTap,
child: child,
),
);
}
}

목록 구조 정의

class MyCustomListWidget extends StatelessWidget {
const MyCustomListWidget({super.key});
@override
Widget build(BuildContext context) {
return Semantics(
role: SemanticsRole.list,
explicitChildNodes: true,
child: Column(
children: <Widget>[
Semantics(
role: SemanticsRole.listItem,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text('첫 번째 항목'),
),
),
Semantics(
role: SemanticsRole.listItem,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text('두 번째 항목'),
),
),
],
),
);
}
}

Watch out - 주의사항#

explicitChildNodes: true를 설정하면 부모 Semantics가 자식의 시맨틱 정보를 병합하지 않습니다. 목록처럼 개별 항목이 독립적인 의미를 가져야 할 때 사용합니다.


역할별 적용 가이드#

버튼과 링크#

// 버튼 역할
Semantics(
role: SemanticsRole.button,
button: true,
label: '제출하기',
child: CustomButtonWidget(),
)
// 링크 역할
Semantics(
role: SemanticsRole.link,
link: true,
label: '자세히 보기',
child: GestureDetector(
onTap: () => launchUrl(uri),
child: Text('자세히 보기'),
),
)

제목 구조#

Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Semantics(
role: SemanticsRole.heading,
header: true,
child: Text(
'주요 기능',
style: Theme.of(context).textTheme.headlineMedium,
),
),
SizedBox(height: 16),
Text('기능에 대한 상세 설명...'),
],
)

테이블 구조#

Semantics(
role: SemanticsRole.table,
child: Table(
children: [
TableRow(
children: [
Semantics(
role: SemanticsRole.cell,
child: Text('이름'),
),
Semantics(
role: SemanticsRole.cell,
child: Text('나이'),
),
],
),
],
),
)

접근성 테스트#

브라우저 개발자 도구 활용#

  1. Chrome DevTools에서 Accessibility 탭을 엽니다
  2. 요소를 선택하여 ARIA 속성이 올바르게 매핑되었는지 확인합니다
  3. Accessibility Tree에서 전체 구조를 검토합니다

스크린 리더 테스트#

운영체제스크린 리더단축키
WindowsNVDACtrl + Alt + N
macOSVoiceOverCmd + F5
ChromeOSChromeVoxCtrl + Alt + Z

자동화된 접근성 검사#

import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('접근성 가이드라인 준수', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// 시맨틱 정보 확인
final semantics = tester.getSemantics(find.byType(CustomButton));
expect(semantics.hasFlag(SemanticsFlag.isButton), isTrue);
});
}

실전 예제: 접근성을 고려한 카드 컴포넌트#

class AccessibleCard extends StatelessWidget {
final String title;
final String description;
final VoidCallback onTap;
final String? imageUrl;
const AccessibleCard({
required this.title,
required this.description,
required this.onTap,
this.imageUrl,
super.key,
});
@override
Widget build(BuildContext context) {
return Semantics(
role: SemanticsRole.button,
button: true,
label: '$title. $description',
child: Card(
child: InkWell(
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (imageUrl != null)
Semantics(
image: true,
label: '$title 이미지',
excludeSemantics: true,
child: Image.network(imageUrl!),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 제목은 부모 Semantics의 label에 포함되므로 제외
ExcludeSemantics(
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
),
const SizedBox(height: 8),
ExcludeSemantics(
child: Text(description),
),
],
),
),
],
),
),
),
);
}
}

이 예제에서는:

  • 전체 카드를 하나의 버튼으로 인식하도록 설정
  • 이미지에 대체 텍스트 제공
  • 중복 읽기를 방지하기 위해 ExcludeSemantics 사용
  • 제목과 설명을 결합한 의미 있는 label 제공

마무리#

Flutter 웹 접근성의 핵심 개념을 정리하면:

  1. 활성화 필요: 성능상 기본 비활성화, 명시적 활성화 필요
  2. Semantics → ARIA 변환: Flutter의 시맨틱 정보가 웹 표준으로 변환
  3. SemanticsRole 활용: 커스텀 위젯에 적절한 역할 부여
  4. 표준 위젯 우선: 가능하면 표준 위젯 사용으로 자동 접근성 확보
  5. 테스트 필수: 실제 스크린 리더로 검증

공유

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

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

목차