하이브리드 앱 접근성 향상: JavaScript 브릿지를 활용한 접근성 서비스 감지 가이드
하이브리드 앱 접근성 향상: JavaScript 브릿지를 활용한 접근성 서비스 감지 가이드
안녕하세요, 엔비전스입니다.
들어가며: 동등한 접근성을 위한 도전
모든 사용자에게 동등한 웹 경험을 제공하는 것이 우리의 궁극적인 목표입니다. 그러나 현실적으로, 특히 다양한 기기와 환경에서 완전히 동일한 경험을 제공하기 어려운 경우가 있습니다. 예를 들어, 자동 재생되는 콘텐츠나 복잡한 애니메이션은 스크린 리더 사용자에게 혼란을 줄 수 있습니다. 이런 상황에서 우리는 접근성 향상을 위해 필요한 최소한의 조정을 해야 합니다.
웹 환경의 접근성 감지 차이점
접근성을 향상시키기 위한 첫 걸음은 사용자의 접근성 상태를 감지하는 것입니다. 하지만 이 부분에서 플랫폼별로 중요한 차이가 있습니다:
- 네이티브 앱: 운영 체제 API를 통해 접근성 서비스(VoiceOver/TalkBack) 상태를 직접 감지 가능
- 웹뷰(앱 내 웹): 네이티브 코드와 JavaScript 브릿지를 통해 접근성 상태 정보 전달 가능
- 데스크톱 브라우저: CSS prefers-reduced-motion과 같은 OS 선호도는 감지 가능하지만, 스크린 리더 활성화 여부는 감지 불가능
바로 여기에 핵심 문제가 있습니다. 일반 웹 환경에서는 기술적 제한으로 인해 스크린 리더 활성화 여부를 감지할 수 없지만, 앱 내 웹뷰 환경에서는 JavaScript Bridge를 통해 이러한 정보를 웹 페이지에 전달할 수 있습니다.
JavaScript Bridge란 무엇인가?
JavaScript Bridge는 네이티브 앱 코드와 웹뷰 내 JavaScript 코드 간의 통신을 가능하게 하는 메커니즘입니다. 이는 웹과 네이티브 코드 사이의 '다리' 역할을 합니다.
작동 원리
- 네이티브에서 웹으로: 네이티브 앱에서 접근성 상태를 감지하고 이 정보를 웹뷰에 주입된 JavaScript 함수 호출을 통해 전달
- 웹에서의 처리: 웹 페이지에 미리 정의된 JavaScript 함수가 이 정보를 받아 UI를 적절히 조정
브릿지는 일방향(네이티브→웹)으로 동작할 수도 있고, 양방향으로 통신할 수도 있습니다. 접근성 상태 전달의 경우 주로 네이티브에서 웹으로의 전달이 핵심입니다.
웹 개발자와 앱 개발자 간의 효과적인 협업
JavaScript Bridge를 성공적으로 구현하기 위해서는 웹 개발자와 앱 개발자 간의 원활한 소통과 협업이 필수적입니다. 각자의 역할과 책임을 명확히 하고, 일관된 방식으로 통신하는 것이 중요합니다.
협업 워크플로우
- 명세 합의: 먼저 두 팀이 함께 인터페이스를 설계하고 함수 이름, 매개변수 형식 등을 명확히 정의합니다.
- 웹 측 먼저 구현: 일반적으로 웹 개발자가 먼저 JavaScript 함수를 정의하고 문서화합니다.
- 네이티브 측 구현: 앱 개발자는 합의된 인터페이스에 따라 네이티브 코드에서 해당 함수를 호출하는 방식을 구현합니다.
- 테스트 및 디버깅: 양쪽에서 함께 테스트하며 문제를 해결합니다.
인터페이스 정의 시 고려사항
- 함수 명명 규칙: 직관적이고 일관된 이름(예: onAccessibilityStatusChanged)을 사용합니다.
- 매개변수 형식: JSON과 같은 표준 형식을 사용하여 복잡한 데이터도 쉽게 전달할 수 있게 합니다.
- 오류 처리: 통신 실패 시의 대응 방안을 미리 정의합니다.
- 버전 관리: API가 변경될 경우를 대비한 버전 관리 전략을 수립합니다.
JavaScript 브릿지 구현 상세 설명
1. 웹 페이지에서의 최적 인터페이스 설계
웹 페이지에서 JavaScript 브릿지를 구현할 때는 명확하고 확장성 있는 인터페이스 설계가 중요합니다. 다음은 추천하는 함수 설계 방식과 잘못된 접근법의 비교입니다.
❌ 피해야 할 구현 방식: 플랫폼 종속적 접근법
// 특정 앱에서만 동작하는 직접적 처리
$(document).ready(function() {
if (typeof AndroidApp !== 'undefined') {
// Android 전용 코드...
} else if (typeof webkit !== 'undefined' && webkit.messageHandlers) {
// iOS 전용 코드...
}
});
✅ 권장하는 구현 방식: 통일된 인터페이스
// 1. 접근성 상태 변화를 처리할 전역 함수 정의
window.onAccessibilityStatusChanged = function(status) {
// status 객체 형식: {voiceOver: boolean, talkBack: boolean}
// - voiceOver: iOS의 VoiceOver 활성화 여부
// - talkBack: Android의 TalkBack 활성화 여부
// 2. 스크린 리더 활성화 여부 확인
const screenReaderActive = status && (status.voiceOver || status.talkBack);
// 3. 상태에 따른 UI 변경 처리
if (screenReaderActive) {
// 스크린 리더가 활성화된 경우 적용할 동작
if (typeof mySlider !== 'undefined' && mySlider.autoplay) {
mySlider.autoplay.stop(); // 자동 재생 중지
showAccessibilityNotice(true); // 접근성 모드 알림 표시
}
} else {
// 스크린 리더가 비활성화된 경우 적용할 동작
if (typeof mySlider !== 'undefined' && mySlider.autoplay) {
mySlider.autoplay.start(); // 자동 재생 시작
showAccessibilityNotice(false); // 접근성 모드 알림 숨김
}
}
};
함수 설계 핵심 요소:
- 전역 접근성: window 객체에 함수를 추가하여 어디서든 호출 가능하게 함
- 일관된 매개변수: 표준화된 JSON 객체로 다양한 플랫폼의 상태 정보 전달
- 명확한 네이밍: onAccessibilityStatusChanged와 같이 기능을 명확히 표현하는 이름 사용
- 독립적 설계: 특정 플랫폼에 종속되지 않는 설계로 확장성 확보
2. Android 앱에서의 접근성 브릿지 구현
Android에서는 AccessibilityManager를 사용하여 TalkBack과 같은 접근성 서비스의 상태를 감지하고, 이 정보를 WebView의 JavaScript 함수로 전달합니다.
중요 구성 요소:
- 접근성 상태 감지기 - TouchExplorationStateChangeListener를 통한 TalkBack 감지
- WebView와 연결 - evaluateJavascript를 통한 웹 함수 호출
- 생명주기 관리 - 적절한 시점에 리스너 등록/해제
핵심 코드:
// 1. 접근성 상태 변경 감지기 구현
private val touchExplorationListener =
AccessibilityManager.TouchExplorationStateChangeListener { enabled ->
// TalkBack 활성화 상태가 변경되면 호출됩니다
sendAccessibilityStatusToWebView(enabled)
}
// 2. WebView에 접근성 상태 전송 함수
private fun sendAccessibilityStatusToWebView(talkBackEnabled: Boolean) {
// 페이지 로드 완료 확인 (중요: 로드 전 호출 방지)
if (!pageLoaded) return
// 웹에서 정의한 함수를 호출하는 JavaScript 코드 생성
val js = """
window.onAccessibilityStatusChanged({
voiceOver: false, // iOS에서만 사용
talkBack: $talkBackEnabled // 현재 TalkBack 상태 전달
});
""".trimIndent()
// WebView에서 JavaScript 실행
webView.evaluateJavascript(js, null)
}
// 3. 적절한 시점에 리스너 등록/해제 (생명주기 관리)
override fun onResume() {
super.onResume()
// 화면이 활성화될 때 리스너 등록
accessibilityManager.addTouchExplorationStateChangeListener(touchExplorationListener)
// 현재 접근성 상태 즉시 전송 (중요!)
sendAccessibilityStatusToWebView(accessibilityManager.isTouchExplorationEnabled)
}
override fun onPause() {
super.onPause()
// 화면이 비활성화될 때 리스너 해제 (메모리 누수 방지)
accessibilityManager.removeTouchExplorationStateChangeListener(touchExplorationListener)
}
구현 시 결정적 포인트:
- 페이지 로드 상태 확인: 웹뷰가 완전히 로드되기 전에 JavaScript를 실행하면 오류가 발생합니다.
- 초기 상태 전달: onResume에서 현재 TalkBack 상태를 즉시 전달해야 합니다.
- 메모리 관리: 리스너를 사용하지 않을 때는 반드시 해제해야 합니다.
3. iOS 앱에서의 접근성 브릿지 구현
iOS에서는 VoiceOver 상태 변화를 감지하고 WKWebView에 이 정보를 전달합니다.
핵심 구현 요소:
- VoiceOver 상태 감지 - NotificationCenter를 통한 상태 변경 감지
- WKWebView와 통신 - evaluateJavaScript 메서드를 통한 함수 호출
- 관찰자 생명주기 관리 - 등록 및 해제 관리
// 1. VoiceOver 상태 변경 감지 및 처리
@objc func voiceOverStatusChanged() {
// UIAccessibility를 통해 현재 VoiceOver 상태 확인
let voiceOverEnabled = UIAccessibility.isVoiceOverRunning
// 웹뷰에 상태 전달
sendAccessibilityStatusToWebView(voiceOverEnabled)
}
// 2. 웹뷰에 접근성 상태 전달 함수
private func sendAccessibilityStatusToWebView(_ voiceOverEnabled: Bool) {
// 페이지 로드 완료 확인 (중요!)
guard isPageLoaded else { return }
// 웹 함수 호출을 위한 JavaScript 생성
let js = """
window.onAccessibilityStatusChanged({
voiceOver: \(voiceOverEnabled), // iOS VoiceOver 상태
talkBack: false // iOS에서는 미사용
});
"""
// WKWebView에서 JavaScript 실행
webView.evaluateJavaScript(js, completionHandler: nil)
}
// 3. VoiceOver 상태 변경 감지를 위한 관찰자 등록 (viewDidLoad에서)
override func viewDidLoad() {
super.viewDidLoad()
// 현재 VoiceOver 상태 저장
isVoiceOverRunning = UIAccessibility.isVoiceOverRunning
// VoiceOver 상태 변경 감지를 위한 관찰자 등록
NotificationCenter.default.addObserver(
self,
selector: #selector(voiceOverStatusChanged),
name: UIAccessibility.voiceOverStatusDidChangeNotification,
object: nil
)
// 웹뷰 로드 설정...
}
// 4. 관찰자 해제 (메모리 누수 방지)
deinit {
NotificationCenter.default.removeObserver(self)
}
구현 시 주의 사항:
- 페이지 로드 확인: iOS에서도 페이지가 완전히 로드된 후에만 JavaScript를 실행해야 합니다.
- 초기 상태 전달: 앱 시작 시 또는 화면 진입 시 현재 VoiceOver 상태를 즉시 전달해야 합니다.
- 메모리 관리: deinit에서 관찰자를 해제하여 메모리 누수를 방지합니다.
일반 웹에서의 접근성 최적화
앱 내 웹뷰와 달리, 일반 웹 브라우저에서는 스크린 리더 상태를 직접 감지할 수 없습니다. 그러나 다음과 같은 대안을 활용할 수 있습니다:
- 미디어 쿼리를 통한 모션 감소 감지
- 사용자 제어 옵션 제공
미디어 쿼리를 통한 모션 감소 감지
@media (prefers-reduced-motion: reduce) {
/* 모션 감소 모드일 때 애니메이션 제거 */
.carousel {
animation: none;
transition: none;
}
}
사용자 제어 옵션 제공
<!-- 접근성 배너 제어 버튼 예시 -->
<button id="banner-toggle" aria-pressed="false" aria-label="배너 자동재생 정지 또는 재개">
배너 정지
</button>
<script>
const bannerToggle = document.getElementById('banner-toggle');
let bannerPlaying = true;
bannerToggle.addEventListener('click', function() {
bannerPlaying = !bannerPlaying;
bannerToggle.setAttribute('aria-pressed', String(!bannerPlaying));
bannerToggle.textContent = bannerPlaying ? '배너 정지' : '배너 재생';
if (typeof myBanner !== 'undefined') {
if (bannerPlaying) {
myBanner.play();
} else {
myBanner.pause();
}
}
});
</script>
마무리하며
지금까지 우리는 자바스크립트 브릿지를 활용한 접근성 감지 및 최적화 방안에 대해 알아보았습니다. 이 기술이 적절한 곳에서 잘 활용되어, 더 많은 사용자에게 편리하고 평등한 경험을 제공하는 데 도움이 되길 좋겠습니다.