[웹 접근성] 모달창/팝업창의 접근성을 향상시키는 커스텀 훅 만들기

@eunjios·2024년 2월 1일
0

모달창 접근성 개선하기

개선 방향
1. 모달창 열려 있다면, 모달 외부 요소에 접근 막기
2. 모달창 열리면 해당 컨텐츠에 focus
3. esc 키 로 모달창 닫기

useTrapFocus 커스텀 훅 만들기

프로젝트에 모달창을 사용한 부분은 '앱 다운로드 팝업창' 과 '도움말 플로팅 버튼' 으로, 두 컴포넌트에서 모두 사용할 수 있는 useTrapFocus 커스텀 훅을 만들었다.

컴포넌트에서 다음을 지정할 수 있게 하였다.

  • 모달 ref (모달창 열릴 때 focus 줄 요소이자, 모달창 열린 상태에서 tab으로 접근 가능한 요소들의 컨테이너)
  • esc 키 누를 때 실행시킬 콜백 함수
  • useEffect 의 deps (모달창의 내용이 달라질 경우 필요)

해당 훅에서 구현한 주요 부분은 다음과 같다.

  1. 모달 내부의 focus가 가능한 요소들을 찾는다.
  2. 마지막 요소가 active 상태일 때 tab 을 누르면 첫 번째 요소로 focus 이동
  3. 첫 번째 요소가 active 상태일 때 shift + tab 을 누르면 마지막 요소로 focus 이동

1. focusable elements 찾기

const modalElement = modalRef.current; // 프로퍼티로 받은 ref
const focusableElements = modalElement?.querySelectorAll<HTMLElement>(
  'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);

2. 첫 번째 요소와 마지막 요소 찾기

const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];

3. Tab 키 눌렀을 때 동작할 함수

const handleTabKeyPress = (event: KeyboardEvent) => {
  if (event.key === 'Tab') {
    if (event.shiftKey && document.activeElement === firstElement) {
      event.preventDefault();
      lastElement.focus();
    } else if (!event.shiftKey && document.activeElement === lastElement) {
      event.preventDefault();
      firstElement.focus();
    }
  }
};

4. Esc 키 눌렀을 때 동작할 함수

const handleEscKeyPress = (event: KeyboardEvent) => {
  if (event.key === 'Escape') {
    escCallbackFn?.(); // 프로퍼티로 받은 콜백 함수 
  }
};

5. 이벤트 리스너 등록/삭제

// const modalElement = modalRef.current;
modalElement?.addEventListener('keydown', handleTabKeyPress);
modalElement?.addEventListener('keydown', handleEscKeyPress);
// const modalElement = modalRef.current;
modalElement?.removeEventListener('keydown', handleTabKeyPress);
modalElement?.removeEventListener('keydown', handleEscKeyPress);

useTrapFocus 커스텀 훅 전체 코드

import { useEffect } from 'react';

const useTrapFocus = (
  modalRef: React.RefObject<HTMLDivElement>,
  escCallbackFn?: () => void,
  deps: React.DependencyList = []
) => {
  useEffect(() => {
    const modalElement = modalRef.current;
    const focusableElements = modalElement?.querySelectorAll<HTMLElement>(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    if (!focusableElements) {
      return;
    }
    const firstElement = focusableElements[0];
    const lastElement = focusableElements[focusableElements.length - 1];

    const handleTabKeyPress = (event: KeyboardEvent) => {
      if (event.key === 'Tab') {
        if (event.shiftKey && document.activeElement === firstElement) {
          event.preventDefault();
          lastElement.focus();
        } else if (!event.shiftKey && document.activeElement === lastElement) {
          event.preventDefault();
          firstElement.focus();
        }
      }
    };

    const handleEscKeyPress = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        escCallbackFn?.();
      }
    };

    modalElement?.focus();
    modalElement?.addEventListener('keydown', handleTabKeyPress);
    modalElement?.addEventListener('keydown', handleEscKeyPress);

    return () => {
      modalElement?.removeEventListener('keydown', handleTabKeyPress);
      modalElement?.removeEventListener('keydown', handleEscKeyPress);
    };
  }, deps);
};

export default useTrapFocus;

컴포넌트에서 사용하기

import 나 closeModal 구현 부분은 생략하고 핵심 부분만 보면 다음과 같다.

  1. 콘텐츠 (컨테이너) 를 ref 로 지정하기
  2. 해당 ref 와 esc 키를 눌렀을 때 실행할 콜백 함수 넘기기
  3. 컨테이너의 tabIndex={-1} 로 설정하기
  4. rolearia-modal 속성 설정하기

여기서 유의할 점은 컨테이너의 tabIndex 를 반드시 -1로 설정해야 한다. 그래야 프로그래밍적으로 포커스를 줄 수 있다.

export default function Popup({closeModal} : Props) {
  const popupRef = useRef<HTMLDivElement>(null);
  useTrapFocus(popupRef, closeModal);
  
  return (
    <div
      ref={popupRef}
	  tabIndex={-1}
	  role="dialog"
	  aria-modal="true"
    >
      <p>다양한 기능을 앱에서 사용해 보세요!</p>
      <a>앱 다운받기</a>
      <button>웹으로 볼게요</button>
    </div>
  );
}

완성

이제 tab을 눌러도 모달 내부에만 포커스가 위치하는 것을 볼 수 있다.


느낀점

프로젝트에서 웹 접근성을 개선하는 과정에서 평소 자주 이용하는 페이지들을 참고해 보았는데 생각보다 접근성이 좋지 않았다. 클릭 가능한 요소에 모두 접근하지 못한 경우도 있었고, 특히 슬라이더와 모달창은 접근성을 고려하지 않은 경우가 많았다. 슬라이더나 모달창은 간단히 속성만을 추가하는 것 뿐만 아니라 별도의 프로그래밍이 필요하기 때문에 그런 것 같기도 하다.

혹은 아예 인지하지 못해서 일 수도 있겠다. 나 역시 시각장애인이 사용할 수 있는 서비스를 개발하면서 처음으로 웹 접근성에 대해 고민하게 되었다. 만약 비장애인을 위한 서비스였다면 이렇게까지 접근성에 대해 깊게 고려해 봤을까 싶으면서 기술 외적으로도 느끼는 바가 많았다. 아무튼 이번 기회를 통해 정말 많이 배우고 성장한 느낌이다.


참고 자료

profile
growth

0개의 댓글