접근성있는 Windows 데스크톱 앱 만들기 (Windows UIA를 중심으로) 2: C++와 WinUI 3를 이용한 접근성 구현
안녕하세요. 엔비전스입니다.
1부에서 우리는 Windows 데스크톱 애플리케이션의 접근성이 왜 중요한지, 그리고 Windows UI Automation(UIA)이 어떤 역할을 하는지 살펴보았습니다. UIA는 애플리케이션과 스크린 리더 같은 보조 기술 사이의 다리 역할을 하며, Automation Properties, Control Patterns, Events라는 세 가지 핵심 요소를 통해 UI 정보를 전달합니다.
이번 2부에서는 이론을 넘어 실제 코드로 접근성을 구현해보겠습니다. 사용할 프레임워크는 WinUI 3입니다. WinUI 3는 2025년 현재 Microsoft가 권장하는 Windows 데스크톱 애플리케이션 개발의 표준 프레임워크이며, 접근성 지원이 프레임워크 수준에서 잘 통합되어 있습니다. XAML 기반의 선언적 UI 정의 방식을 사용하므로, 접근성 속성을 직관적으로 설정할 수 있습니다.
이 글에서는 다음 내용을 깊게 다루어보겠습니다.
- 첫째, WinUI 3에서 사용할 수 있는 모든 Control Patterns를 카테고리별로 살펴봅니다. 개발 중에 어떤 패턴을 적용해야 할지 참조할 수 있는 목록입니다.
- 둘째, 간단한 사용자 등록 폼을 만들며 AutomationProperties의 기본 사용법을 익힙니다.
- 셋째, 키보드 접근성과 Live Regions를 통한 동적 알림 구현 방법을 알아봅니다.
- 넷째, ListView, DataGrid, 모달 대화상자 같은 복잡한 컨트롤의 접근성 원리를 개념 수준에서 이해합니다.
- 다섯째, 별점 위젯이라는 커스텀 컨트롤을 직접 만들어 Automation Peer를 구현하는 방법을 상세히 살펴봅니다.
이 글은 Windows App SDK 1.8.4 (2025년 12월 기준 최신 안정 버전)를 기준으로 작성되었습니다. Visual Studio 2022 17.12 이상과 Windows 11 또는 Windows 10 버전 1809 이상이 필요합니다.
이 글에서 다루는 실제 예제는 이 Repository에서 확인하실 수 있습니다.
개발 환경 설정
필수 구성 요소
WinUI 3 개발을 시작하기 전에 다음 도구들이 설치되어 있어야 합니다.
- Visual Studio 2022 (버전 17.12 이상)
- Visual Studio Installer에서 다음 워크로드와 구성 요소를 선택합니다.
- ".NET 데스크톱 개발" 워크로드
- "C++를 사용한 데스크톱 개발" 워크로드
- Windows 11 SDK (10.0.22621.0 이상)
- C++ (v143) Universial Windows 플랫폼 도구
- Windows App SDK
- NuGet 패키지로 제공되므로 별도 설치가 필요하지 않습니다. 프로젝트 생성 시 자동으로 포함됩니다.
참고: 본문 후반부에서 언급할 DataGrid 등의 일부 컨트롤은 WinUI 3 기본 라이브러리에 포함되어 있지 않습니다. 이를 사용하려면 Windows Community Toolkit NuGet 패키지를 별도로 설치해야 합니다.
프로젝트 생성
Visual Studio에서 새 프로젝트를 만들 때 "Blank App, Packaged (WinUI 3 in Desktop)" 템플릿을 선택합니다. C++/WinRT 언어를 선택하면 이 글의 예제와 동일한 환경이 구성됩니다.
프로젝트가 생성되면 pch.h 파일에서 Windows App SDK 헤더들이 포함되어 있는지 확인합니다. 접근성 관련 기능은 별도의 추가 설정 없이 바로 사용할 수 있습니다.
테스트 환경 준비
접근성 구현을 검증하려면 스크린 리더가 필요합니다. Windows에 기본 포함된 Narrator를 사용할 수 있으며, 더 널리 사용되는 NVDA (NonVisual Desktop Access)를 설치하는 것을 권장합니다. NVDA는 무료 오픈소스 스크린 리더로, 실제 사용자들이 많이 사용하므로 테스트에 적합합니다.
UI Automation Control Patterns 목록
1부에서 Control Patterns이 UIA의 핵심 구성 요소라고 설명드렸는데요. 본격적인 시작에 앞서 WinUI 3에서 사용 가능한 모든 Control Patterns를 카테고리별로 살펴보고 넘어가도록 하겠습니다. 이 목록은 개발 중에 "이 컨트롤에 어떤 패턴을 적용해야 하지?"라는 질문에 답하는 참고 자료로 활용할 수 있기 때문입니다.
실행 및 호출 패턴
이 카테고리의 패턴들은 사용자가 컨트롤을 활성화하거나 실행할 때 사용됩니다.
- Invoke Pattern
- 버튼 클릭, 하이퍼링크 활성화, 메뉴 항목 선택처럼 단일 동작을 수행하는 컨트롤에 사용됩니다. 가장 기본적이고 자주 사용되는 패턴입니다.
- Toggle Pattern
- 체크박스, 토글 버튼, 토글 스위치처럼 켜짐/꺼짐 상태를 전환하는 컨트롤에 적용됩니다. ToggleState 속성을 통해 현재 상태(On, Off, Indeterminate)를 노출합니다.
값 관련 패턴
사용자가 값을 읽거나 설정할 수 있는 컨트롤에 적용됩니다.
- Value Pattern
- 텍스트 입력 필드, 날짜 선택기처럼 문자열 값을 다루는 컨트롤에 사용됩니다. Value 속성으로 현재 값을 읽고, SetValue 메서드로 값을 설정합니다.
- RangeValue Pattern
- 슬라이더, 진행률 표시줄, 스피너, 볼륨 조절기처럼 숫자 범위 내에서 값을 조정하는 컨트롤에 적용됩니다. Value, Minimum, Maximum, SmallChange, LargeChange 속성을 제공합니다. 이 패턴은 커스텀 컨트롤에서 매우 자주 사용되므로 뒤에서 별점 위젯 예제를 통해 상세히 다룹니다.
선택 패턴
목록이나 그룹에서 항목을 선택하는 기능을 제공합니다.
- Selection Pattern
- 리스트박스, 콤보박스, 탭 컨트롤처럼 자식 항목 중 하나 이상을 선택할 수 있는 컨테이너 컨트롤에 적용됩니다.
- SelectionItem Pattern
- Selection Pattern을 지원하는 컨테이너의 개별 항목에 적용됩니다. IsSelected 속성과 Select, AddToSelection, RemoveFromSelection 메서드를 제공합니다.
확장/축소 패턴
콘텐츠를 펼치거나 접는 기능을 제공합니다.
- ExpandCollapse Pattern
- 트리뷰 노드, 아코디언, 드롭다운 메뉴, 콤보박스처럼 확장/축소되는 컨트롤에 적용됩니다. ExpandCollapseState 속성으로 현재 상태(Collapsed, Expanded, PartiallyExpanded, LeafNode)를 알 수 있습니다.
스크롤 패턴
스크롤 가능한 영역을 제어합니다.
- Scroll Pattern
- 스크롤바가 있는 리스트, 텍스트 영역, 문서 뷰어 같은 컨트롤에 적용됩니다. 수평/수직 스크롤 위치와 뷰포트 크기 정보를 제공합니다.
- ScrollItem Pattern
- 가상화된 리스트에서 특정 항목이 보이도록 스크롤하는 기능을 제공합니다. ScrollIntoView 메서드가 핵심입니다.
그리드 및 테이블 패턴
표 형식의 데이터를 다루는 컨트롤에 사용됩니다.
- Grid Pattern
- 행과 열로 구성된 컨테이너에 적용됩니다. RowCount, ColumnCount 속성과 GetItem 메서드로 특정 셀에 접근할 수 있습니다.
- GridItem Pattern
- Grid 내의 개별 셀에 적용됩니다. Row, Column, RowSpan, ColumnSpan 속성으로 셀의 위치와 크기를 알 수 있습니다.
- Table Pattern
- 헤더가 있는 테이블에 적용됩니다. Grid Pattern을 확장하여 행/열 헤더 정보를 제공합니다.
- TableItem Pattern
- Table 내의 개별 셀에 적용되며, 해당 셀과 연관된 헤더 정보를 제공합니다.
텍스트 패턴
텍스트 콘텐츠를 다루는 컨트롤에 사용됩니다.
- Text Pattern
- 텍스트 편집기, 문서 뷰어처럼 서식 있는 텍스트를 포함하는 컨트롤에 적용됩니다. 텍스트 범위 선택, 검색, 서식 정보 접근 등의 기능을 제공합니다.
- Text Pattern 2
- Text Pattern의 확장 버전으로, 캐럿 이동과 주석 지원 등 추가 기능을 제공합니다.
- TextChild Pattern
- Text Pattern을 지원하는 컨테이너 내에 포함된 자식 요소(예: 텍스트 내 이미지)에 적용됩니다.
- TextEdit Pattern
- 편집 가능한 텍스트 컨트롤에 적용되며, 텍스트 변경 이벤트와 입력 컴포지션 정보를 제공합니다.
창 및 도킹 패턴
창 관리와 레이아웃에 관련된 패턴입니다.
- Window Pattern
- 최상위 창, 대화상자, 팝업에 적용됩니다. 창의 최소화, 최대화, 복원, 닫기 기능과 모달 상태 정보를 제공합니다.
- Dock Pattern
- 도킹 가능한 도구 모음이나 패널에 적용됩니다. 현재 도킹 위치(Top, Bottom, Left, Right, Fill, None)를 나타냅니다.
- Transform Pattern
- 이동, 크기 조정, 회전이 가능한 요소에 적용됩니다. 디자이너 도구나 그래픽 편집기에서 주로 사용됩니다.
- Transform Pattern 2
- Transform Pattern의 확장 버전으로, 줌 기능을 추가로 지원합니다.
드래그 앤 드롭 패턴
끌어서 놓기 작업을 지원합니다.
- Drag Pattern
- 드래그 가능한 요소에 적용됩니다. 이것은 드래그 중인 항목의 정보를 제공합니다.
- DropTarget Pattern
- 드롭 대상이 될 수 있는 영역에 적용됩니다. 현재 드롭 효과(Copy, Move, Link, None)를 나타냅니다.
기타 패턴
특수한 용도의 패턴들입니다.
- Annotation Pattern
- 문서의 주석이나 메모에 적용됩니다. 주석의 작성자, 날짜, 유형 정보를 제공합니다.
- MultipleView Pattern
- 여러 뷰 모드를 지원하는 컨트롤(예: Windows 탐색기의 아이콘/목록/자세히 보기)에 적용됩니다.
- ItemContainer Pattern
- 가상화된 컨테이너에서 항목을 검색하는 기능을 제공합니다.
- VirtualizedItem Pattern
- 가상화로 인해 아직 실체화되지 않은 항목을 화면에 표시하도록 요청하는 기능을 제공합니다.
- SynchronizedInput Pattern
- 동기화된 입력을 기다리는 컨트롤에 적용됩니다. 보안 관련 입력이나 특수 입력 대기 상태를 나타냅니다.
- Styles Pattern
- 리치 텍스트의 스타일 정보(글꼴, 색상, 볼드 등)를 제공합니다.
- Spreadsheet Pattern
- 스프레드시트 애플리케이션의 시트에 적용됩니다.
- SpreadsheetItem Pattern
- 스프레드시트의 개별 셀에 적용되며, 수식 정보 등을 제공합니다.
- ObjectModel Pattern
- 복잡한 객체 모델을 가진 문서(예: Office 문서)에서 내부 객체에 접근하는 기능을 제공합니다.
- CustomNavigation Pattern
- 표준 트리 탐색으로 접근하기 어려운 복잡한 UI 구조에서 사용자 정의 탐색을 제공합니다.
- LegacyIAccessible Pattern
- MSAA(Microsoft Active Accessibility)와의 하위 호환성을 위한 브리지 패턴입니다. 레거시 애플리케이션 지원에 사용됩니다.
패턴 선택 가이드
하나의 컨트롤이 여러 패턴을 동시에 가질 수 있습니다. 예를 들어 편집 가능한 콤보박스는 ExpandCollapse, Selection, Value 패턴을 모두 지원합니다.
WinUI 3의 기본 제공 컨트롤들은 적절한 패턴을 이미 구현하고 있으므로, 개발자가 별도로 패턴을 구현할 필요가 없습니다. 패턴 구현이 필요한 경우는 커스텀 컨트롤을 만들 때입니다.
기본 폼 만들기: AutomationProperties 활용
그럼 예고했듯 가장 기본적인 접근성 구현부터 시작해보겠습니다. 사용자 등록 폼을 만들면서 AutomationProperties의 사용법을 익힙니다.
예제 시나리오
이름, 이메일을 입력받고, 이용약관 동의 후 제출하는 간단한 폼을 만듭니다. 스크린 리더 사용자가 각 입력 필드의 용도를 명확히 이해할 수 있어야 합니다.
XAML 구현
아래 코드는 접근성을 고려한 폼의 XAML 구현입니다.
<StackPanel Spacing="16" Padding="24">
<!-- 이름 입력 필드 -->
<StackPanel>
<TextBlock x:Name="NameLabel" Text="이름" />
<TextBox
x:Name="NameTextBox"
AutomationProperties.LabeledBy="{x:Bind NameLabel}"
AutomationProperties.IsRequiredForForm="True"
PlaceholderText="홍길동" />
</StackPanel>
<!-- 이메일 입력 필드 -->
<StackPanel>
<TextBlock x:Name="EmailLabel" Text="이메일 주소" />
<TextBox
x:Name="EmailTextBox"
AutomationProperties.LabeledBy="{x:Bind EmailLabel}"
AutomationProperties.IsRequiredForForm="True"
InputScope="EmailSmtpAddress"
PlaceholderText="example@email.com" />
</StackPanel>
<!-- 이용약관 동의 -->
<CheckBox
x:Name="AgreeCheckBox"
AutomationProperties.Name="이용약관에 동의합니다"
Content="이용약관에 동의합니다 (필수)" />
<!-- 제출 버튼 -->
<Button
x:Name="SubmitButton"
AutomationProperties.Name="등록 정보 제출"
Content="제출"
Click="SubmitButton_Click" />
<!-- 상태 메시지 -->
<TextBlock
x:Name="StatusMessage"
AutomationProperties.LiveSetting="Polite" />
</StackPanel>
핵심 AutomationProperties 설명
위 코드에서 사용된 AutomationProperties 속성들을 하나씩 살펴보겠습니다.
AutomationProperties.LabeledBy
AutomationProperties.LabeledBy="{x:Bind NameLabel}"
이 속성은 컨트롤의 레이블 역할을 하는 다른 요소를 지정합니다. 스크린 리더가 TextBox에 포커스하면 연결된 TextBlock의 텍스트를 함께 읽어줍니다. "이름, 편집"과 같이 읽히게 됩니다.
LabeledBy를 사용하면 시각적 레이블과 접근성 레이블을 일치시킬 수 있어, 유지보수가 쉬워집니다. 레이블 텍스트를 변경하면 자동으로 스크린 리더가 읽는 내용도 변경됩니다.
AutomationProperties.Name
AutomationProperties.Name="이용약관에 동의합니다"
컨트롤의 접근 가능한 이름을 직접 지정합니다. 레이블이 없거나 Content 속성만으로는 충분하지 않을 때 사용합니다.
CheckBox의 경우 Content 속성의 텍스트가 자동으로 Name이 되지만, 더 명확한 설명이 필요하거나 Content에 아이콘만 있는 경우 Name을 직접 지정합니다.
AutomationProperties.IsRequiredForForm
AutomationProperties.IsRequiredForForm="True"
이 속성은 해당 필드가 폼 제출에 필수임을 나타냅니다. 스크린 리더는 "필수 항목"이라고 추가로 안내합니다.
AutomationProperties.LiveSetting
AutomationProperties.LiveSetting="Polite"
이 속성은 Live Regions와 관련되며, 뒤에서 자세히 설명합니다.
데이터 바인딩과 접근성
WinUI 3에서 데이터 바인딩을 사용하면 접근성 정보도 자연스럽게 동기화됩니다.
<TextBox
Text="{x:Bind ViewModel.UserName, Mode=TwoWay}"
AutomationProperties.Name="{x:Bind ViewModel.UserNameLabel}" />
이 코드에서 AutomationProperties.Name도 바인딩되어 있습니다. ViewModel에서 UserNameLabel 값을 변경하면 스크린 리더가 읽는 레이블도 자동으로 변경됩니다. 이는 다국어 지원 애플리케이션에서 특히 유용합니다.
데이터 바인딩의 또 다른 장점은 컨트롤의 값이 변경될 때 UIA가 자동으로 PropertyChanged 이벤트를 발생시킨다는 것입니다. 개발자가 별도로 이벤트를 발생시키는 코드를 작성할 필요가 없습니다.
예를 들어 TextBox의 Text 속성이 바인딩되어 있고 값이 변경되면, UIA는 Value Pattern의 ValueChanged 이벤트를 자동으로 발생시킵니다. 보조 기술은 이 이벤트를 수신하여 사용자에게 변경 사항을 알립니다.
자주 사용되는 AutomationProperties
AutomationProperties.HelpText: 컨트롤 사용 방법에 대한 추가 설명을 제공합니다.
<TextBox
AutomationProperties.HelpText="비밀번호는 8자 이상이어야 합니다" />
AutomationProperties.AccessKey: 키보드 단축키를 지정합니다.
<Button
AutomationProperties.AccessKey="Alt+S"
Content="저장" />
AutomationProperties.AutomationId: 테스트 자동화를 위한 고유 식별자입니다. 사용자에게는 노출되지 않습니다.
<Button AutomationProperties.AutomationId="SubmitButton" />
AutomationProperties.HeadingLevel: 콘텐츠의 제목 수준을 지정합니다. 스크린 리더 사용자가 제목으로 빠르게 탐색할 수 있게 합니다.
<TextBlock
Text="계정 설정"
AutomationProperties.HeadingLevel="Level1" />
키보드 접근성 구현
마우스 사용이 어려운 사용자에게 키보드 접근성은 필수입니다. WinUI 3는 기본적으로 Tab 탐색을 지원하지만, 복잡한 UI에서는 추가 작업이 필요합니다.
Tab 순서 제어
Tab 키를 눌렀을 때 포커스가 이동하는 순서는 기본적으로 XAML에서 요소가 선언된 순서를 따릅니다. 하지만 시각적 배치와 논리적 순서가 다를 경우 TabIndex를 사용하여 명시적으로 지정할 수 있습니다.
<Grid>
<TextBox TabIndex="1" x:Name="FirstName" />
<TextBox TabIndex="2" x:Name="LastName" />
<Button TabIndex="3" Content="다음" />
</Grid>
TabIndex 값이 작을수록 먼저 포커스를 받습니다. 같은 값을 가진 요소들은 선언 순서대로 포커스가 이동합니다.
주의: TabIndex를 과도하게 사용하면 유지보수가 어려워집니다. 가능하면 XAML 구조 자체를 논리적 순서에 맞게 배치하는 것이 좋습니다.
포커스 제외
장식용 요소나 비활성화된 컨트롤은 Tab 탐색에서 제외해야 합니다.
<Image
Source="decoration.png"
IsTabStop="False" />
IsTabStop="False"를 설정하면 해당 요소는 Tab 탐색에서 건너뛰어집니다.
화살표 키 탐색 (XYFocusKeyboardNavigation 주의사항)
WinUI 3에는 XYFocusKeyboardNavigation이라는 속성이 있습니다. 이 속성은 주로 게임패드(Xbox 컨트롤러)나 리모컨 입력을 위해 설계된 기능입니다.
<StackPanel XYFocusKeyboardNavigation="Enabled">
<!-- 게임패드 사용 시 유용한 설정 -->
<Button Content="옵션 1" />
<Button Content="옵션 2" />
</StackPanel>
주의: 일반적인 데스크톱 애플리케이션에서 이 속성을 남용하면 Tab 키를 이용한 표준 순차 탐색을 방해할 수 있습니다. 키보드 사용자는 Tab으로 컨트롤 그룹 간 이동을, 화살표 키로 그룹 내 항목(예: 라디오 버튼) 이동을 기대합니다. 따라서 라디오 버튼 그룹과 같은 표준 동작은 컨트롤 자체 내장 기능을 우선 활용하고, XYFocusKeyboardNavigation은 10ft 경험(TV 환경 등)을 고려할 때 신중히 사용해야 합니다.
키보드 이벤트 처리
특정 키 입력에 반응해야 할 때는 KeyDown 또는 KeyUp 이벤트를 처리합니다.
다음 코드는 사용자가 Enter 키를 눌렀을 때 폼을 제출하는 예제입니다. 코드가 하는 일은 KeyDown 이벤트에서 Enter 키를 감지하고, 제출 버튼의 클릭 이벤트를 프로그래밍 방식으로 호출하는 것입니다.
// MainWindow.xaml.cpp
void MainWindow::FormPanel_KeyDown(
IInspectable const& sender,
KeyRoutedEventArgs const& e)
{
if (e.Key() == VirtualKey::Enter)
{
// Enter 키로 폼 제출
SubmitButton_Click(nullptr, nullptr);
e.Handled(true);
}
}
포커스 표시 개선
WinUI 3는 기본적으로 포커스 표시를 제공하지만, 고대비 테마나 특수한 디자인에서는 커스터마이징이 필요할 수 있습니다.
<Button Content="저장">
<Button.Resources>
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="#0078D4" />
<SolidColorBrush x:Key="ButtonBorderBrushFocused" Color="#005A9E" />
<Thickness x:Key="ButtonBorderThicknessFocused">2</Thickness>
</Button.Resources>
</Button>
중요: 포커스 표시를 완전히 제거하면 안 됩니다. 시각적으로 포커스 위치를 확인하는 사용자에게 필수적인 정보입니다.
Live Regions: 동적 콘텐츠 알림
화면의 일부 영역이 사용자 조작 없이 변경될 때, 스크린 리더 사용자에게 이를 알려야 합니다. 예를 들어 폼 제출 결과, 실시간 업데이트, 오류 메시지 등이 해당합니다. 이때 Live Regions를 사용합니다.
LiveSetting 속성
AutomationProperties.LiveSetting 속성으로 Live Region을 지정합니다.
<TextBlock
x:Name="StatusMessage"
AutomationProperties.LiveSetting="Polite"
Text="" />
LiveSetting은 세 가지 값을 가질 수 있습니다.
- Off: 기본값입니다. 변경 사항을 알리지 않습니다.
- Polite: 사용자의 현재 작업을 방해하지 않고, 스크린 리더 메시지 출력이 끝난 후 변경 사항을 알립니다. 상태 업데이트, 성공 메시지에 적합합니다.
- Assertive: 즉시 변경 사항을 알립니다. 긴급한 오류 메시지나 경고에 사용하되, 남용하면 사용자 경험을 해칠 수 있습니다.
코드에서 Live Region 업데이트
Live Region의 텍스트를 변경하면 자동으로 보조 기술에 알림이 전달됩니다.
다음 코드는 폼 제출 후 상태 메시지를 업데이트하는 예제입니다. 코드가 하는 일은 StatusMessage TextBlock의 Text 속성을 변경하여 등록 성공 메시지를 표시하는 것이며, LiveSetting이 Polite로 설정되어 있으므로 스크린 리더가 이 변경을 자동으로 읽어줍니다.
// MainWindow.xaml.cpp
void MainWindow::SubmitButton_Click(
IInspectable const& sender,
RoutedEventArgs const& e)
{
// 폼 검증 및 제출 로직...
// 성공 메시지 표시 - Live Region이 자동으로 알림
StatusMessage().Text(L"등록이 완료되었습니다. 확인 이메일을 발송했습니다.");
}
AccessibilityAnnounce 메서드 (안전한 구현)
텍스트 변경 없이 알림만 보내야 할 경우, AutomationPeer의 RaiseAutomationEvent를 사용합니다. 단, 정적 텍스트 요소(TextBlock)는 기본적으로 UIA 최적화로 인해 AutomationPeer가 생성되지 않을 수 있습니다. 따라서 안전한 코드가 필요합니다.
다음 코드는 프로그래밍 방식으로 스크린 리더에 알림을 보내는 예제입니다.
void MainWindow::AnnounceMessage(hstring const& message)
{
// TextBlock은 기본적으로 Peer가 없을 수 있으므로 XAML에서
// AutomationProperties.AccessibilityView="Control" 설정이 권장됩니다.
auto peer = FrameworkElementAutomationPeer::FromElement(StatusMessage());
// Peer가 유효한지 반드시 확인해야 합니다.
if (peer)
{
peer.RaiseAutomationEvent(AutomationEvents::LiveRegionChanged);
}
}
Live Region 사용 시 주의사항
다시 언급하지만, Live Region의 오남용은 사용자 경험을 망치게 됩니다. Live Region을 과도하게 사용하면 스크린 리더가 쉬지 않고 메시지를 읽어 사용자를 피로하게 하기 때문입니다. 반드시 알려야하는 이벤트가 발생했을 때 사용하십시오.
실시간으로 빠르게 변하는 값(예: 다운로드 진행률)은 매 변화마다 알리지 말고, 의미 있는 시점(10%, 50%, 100% 등)에만 알립니다.
void MainWindow::UpdateProgress(int percentage)
{
ProgressBar().Value(percentage);
// 10% 단위로만 알림
if (percentage % 10 == 0)
{
StatusMessage().Text(hstring(L"다운로드 ") +
to_hstring(percentage) + L"% 완료");
}
}
복잡한 컨트롤의 접근성 원리
ListView, DataGrid, 모달 대화상자 같은 복잡한 컨트롤은 여러 Control Patterns를 조합하여 접근성을 제공합니다. 이 섹션에서는 이들 컨트롤의 접근성이 어떤 원리로 동작하는지 개념 수준에서 살펴봅니다. 상세한 구현 예제는 분량 관계상 추후 별도 글에서 다룰 예정입니다.
ListView와 GridView의 접근성
ListView는 복잡한 접근성 시나리오 중 하나입니다. 대량의 데이터를 가상화하면서도 각 항목에 접근 가능해야 하기 때문입니다.
WinUI 3의 ListView는 다음 패턴들을 조합합니다.
- Selection Pattern: 리스트 전체에 적용되어 선택된 항목을 관리합니다.
- SelectionItem Pattern: 각 리스트 항목에 적용되어 선택 상태를 나타냅니다.
- ScrollItem Pattern: 화면에 보이지 않는 항목을 스크롤하여 표시합니다.
- ItemContainer Pattern: 가상화된 항목을 검색합니다.
ListView 항목의 접근성 이름은 기본적으로 항목 템플릿의 첫 번째 텍스트 요소에서 가져옵니다. 복잡한 항목 템플릿을 사용할 경우 AutomationProperties.Name을 명시적으로 설정해야 합니다.
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:Contact">
<Grid AutomationProperties.Name="{x:Bind FullName}">
<Image Source="{x:Bind Photo}" />
<TextBlock Text="{x:Bind FullName}" />
<TextBlock Text="{x:Bind Email}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
DataGrid의 접근성
참고로, WinUI 3 기본 라이브러리에는 DataGrid 컨트롤이 포함되어 있지 않습니다. 주로 사용되는 DataGrid는 Windows Community Toolkit 패키지를 통해 제공됩니다.
DataGrid는 Grid Pattern과 Table Pattern을 사용하여 행과 열 구조를 표시합니다. 스크린 리더는 다음과 같이 DataGrid를 탐색합니다.
- 첫째, Grid 전체에서 RowCount와 ColumnCount를 읽어 표의 크기를 파악합니다.
- 둘째, 화살표 키 또는 Tab 키로 각 셀로 이동할 때 해당 셀의 Row, Column 위치와 연관된 헤더 정보를 읽습니다.
- 셋째, 셀 내용이 편집 가능한 경우 Value Pattern을 통해 값을 수정합니다.
개발자가 주의할 점은 열 헤더와 행 헤더의 관계를 올바르게 설정하는 것입니다. 헤더 정보가 없으면 사용자는 현재 셀이 어떤 열에 속하는지 알 수 없습니다.
모달 대화상자의 접근성
모달 대화상자(ContentDialog)는 Window Pattern을 사용합니다. 접근성 관점에서 중요한 원리들을 준수하는 것이 바람직합니다.
- 포커스 트래핑: 모달이 열리면 포커스가 대화상자 내부에 갇혀야 합니다. Tab 키를 눌러도 대화상자 밖으로 나가면 안 됩니다. WinUI 3의 ContentDialog는 이를 자동으로 처리합니다.
- 초기 포커스: 대화상자가 열릴 때 적절한 요소에 초기 포커스가 가야 합니다. 일반적으로 첫 번째 입력 필드나 기본 버튼입니다.
- 닫힘 알림: 대화상자가 닫히면 포커스가 대화상자를 연 원래 요소로 돌아가야 합니다.
- 역할 알림: 스크린 리더는 대화상자가 열릴 때 "대화 상자"라고 알립니다. 이는 Window Pattern의 WindowPattern.WindowInteractionState 속성을 통해 제공됩니다.
<ContentDialog
x:Name="ConfirmDialog"
Title="삭제 확인"
PrimaryButtonText="삭제"
CloseButtonText="취소"
DefaultButton="Close">
<TextBlock Text="이 항목을 삭제하시겠습니까?" />
</ContentDialog>
ContentDialog의 Title은 자동으로 대화상자의 접근 가능한 이름이 됩니다. DefaultButton 속성은 Enter 키를 눌렀을 때 어떤 버튼이 활성화될지, 그리고 시각적으로 어떤 버튼이 강조될지를 결정합니다.
커스텀 컨트롤 접근성: 별점 위젯 구현
여러 차례 언급했듯 WinUI 3의 기본 컨트롤을 사용할 때 접근성이 자동으로 제공됩니다. 하지만 완전히 새로운 컨트롤을 만들 때는 Automation Peer를 직접 구현해야 합니다.
이 섹션에서는 별점 위젯을 예제로 Automation Peer 구현 방법을 상세히 알아봅니다.
별점 위젯이란?
별점 위젯은 사용자가 별 아이콘을 클릭하여 1점부터 5점까지 평가할 수 있는 UI 컨트롤입니다. Amazon, Google Play 등 다양한 서비스에서 사용되는 친숙한 패턴입니다.
이 컨트롤은 RangeValue Pattern을 구현하기에 적합합니다. RangeValue Pattern은 슬라이더, 볼륨 조절기, 진행률 표시줄, 밝기 조절 등 숫자 범위 내에서 값을 조정하는 모든 컨트롤에 활용됩니다.
IDL(Interface Definition Language) 정의
C++/WinRT에서 커스텀 컨트롤과 그 Peer를 사용하기 위해서는 IDL 정의가 필수적입니다.
RatingControl.idl
namespace YourApp
{
runtimeclass RatingControl : Microsoft.UI.Xaml.Controls.Control
{
RatingControl();
Double Value;
Double Minimum;
Double Maximum;
static Microsoft.UI.Xaml.DependencyProperty ValueProperty{ get; };
static Microsoft.UI.Xaml.DependencyProperty MinimumProperty{ get; };
static Microsoft.UI.Xaml.DependencyProperty MaximumProperty{ get; };
}
// Automation Peer 클래스 정의
runtimeclass RatingControlAutomationPeer : Microsoft.UI.Xaml.Automation.Peers.FrameworkElementAutomationPeer,
Microsoft.UI.Xaml.Automation.Provider.IRangeValueProvider
{
RatingControlAutomationPeer(YourApp.RatingControl owner);
}
}
기본 컨트롤 구현
먼저 접근성 없이 기본 컨트롤을 구현합니다.
RatingControl.xaml (컨트롤 템플릿)
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:YourApp">
<Style TargetType="local:RatingControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:RatingControl">
<StackPanel
x:Name="StarPanel"
Orientation="Horizontal"
Background="Transparent">
<FontIcon x:Name="Star1" Glyph="&#xE735;" />
<FontIcon x:Name="Star2" Glyph="&#xE735;" />
<FontIcon x:Name="Star3" Glyph="&#xE735;" />
<FontIcon x:Name="Star4" Glyph="&#xE735;" />
<FontIcon x:Name="Star5" Glyph="&#xE735;" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Automation Peer 클래스 생성
Automation Peer는 컨트롤과 UIA 사이의 어댑터 역할을 합니다. 모든 커스텀 컨트롤의 Automation Peer는 FrameworkElementAutomationPeer를 상속받습니다.
다음 코드는 RatingControlAutomationPeer의 헤더 파일입니다.
RatingControlAutomationPeer.h
#pragma once
#include "RatingControl.h"
#include "RatingControlAutomationPeer.g.h" // IDL로부터 생성된 헤더
namespace winrt::YourApp::implementation
{
struct RatingControlAutomationPeer :
RatingControlAutomationPeerT<RatingControlAutomationPeer>
{
RatingControlAutomationPeer(YourApp::RatingControl const& owner);
// AutomationPeer 오버라이드
hstring GetClassNameCore();
winrt::Microsoft::UI::Xaml::Automation::Peers::AutomationControlType
GetAutomationControlTypeCore();
hstring GetNameCore();
winrt::Windows::Foundation::IInspectable GetPatternCore(
winrt::Microsoft::UI::Xaml::Automation::Peers::PatternInterface patternInterface);
// IRangeValueProvider 구현
double Value();
bool IsReadOnly();
double Maximum();
double Minimum();
double SmallChange();
double LargeChange();
void SetValue(double value);
private:
winrt::weak_ref<YourApp::RatingControl> m_owner;
};
}
기본 메타데이터 구현
Automation Peer가 반환해야 하는 기본 정보들을 구현합니다.
다음 코드는 GetClassNameCore와 GetAutomationControlTypeCore 메서드의 구현입니다.
// RatingControlAutomationPeer.cpp
hstring RatingControlAutomationPeer::GetClassNameCore()
{
return L"RatingControl";
}
AutomationControlType RatingControlAutomationPeer::GetAutomationControlTypeCore()
{
// 슬라이더 유형으로 지정 - 범위 값을 조정하는 컨트롤이므로
return AutomationControlType::Slider;
}
hstring RatingControlAutomationPeer::GetNameCore()
{
// AutomationProperties.Name이 설정되어 있으면 그 값 사용
auto name = FrameworkElementAutomationPeer::GetNameCore();
if (!name.empty())
{
return name;
}
// 기본 이름 제공
return L"평점";
}
RangeValue Pattern 구현
RangeValue Pattern은 숫자 범위 내에서 값을 조정하는 기능을 노출합니다.
다음 코드는 GetPatternCore 메서드 구현입니다.
IInspectable RatingControlAutomationPeer::GetPatternCore(PatternInterface patternInterface)
{
if (patternInterface == PatternInterface::RangeValue)
{
// 이 Peer가 IRangeValueProvider를 구현하므로 자기 자신을 반환
return *this;
}
return FrameworkElementAutomationPeer::GetPatternCore(patternInterface);
}
다음 코드는 IRangeValueProvider 인터페이스의 속성 구현입니다.
double RatingControlAutomationPeer::Value()
{
if (auto owner = m_owner.get())
{
return owner.Value();
}
return 0.0;
}
double RatingControlAutomationPeer::Minimum()
{
if (auto owner = m_owner.get())
{
return owner.Minimum();
}
return 1.0;
}
double RatingControlAutomationPeer::Maximum()
{
if (auto owner = m_owner.get())
{
return owner.Maximum();
}
return 5.0;
}
double RatingControlAutomationPeer::SmallChange()
{
return 1.0; // 별 하나 단위로 변경
}
double RatingControlAutomationPeer::LargeChange()
{
return 1.0; // 큰 변경도 별 하나 단위
}
bool RatingControlAutomationPeer::IsReadOnly()
{
if (auto owner = m_owner.get())
{
return !owner.IsEnabled();
}
return true;
}
양방향 동기화 구현 (중요)
접근성 구현의 완성도를 위해 가장 중요한 부분입니다. 스크린 리더가 값을 바꿀 때뿐만 아니라, 사용자가 마우스로 값을 바꿀 때도 스크린 리더에게 알려야 합니다.
1. 스크린 리더가 값을 변경할 때 (SetValue)
// RatingControlAutomationPeer.cpp
void RatingControlAutomationPeer::SetValue(double value)
{
if (auto owner = m_owner.get())
{
if (!IsReadOnly())
{
// 범위 내로 값 제한
double newValue = std::max(Minimum(), std::min(Maximum(), value));
// 실제 컨트롤의 값을 변경
// (컨트롤 내부 로직에 의해 OnValueChanged가 호출될 것입니다)
owner.Value(newValue);
}
}
}
2. 컨트롤 값이 변경될 때 (마우스/키보드 입력)
컨트롤 코드 내부에서 DependencyPropertyChanged 이벤트가 발생할 때, Peer를 통해 UIA 이벤트를 발생시켜야 합니다.
// RatingControl.cpp
void RatingControl::OnValueChanged(DependencyPropertyChangedEventArgs const& args)
{
// 1. UI 업데이트 로직 (별 아이콘 채우기 등)
UpdateVisuals();
// 2. 접근성 이벤트 발생 (양방향 동기화)
if (auto peer = FrameworkElementAutomationPeer::FromElement(*this))
{
// 값이 변경되었음을 UIA에 알림 (이전 값 -> 새 값)
peer.RaisePropertyChangedEvent(
RangeValuePatternIdentifiers::ValueProperty(),
args.OldValue(),
args.NewValue());
}
}
이렇게 구현해야 마우스로 별점을 클릭했을 때도 스크린 리더가 변경된 값을 즉시 읽어줄 수 있습니다.
컨트롤과 Automation Peer 연결
컨트롤 클래스에서 OnCreateAutomationPeer 메서드를 오버라이드하여 Automation Peer를 연결합니다.
// RatingControl.cpp
AutomationPeer RatingControl::OnCreateAutomationPeer()
{
return winrt::make<RatingControlAutomationPeer>(*this);
}
키보드 이벤트 처리
접근성 완성을 위해 키보드로 값을 조정할 수 있어야 합니다.
// RatingControl.cpp
void RatingControl::OnKeyDown(KeyRoutedEventArgs const& e)
{
Control::OnKeyDown(e);
if (e.Handled())
return;
double currentValue = Value();
double newValue = currentValue;
switch (e.Key())
{
case VirtualKey::Left:
case VirtualKey::Down:
newValue = std::max(Minimum(), currentValue - 1);
break;
case VirtualKey::Right:
case VirtualKey::Up:
newValue = std::min(Maximum(), currentValue + 1);
break;
case VirtualKey::Home:
newValue = Minimum();
break;
case VirtualKey::End:
newValue = Maximum();
break;
default:
return;
}
if (newValue != currentValue)
{
Value(newValue);
e.Handled(true);
}
}
고대비(High Contrast) 지원 팁
접근성은 스크린 리더 지원뿐만 아니라 저시력 사용자를 위한 고대비 모드 지원도 포함합니다. 커스텀 컨트롤(RatingControl)을 그릴 때 별 아이콘의 색상을 노란색 등으로 하드코딩하면 고대비 모드(예: 검은 배경에 흰 텍스트)에서 보이지 않을 수 있습니다.
따라서 SystemColorWindowTextBrush와 같은 시스템 테마 브러시를 사용하거나, ThemeDictionaries를 활용해 고대비(HighContrast) 모드일 때 사용할 색상 리소스를 별도로 정의하는 것이 좋습니다.
Windows 접근성 검증 도구 알아보기
개발 중에 접근성 구현을 검증하는 것은 필수입니다. 이 섹션에서는 주요 검증 도구를 간략히 소개합니다. 각 도구의 상세한 사용 방법은 5부에서 다루도록 하겠습니다.
Accessibility Insights for Windows
Microsoft가 개발한 공식 접근성 검증 도구로, 2025년 현재까지 업데이트되고 있습니다. 무료로 사용할 수 있으며, 다음 기능을 제공합니다.
- Live Inspect: 마우스 또는 키보드 커서 아래의 요소에 대한 UIA 속성을 실시간으로 확인할 수 있습니다. 각 컨트롤의 Name, ControlType, 지원하는 Control Patterns 등을 즉시 볼 수 있습니다.
- FastPass: 자동화된 접근성 테스트를 실행하여 5분 내에 주요 문제를 발견합니다. 60개 이상의 접근성 규칙을 검사합니다.
- Full Test: 더 상세한 수동 및 자동 테스트를 조합하여 포괄적인 접근성 감사를 수행합니다.
- Tab Stops: Tab 키 탐색 순서를 시각적으로 확인할 수 있어, 논리적 순서가 올바른지 검증하기 용이합니다.
Inspect.exe
Windows SDK에 포함된 도구로, UIA 트리 구조를 탐색하고 속성을 확인할 수 있습니다. Accessibility Insights보다 저수준의 정보를 제공하므로, 복잡한 디버깅 상황에서 유용합니다.
NVDA
사용자 입장에서 실제 스크린 리더로 테스트하는 것은 자동화 도구보다 더 중요합니다. NVDA는 무료 오픈소스 스크린 리더로, 실제 사용자들이 많이 사용합니다. 개발 중에 NVDA를 켜고 키보드만으로 애플리케이션을 사용해보면 접근성 문제를 직접 확인할 수 있습니다.
간단한 자가 점검 체크리스트
검증 도구를 사용하기 전에 다음 항목들을 스스로 점검해볼 수 있습니다.
- 모든 대화형 요소(버튼, 입력 필드, 체크박스 등)에 의미 있는 접근 가능한 이름(Name)이 있는가?
- 키보드만으로 모든 기능에 접근할 수 있는가?
- Tab 순서가 시각적 순서와 일치하는가?
- 동적으로 변하는 콘텐츠가 Live Region으로 알림되는가?
- 커스텀 컨트롤에 적절한 Automation Peer가 구현되어 있는가?
글을 맺으며
이번 2부에서는 WinUI 3를 사용하여 실제로 접근성있는 애플리케이션을 구현하는 방법을 살펴보았습니다.
WinUI 3는 프레임워크 수준에서 대부분의 접근성을 자동으로 지원합니다. 기본 제공 컨트롤들은 적절한 Control Patterns를 이미 구현하고 있으므로, 개발자는 AutomationProperties를 통해 레이블과 추가 정보만 제공하면 됩니다.
데이터 바인딩은 접근성과 자연스럽게 통합됩니다. 바인딩된 속성이 변경되면 UIA 이벤트가 자동으로 발생하여 보조 기술에 전달됩니다.
커스텀 컨트롤을 만들 때만 Automation Peer를 직접 구현해야 합니다. 이때 중요한 것은 컨트롤의 기능에 맞는 Control Pattern을 선택하고, 키보드 접근성 및 양방향 이벤트 동기화를 함께 구현하는 것입니다.
3부에서는 Win32 API를 사용하여 접근성을 구현하는 방법을 다룹니다. IRawElementProviderSimple 인터페이스를 직접 구현하고, UIA Provider를 수동으로 등록하는 과정을 살펴볼 예정입니다. WinUI 3가 내부적으로 처리해주는 것들을 직접 구현함으로써, UIA의 동작 원리를 더 깊이 이해할 수 있을 것입니다. 긴 글 읽어주셔서 감사합니다.