main thread blocking으로 접근성 트리가 고장났던 이야기

시작
한국은 「장애인차별금지법」(장차법)과 「국가정보화기본법」에서 공공기관뿐만 아니라 일정 규모 이상의 민간 서비스에도 웹 접근성 준수 의무를 부과하고 있습니다.
특히 현재 재직중인 회사는 결제 서비스를 제공해주는 회사로, 금융·결제라는 필수 생활 서비스이기 때문에, 장애인·고령자도 동등하게 사용할 수 있도록 준수를 해야합니다.
웹 접근성 인증 심사를 진행을 하게되면서, 기존 서비스를 수정하는 과정에서 특정 상황에 스크린리더가 접근성 트리를 인식하지 못했던 이슈를 공유드립니다.
웹 접근성이란
웹 접근성(Web Accessibility)이란 장애인이나 고령자분들을 포함하여 모든 사용자들이 동등하게 웹 사이트에 접근하고 이용할 수 있도록 보장하는 것을 말합니다.
W3C에서 웹 접근성을 증가시키는 방법을 규정하고 제안하는 사양으로 WAI-ARIA 가 있으며,
한국웹접근성인증 평가원에서 제안하는 웹접근성 준수 고려사항으로는 다음과 같은 상황들이 있습니다.
- 시각
실명, 색각 이상,다양한 형태의 저시력을 포함한 시각 장애 - 이동성
파킨슨병, 근육병, 뇌성마비, 뇌졸중과 같은 조건으로 인한 근육 속도 저하, 근육 제어 손실로 말미암아 손을 쓰기 어렵거나 쓸 수 없는 상태 - 청각
영상, 음성 콘텐츠에 자막,원고, 수화등의 대체수단 부제로인한 인식이 불가능한 상태 - 인지
문제 해결과 논리 능력, 집중력, 기억력에 문제가 있는 정신 지체 및 발달 장애, 학습 장애(난독증, 난산증 등)
상황
유저가 정보를 입력하는 폼을 작성하고 버튼을 클릭했을때 서버로 요청을 보내 인증 혹은 검증을 수행하고
다음 화면으로 전환되거나, 실패했을 경우 레이어팝업을 통해 알려주는 흔한 흐름이 있었습니다.
웹 접근성 심사를 받는 서비스는 모던 웹 프론트엔드 환경이 아닌 레거시 서비스였고, jQuery의 ajax를 통해 http 요청을 하고있기도 했습니다.
jQuery의 ajax는 axios 혹은 fetch와 같은 http 통신을 담당하는 라이브러리 혹은 Web API가 보편화되기 전에 많이 사용을 하던 jQuery의 메소드입니다
문제의 시작은 iOS 환경에서 ajax를 통해 api 요청을 보내고, 응답을 받아 화면에 팝업을 보여주는 로직이 있었습니다.

(대략 이렇게 생김)
그리고 팝업 ui가 노출이 되었을때 팝업 내에 포커스가 이동이 되어야했습니다. 하지만 내부에서 포커스 이동이 불가능했던 점이 있었고
이로인해 스크린 리더 사용자는 팝업이 열렸다는 사실조차 인식하지 못했으며, 이는 시각적으로 불편한 사용자에게 팝업이 열렸다는것을 인지시켜주지 못하는 상황이 생겼습니다.
디버깅 결과, ajax 요청 시 async: false 옵션을 사용하고 있었던 부분이 원인이었습니다.
// example
$.ajax({
url: url,
data: data,
async: false,
type: "post",
error: function (result) {
response = {code : "200", message: "재시도 해주세요." };
if (fn) fn(response);
},
success: function (result) {
response = result;
if (fn) fn(response);
},
});
그리고 jQuery 라이브러리의 ajax 메소드는 다음과 같이 구현이 되어있습니다.
https://github.com/jquery/jquery/blob/main/src/ajax/xhr.js
XMLHttpRequest를 통해 요청을 보내고, xhr.open()의 3번째 파라미터인 async 옵션을 false로 설정하면 요청이 완료될 때까지 브라우저의 메인 스레드가 차단(block)됩니다.
문제는 이 동작이 브라우저의 렌더링 및 접근성 트리(Accessibility Tree) 생성 과정까지 일시적으로 정지시킨다는 점이었습니다.
특히 iOS Safari는 렌더링 파이프라인과 접근성 트리 갱신을 WebKit 내부에서 처리하는 구조를 가지고 있는데, 동기 요청 중에는 다음과 같은 현상이 발생합니다.
메인 스레드 블록(Main thread blocking)
XMLHttpRequest를 async: false로 호출하면 자바스크립트 실행 컨텍스트가 요청 완료까지 멈춥니다.
이로 인해 DOM 변경, 렌더링, 그리고 접근성 트리 갱신이 모두 일시 정지됩니다.
접근성 트리 미반영(Accessibility tree stale)
팝업을 띄우는 시점에 포커스를 이동시키려 해도, 브라우저가 새로운 노드(팝업 요소)를 접근성 트리에 반영하기 전에 스크립트가 실행됩니다.
결과적으로, 스크린 리더는 여전히 이전 포커스 위치만 인식하게 되어 팝업이 존재하지 않는 것처럼 인식하는 문제가 발생합니다.
iOS & Android 차이
다만, AOS에서는 포커스 이동이 정상이었습니다.
AOS에서 주로 사용하는 Screen Reader인 TalkBack과 IOS에서 사용하는 VoiceOver의 접근성트리를 해석하는 과정이 다르거나,
브라우저엔진의 차이정도로 이해를 했습니다. 결론적으로 ios에서는 어느정도의 sleep 후, 포커스이동이 정상적이지만 그렇지않은
동기 요청으로 접근성트리가 생성되고 해석되기전에 스크린리더의 포커스 이동이 되지 않았습니다.
이러한 이유로, 비동기(async) 요청으로 전환하는 것이 필수적이었습니다. async: true로 설정하고, 요청 완료 시점에 콜백 함수를 실행하도록 코드를 변경하여 문제를 해결되었습니다. VoiceOver에서 포커스 이동이 정상적으로 작동하고, 스크린리더가 팝업의 제목과 버튼을 정확히 읽어줄 수 있게 되었습니다.
다만, 해당 서비스는 여전히 IE11을 지원해야 했기 때문에, async/await와 같은 최신 비동기 문법을 사용할 수 없었습니다. 따라서 Promise 기반의 구조로 리팩터링하기보다는, 기존 jQuery ajax 콜백 패턴을 유지한 채 접근성 이슈만 최소 변경으로 해결하는 방향으로 수정을 진행했습니다.
추가로, 브라우저에서 동기적 HTTP 요청은 공식적으로 비권장(deprecated) 상태입니다.
- https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Synchronous_and_Asynchronous_Requests#synchronous_request
- https://xhr.spec.whatwg.org/
W3C와 WHATWG 명세에서는 다음과 같은 이유로 이를 지양하도록 권장합니다:
- 메인 스레드를 차단하여 UI가 멈춘 것처럼 느려짐
- 사용자 입력(스크린 리더, 키보드 포커스, 마우스 이벤트 등)이 무시됨
- 접근성 API 갱신이 지연되어 보조기기 사용자 경험이 저하됨
결론
이 문제는 단순히 브라우저에서 자바스크립트 동기적인 요청에 의한 이슈뿐만 아니라, 브라우저 렌더링 파이프라인과 접근성 트리의 동작과 연관이 있던 접근성 버그였습니다.
API요청을 비동기 요청으로 전환한 후에는 iOS와 안드로이드 모두에서 포커스 이동이 정상적으로 작동하며, 최종적으로 웹 접근성 인증마크(SA)를 획득할 수 있었습니다.