아티클

하이브리드 앱 접근성 향상: JavaScript 브릿지를 활용한 접근성 서비스 감지 가이드

엔비전스 접근성 2025-05-21 14:40:32

하이브리드 앱 접근성 향상: JavaScript 브릿지를 활용한 접근성 서비스 감지 가이드

안녕하세요, 엔비전스입니다.

들어가며: 동등한 접근성을 위한 도전

모든 사용자에게 동등한 웹 경험을 제공하는 것이 우리의 궁극적인 목표입니다. 그러나 현실적으로, 특히 다양한 기기와 환경에서 완전히 동일한 경험을 제공하기 어려운 경우가 있습니다. 예를 들어, 자동 재생되는 콘텐츠나 복잡한 애니메이션은 스크린 리더 사용자에게 혼란을 줄 수 있습니다. 이런 상황에서 우리는 접근성 향상을 위해 필요한 최소한의 조정을 해야 합니다.

웹 환경의 접근성 감지 차이점

접근성을 향상시키기 위한 첫 걸음은 사용자의 접근성 상태를 감지하는 것입니다. 하지만 이 부분에서 플랫폼별로 중요한 차이가 있습니다:

  • 네이티브 앱: 운영 체제 API를 통해 접근성 서비스(VoiceOver/TalkBack) 상태를 직접 감지 가능
  • 웹뷰(앱 내 웹): 네이티브 코드와 JavaScript 브릿지를 통해 접근성 상태 정보 전달 가능
  • 데스크톱 브라우저: CSS prefers-reduced-motion과 같은 OS 선호도는 감지 가능하지만, 스크린 리더 활성화 여부는 감지 불가능

바로 여기에 핵심 문제가 있습니다. 일반 웹 환경에서는 기술적 제한으로 인해 스크린 리더 활성화 여부를 감지할 수 없지만, 앱 내 웹뷰 환경에서는 JavaScript Bridge를 통해 이러한 정보를 웹 페이지에 전달할 수 있습니다.

JavaScript Bridge란 무엇인가?

JavaScript Bridge는 네이티브 앱 코드와 웹뷰 내 JavaScript 코드 간의 통신을 가능하게 하는 메커니즘입니다. 이는 웹과 네이티브 코드 사이의 '다리' 역할을 합니다.

작동 원리

  • 네이티브에서 웹으로: 네이티브 앱에서 접근성 상태를 감지하고 이 정보를 웹뷰에 주입된 JavaScript 함수 호출을 통해 전달
  • 웹에서의 처리: 웹 페이지에 미리 정의된 JavaScript 함수가 이 정보를 받아 UI를 적절히 조정

브릿지는 일방향(네이티브→웹)으로 동작할 수도 있고, 양방향으로 통신할 수도 있습니다. 접근성 상태 전달의 경우 주로 네이티브에서 웹으로의 전달이 핵심입니다.

웹 개발자와 앱 개발자 간의 효과적인 협업

JavaScript Bridge를 성공적으로 구현하기 위해서는 웹 개발자와 앱 개발자 간의 원활한 소통과 협업이 필수적입니다. 각자의 역할과 책임을 명확히 하고, 일관된 방식으로 통신하는 것이 중요합니다.

협업 워크플로우

  1. 명세 합의: 먼저 두 팀이 함께 인터페이스를 설계하고 함수 이름, 매개변수 형식 등을 명확히 정의합니다.
  2. 웹 측 먼저 구현: 일반적으로 웹 개발자가 먼저 JavaScript 함수를 정의하고 문서화합니다.
  3. 네이티브 측 구현: 앱 개발자는 합의된 인터페이스에 따라 네이티브 코드에서 해당 함수를 호출하는 방식을 구현합니다.
  4. 테스트 및 디버깅: 양쪽에서 함께 테스트하며 문제를 해결합니다.

인터페이스 정의 시 고려사항

  • 함수 명명 규칙: 직관적이고 일관된 이름(예: 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 함수로 전달합니다.

중요 구성 요소:

  1. 접근성 상태 감지기 - TouchExplorationStateChangeListener를 통한 TalkBack 감지
  2. WebView와 연결 - evaluateJavascript를 통한 웹 함수 호출
  3. 생명주기 관리 - 적절한 시점에 리스너 등록/해제

핵심 코드:

// 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)
}

구현 시 결정적 포인트:

  1. 페이지 로드 상태 확인: 웹뷰가 완전히 로드되기 전에 JavaScript를 실행하면 오류가 발생합니다.
  2. 초기 상태 전달: onResume에서 현재 TalkBack 상태를 즉시 전달해야 합니다.
  3. 메모리 관리: 리스너를 사용하지 않을 때는 반드시 해제해야 합니다.

3. iOS 앱에서의 접근성 브릿지 구현

iOS에서는 VoiceOver 상태 변화를 감지하고 WKWebView에 이 정보를 전달합니다.

핵심 구현 요소:

  1. VoiceOver 상태 감지 - NotificationCenter를 통한 상태 변경 감지
  2. WKWebView와 통신 - evaluateJavaScript 메서드를 통한 함수 호출
  3. 관찰자 생명주기 관리 - 등록 및 해제 관리
// 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)
}

구현 시 주의 사항:

  1. 페이지 로드 확인: iOS에서도 페이지가 완전히 로드된 후에만 JavaScript를 실행해야 합니다.
  2. 초기 상태 전달: 앱 시작 시 또는 화면 진입 시 현재 VoiceOver 상태를 즉시 전달해야 합니다.
  3. 메모리 관리: 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>

마무리하며

지금까지 우리는 자바스크립트 브릿지를 활용한 접근성 감지 및 최적화 방안에 대해 알아보았습니다. 이 기술이 적절한 곳에서 잘 활용되어, 더 많은 사용자에게 편리하고 평등한 경험을 제공하는 데 도움이 되길 좋겠습니다.

샘플 코드 다운로드

댓글 0
댓글을 작성하려면 해주세요.