Accessibility Trait는 VoiceOver에 대한 요소의 역할과 동작을 결정하는 iOS 요소 속성입니다. 트레이트는 스크린 리더에게 요소가 어떻게 읽혀야 하는지, 어떤 제스처를 사용할 수 있는지(버튼, 헤더, 링크, 검색 필드 등)를 알려줍니다. Apple UIAccessibilityTraits, 2024에 따르면, 시스템은 비트 마스크를 사용하여 결합할 수 있는 15개 이상의 상수를 지원합니다. 올바르게 선택된 트레이트는 VoiceOver 사용자의 탐색 시간을 최대 50%까지 절약합니다.
주요 포인트
Accessibility Trait는 VoiceOver에 의미론적 역할을 나타내기 위해 UIView 요소에 설정되는 플래그입니다. 트레이트는 Apple 접근성 삼요소(Label(이름), Hint(설명), Trait(역할)) 중 하나입니다. iOS는 UIAccessibilityTraits 비트마스크(UInt64)를 사용하며, 각 비트는 특정 역할에 해당합니다. VoiceOver는 Label과 Hint 다음에 역할을 읽습니다: “전송 버튼. 양식을 엽니다” — UIAccessibilityTraitButton 트레이트 덕분에 “버튼”이 추가되었습니다.
기본적으로 UIButton은 UIAccessibilityTraitButton을, UILabel은 UIAccessibilityTraitStaticText를, UIImageView는 UIAccessibilityTraitImage를 받습니다. 사용자 정의 컨트롤을 사용할 때는 개발자가 수동으로 트레이트를 설정해야 합니다. Apple Human Interface Guidelines, 2024는 이를 “접근성을 보장하는 가장 중요한 단계 중 하나”라고 말합니다.
올바른 트레이트가 없으면 사용자는 어떤 제스처를 적용해야 할지 알 수 없습니다: 단일 탭(버튼 활성화), 더블 탭(확대), 스와이프 제스처(토글). 트레이트는 요소에서 어떤 VoiceOver 제스처가 활성화되는지 결정합니다.
UIAccessibilityTraits는 typealias UInt64입니다. 각 트레이트는 정확히 하나의 비트가 설정된 상수입니다. 예: UIAccessibilityTraitButton = 0x0000000000000001, UIAccessibilityTraitLink = 0x0000000000000002, UIAccessibilityTraitHeader = 0x0000000000000008. 결합은 비트별 OR로 이루어집니다: 0x0001 | 0x0008 = 0x0009. VoiceOver는 마스크를 분석하고 동작을 결정합니다.
iOS는 15개 이상의 트레이트 상수를 제공합니다. 90% 시나리오에서 사용되는 주요 항목을 살펴보겠습니다:
| 트레이트 | 상수 | VoiceOver 동작 |
|---|---|---|
| Button | UIAccessibilityTraitButton | 더블 탭으로 활성화 |
| Header | UIAccessibilityTraitHeader | 헤더로 빠른 탐색 |
| Link | UIAccessibilityTraitLink | 링크로 활성화 |
| StaticText | UIAccessibilityTraitStaticText | 읽기 전용, 활성화 없음 |
| SearchField | UIAccessibilityTraitSearchField | 특수 동작이 있는 검색 필드 |
| Image | UIAccessibilityTraitImage | 이미지, 활성화 제스처 없음 |
| Selected | UIAccessibilityTraitSelected | “선택됨” 상태 |
| PlaysSound | UIAccessibilityTraitPlaysSound | 활성화 시 소리 재생 |
| KeyboardKey | UIAccessibilityTraitKeyboardKey | 키보드 키 |
| TabBar | UIAccessibilityTraitTabBar | 탭 바 요소 |
상수는 iOS 3.0부터 UIKit에서 사용 가능합니다. iOS 14+는 .accessibilityAddTraits() 수정자를 통해 SwiftUI에서 UIAccessibilityTraits 지원을 추가했습니다.
UIAccessibilityTraitAdjustable — 조정 가능한 값용(슬라이더, 피커, 볼륨 슬라이더). VoiceOver는 accessibilityIncrement 및 accessibilityDecrement로 정의된 단계로 값을 변경하기 위해 위/아래로 스와이프할 수 있습니다. UIAccessibilityTraitUpdatesFrequently — 자주 변경되는 값이 있는 요소용(타이머, 진행 표시기). VoiceOver는 변경될 때마다 값을 읽지 않고 일시 중지합니다. UIAccessibilityTraitAllowsDirectInteraction — 사용자가 VoiceOver 제스처를 거치지 않고 직접 상호작용할 수 있는 요소용(키보드, 그림 그리기).
하나의 요소에 여러 트레이트를 동시에 가질 수 있습니다 — 결합은 비트별 OR(|)로 설정합니다. 예: 현재 선택된 버튼 — Button | Selected. VoiceOver는 “선택됨. 가격별로 필터링됨. 버튼.”이라고 읽습니다.
코드에서 트레이트 설정:
filterButton.accessibilityTraits.insert(.button)
filterButton.accessibilityTraits.insert(.selected)
// 또는 마스크를 통해:
filterButton.accessibilityTraits = [.button, .selected]
트레이트가 기본적으로 설정되지 않은 사용자 정의 UIView의 경우:
class CustomToggle: UIControl {
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isOn {
return [.button, .selected]
} else {
return .button
}
}
set {}
}
}
결합 규칙: 요소당 3-4개 이하의 트레이트. 과도한 트레이트(예: Button + Link + Header)는 VoiceOver 읽기가 너무 길고 혼란스럽게 만듭니다. Apple에 따르면, “추가 속성마다 사용자의 인지 부하가 증가합니다.”
SwiftUI에서는 .accessibilityAddTraits() 및 .accessibilityRemoveTraits() 수정자를 사용하여 트레이트를 설정합니다. 예: Text(“제목”).font(.largeTitle).accessibilityAddTraits(.isHeader). .isHeader 수정자는 UIAccessibilityTraitHeader를 추가합니다. SwiftUI 트레이트 목록: .isButton, .isHeader, .isLink, .isSelected, .isImage, .isSearchField, .isKeyboardKey, .isStaticText, .isSummaryElement, .isToggle, .playsSound, .startsMediaSession, .updatesFrequently, .allowsDirectInteraction, .causesPageTurn, .isModal, .tabBar.
Button 대신 StaticText — 시각적으로 버튼처럼 보이는 사용자 정의 컨트롤이 기본적으로 StaticText 트레이트를 받습니다. VoiceOver는 활성화 제스처를 제공하지 않으므로 사용자가 요소를 “누를” 수 없습니다. 해결책: 명시적으로 .button을 설정합니다.
트레이트가 없는 이미지 — 접근성이 활성화된 UIImageView는 실제로 사진을 확대하는 버튼이어도 Image 트레이트를 받습니다. .button과 Label “사진 확대”를 할당하세요. WWDC 2023, “Deliver an Exceptional Accessibility Experience”에 따르면, 새 앱 버전의 접근성 회귀 중 40%는 트레이트 불일치로 인해 발생합니다.
모든 요소에 Header — Header 트레이트는 화면의 구조적 헤더를 위한 것입니다. 모든 UILabel을 헤더로 만들면 VoiceOver 로터가 “헤더” 모드에서 쓸모없어집니다 — 모든 단어에서 멈춥니다.
트레이트 손실의 일반적인 원인은 리팩토링입니다: 개발자가 사용자 정의 표시를 위해 UIButton을 UIControl로 교체합니다. UIButton은 자동으로 Button 트레이트를 받지만 UIControl은 받지 않습니다. 리팩토링 후 명시적으로 accessibilityTraits = .button을 설정해야 합니다. 코드 리뷰에 확인 사항을 추가하세요: “UIButton을 UIControl로 교체한 경우 — 트레이트를 확인하세요.”
상태가 변하는 요소(예: 좋아요 버튼)의 경우 트레이트가 동적으로 변경되어야 합니다. “좋아요 안 함” 상태 — Button, “좋아요 함” 상태 — Button + Selected + Image(아이콘이 있는 경우). VoiceOver 읽기가 변경됩니다: “좋아요. 버튼.” vs “선택됨. 좋아요. 버튼.” Selected 트레이트가 충분하지 않은 경우 상태를 전달하기 위해 accessibilityValue를 사용하세요. 구독 버튼, 즐겨찾기, 필터 및 토글에 적용됩니다.
Android에는 트레이트에 직접 대응하는 것이 없습니다. 비트마스크 대신 다음이 사용됩니다:
Android의 사용자 정의 View에서는 onInitializeAccessibilityNodeInfo를 재정의해야 합니다:
class CustomButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
override fun onInitializeAccessibilityNodeInfo(
info: AccessibilityNodeInfo
) {
super.onInitializeAccessibilityNodeInfo(info)
info.className = "android.widget.Button"
info.isClickable = true
}
}
Flutter 개발자는 Semantics 위젯에서 semanticsRole 매개변수를 사용해야 합니다: button, header, image, link, textField 등. 또한 semanticsLabel 및 semanticsHint를 사용할 수 있습니다 — iOS 삼요소 Label + Hint + Trait의 완전한 대응입니다.
모바일 앱의 웹 버전(PWA, WebView)에서는 WAI-ARIA의 role 속성이 사용됩니다: role="button", role="heading", role="link". 이는 accessibilityTraits의 직접적인 대응입니다. 하이브리드 앱에서는 WebView가 ARIA 역할을 네이티브 접근성 계층에 전달하는지 확인하세요. 이를 위해 iOS에서는 UIAccessibilityContainerDataTable 프로토콜을, Android에서는 setAccessibilityDelegate를 사용합니다. JavaScript가 활성화된 WebView는 ARIA 역할을 올바르게 전달하지 못할 수 있으므로 별도로 테스트하세요.
Android에서는 AccessibilityNodeInfo에 사용자 정의 작업을 추가할 수 있습니다: AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK 및 ACTION_LONG_CLICK. 이는 추가 제스처가 있는 Button 트레이트에 해당합니다. 슬라이더에는 ACTION_SET_PROGRESS를 사용합니다 — Adjustable에 해당합니다. Spinner 및 DatePicker에는 — ACTION_SET_SELECTION, ACTION_SET_DATE 및 ACTION_SET_TIME을 사용합니다.
Xcode Accessibility Inspector는 iOS의 주요 도구입니다: 요소를 선택하고 Traits 필드를 보면 설정된 트레이트 목록이 표시됩니다. “요소” 모드의 VoiceOver 로터를 사용하면 화면의 모든 컨트롤을 탐색할 수 있습니다.
트레이트 확인을 위한 자동화된 Swift 테스트:
func testSubmitButtonTrait() {
let app = XCUIApplication()
app.launch()
let submitButton = app.buttons["제출"]
XCTAssertTrue(submitButton.isEnabled)
// XCUIElement는 트레이트에 직접 접근을 제공하지 않습니다
// 제스처 활성화를 통한 확인
submitButton.tap()
XCTAssertTrue(app.staticTexts["양식이 제출되었습니다"].exists)
}
VoiceOver를 통한 수동 확인: VoiceOver를 켜고, 요소로 스와이프한 후 더블 탭합니다 — Button이면 요소가 활성화되어야 합니다. 요소가 더블 탭에 반응하지 않으면 트레이트가 잘못된 것입니다. Rotor 제스처를 사용하여 모드 간 전환(“헤더”, “링크”, “버튼”) — 각 모드는 해당 트레이트가 있는 요소만 표시합니다.
iOS 14 이전에는 유닛 테스트에서 accessibilityTraits에 직접 접근할 수 없었습니다. iOS 14부터 속성을 사용할 수 있습니다: XCTAssertEqual(customButton.accessibilityTraits, .button). 사용자 정의 컨트롤을 확인하기 위해 유닛 테스트에서 이를 사용하세요. 특히 리팩토링이나 부모 클래스 변경 후에는 새 사용자 정의 UIView마다 트레이트의 정확성을 테스트하는 것이 좋습니다.
자주 묻는 질문
요소당 3-4개까지입니다. 더 많으면 VoiceOver 읽기가 중복됩니다. Button + Selected, Header + StaticText 조합을 사용하세요.
UIAccessibilityTraitButton입니다. iOS가 모든 UIButton 인스턴스에 자동으로 설정합니다. UIView에서 상속받아 버튼을 시뮬레이션하는 경우 트레이트를 수동으로 설정해야 합니다.
네, UIAccessibilityTraitAdjustable — 조정 가능한 값이 있는 요소용(슬라이더, 피커, 카운터). VoiceOver는 위/아래로 스와이프하여 값을 변경하고 현재 상태를 읽을 수 있습니다.
.accessibilityAddTraits() 수정자를 사용하세요: Text(“제목”).font(.title).accessibilityAddTraits(.isHeader). 이 메서드는 iOS 14+에서 작동합니다.
VoiceOver가 None 트레이트를 할당합니다. 요소에 역할이 없어 스크린 리더가 유형을 표시하지 않고 Label만 읽습니다. 사용자는 활성화 제스처를 사용할 수 있는지 알 수 없습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.