
제가 참여하고 있는 프로젝트는 한국형 웹 콘텐츠 접근성 지침인 KWCAG 2.2를 준수하는 것을 목표로 하고 있습니다. 웹 접근성 가이드라인을 준수하기 위해서는 다양한 요소들을 고려해야 하지만, 그 중에서도 모달은 특히 접근성 이슈가 발생하기 쉬운 컴포넌트입니다. 이번 포스팅에서는 모달을 구현할 때 고려해야 할 이슈들을 정리해보고, 라이브러리를 활용해 해결한 사례를 공유해보려고 합니다.
모달을 구현할 때 접근성을 준수하기 위해 생각보다 많은 부분을 고려해야 할 수 있습니다. 웹 접근성 가이드라인을 제공해주는 WAI의 WAI patterns에서는 다음과 같은 내용이 포함되어 있습니다.
- 모달이 열렸을 때, 첫 번째 포커스 가능한 요소로 포커스가 이동해야 합니다.
- 포커스가 모달 안에서만 이동할 수 있어야 합니다.
- ESC로 모달을 닫을 수 있어야 합니다.
- 모달이 닫혔을 때, 모달을 열었던 요소로 포커스가 이동해야 합니다.
- 모달을 여는 버튼에
aria-haspopup=dialog
속성이 존재해야 합니다. - 모달에
role=dialog
속성이 존재해야 합니다. - 유저에게 중요한 메시지를 전달하는 역할이라면
role=alertdialog
속성을 사용해야 합니다. - 모달에
aria-modal=true
속성이 존재해야 합니다. - 모달에
aria-labelledby
혹은aria-label
를 사용해 타이틀을 표시해주어야 합니다. - (선택)
aria-describedby
를 사용해 모달에 대한 설명을 표시해주어야 합니다.
특히, 포커스를 모달 안으로 가두는 기능 (Focus Scope 혹은 Focus Trap이라고 불립니다)은 직접 구현하려고 하면 생각했던 것보다 더 많은 코드가 필요할 수 있습니다.
이러한 문제를 해결해주기 위해 해당 기능을 제공하는 헤드리스 라이브러리들이 많이 존재하는데 (Base UI, Headless UI, React Aria, Ariakit 등), 저는 개인적으로 Radix primitives의 Dialog
컴포넌트를 자주 사용하고 있습니다. 다른 Radix primitives의 컴포넌트들과 잘 통합되고 asChild
props 기반의 렌더링 위임 기능이 강력해서 장점이 분명한 라이브러리입니다.
라이브러리가 대부분의 접근성 이슈를 해결해주지만, 몇 가지 추가적인 고려사항이 존재합니다. 아래는 제가 라이브러리를 사용해보며 겪었던 접근성 관련 이슈들입니다.
- 모달이 닫혀있을 때에도 모달을 여는 버튼에
aria-controls
속성이 존재하기 때문에 접근성 가이드라인에 맞지 않을 수 있습니다. - 모달에
aria-modal=true
속성을 직접 추가해야 합니다.
드랍다운과 함께 사용하기

모달을 드랍다운과 함께 사용한다고 하면 약간 복잡해집니다. 예를 들어, 드랍다운을 여는 버튼이 존재하고 드랍다운 메뉴를 클릭했을 때 드랍다운이 닫히면서 모달이 열리는 UI를 생각해 볼 수 있습니다. 모달이 닫히면 드랍다운 메뉴 버튼이 아닌 드랍다운을 열었던 버튼으로 포커스가 이동해야 합니다. 키보드 사용자들이 모달을 닫고 탭 키를 눌러 바로 다음 컨텐츠를 탐색할 수 있도록 하기 위함입니다.
다른 라이브러리인 Base ui에서는 비슷한 케이스를 다루고 있습니다. Radix에서는 기본적으로 모달이 닫혔을 때 Trigger로 포커스가 이동하기 때문에 컴포넌트에 로직을 추가하여 이 문제를 해결할 수 있습니다.