프론트엔드

[프론트엔드] 접근성을 고려한 컴포넌트 설계 (Dropdown)

취업 드가자잇 2025. 6. 24. 03:25

 

최근 만들어본 드롭다운 컴포넌트

 

최근 드롭다운 컴포넌트를 구현하면서 그동안 소홀했던 접근성을 더 깊이 고려해보게 되었습니다.

 

1. 키보드 네비게이션 구현

드롭다운에서 지원해야 할 핵심 키보드 이벤트는 Enter, ArrowUp, ArrowDown, Escape 정도라고 생각합니다.

이 중에서 preventDefault()가 필요한 키들이 있는데, 이를 처리하지 않으면 기본 동작과 드롭다운 이벤트가 함께 발생하여 의도치 않은 부작용을 초래할 수 있습니다.

  • ArrowDown/Up 기본 동작: 페이지 스크롤
  • Enter 기본 동작: 폼 제출 또는 기본 버튼 동작

이런 부작용을 막기 위해 기본 동작을 차단하고, 대신 드롭다운만의 고유한 동작을 구현했습니다.

구현 시에는 focusedIdx라는 상태를 두어 현재 포커스된 옵션을 추적하도록 했습니다:

 

const [isOpen, setIsOpen] = useState(false);
const [focusedIdx, setFocusedIdx] = useState(-1);

const handleKeyDown = (e: React.KeyboardEvent) => {
  if (disabled) return;

  switch (e.key) {
    case 'Enter':
      e.preventDefault();
      if (!isOpen) {
        setIsOpen(true);
        setFocusedIdx(0);
      } else if (focusedIdx >= 0) {
        handleSelect(options[focusedIdx]);
      }
      break;
    case 'ArrowDown':
      e.preventDefault();
      if (isOpen) {
        setFocusedIdx(prev => (prev < options.length - 1 ? prev + 1 : prev));
      }
      break;
    case 'ArrowUp':
      e.preventDefault();
      if (isOpen) {
        setFocusedIdx(prev => (prev > 0 ? prev - 1 : prev));
      }
      break;
    case 'Escape':
      setIsOpen(false);
      setFocusedIdx(-1);
      break;
  }
};

 

2. 외부 클릭으로 드롭다운 닫기

드롭다운처럼 토글 형태로 열리는 UI에서는 다른 영역을 클릭했을 때 자동으로 닫히는 것이 사용성 측면에서 중요하다고 생각했습니다.

그렇지 않으면 사용자가 매번 드롭다운 버튼을 다시 클릭해야 하는데, 그러면 좀 짜칠 것 같다고 생각했습니다.

이를 위해 mousedown 이벤트를 활용했습니다. 다만, 리액트만으로는 특정 요소가 클릭 범위에 포함되는지 판단하기 어려우므로, DOM API의 contains() 메서드를 사용했습니다:

 

const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  const handleClickOutside = (event: MouseEvent) => {
    if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
      setIsOpen(false);
      setFocusedIdx(-1);
    }
  };

  document.addEventListener('mousedown', handleClickOutside);
  return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

 

3. ARIA 속성으로 스크린 리더 지원

 

ARIA 속성은 스크린 리더 사용자에게 컴포넌트의 현재 상태와 역할을 명확히 전달하는 역할을 한다고 합니다.

 

aria-expanded={isOpen}

  • 드롭다운의 확장/축소 상태를 알려줌
  • true일 때 "확장됨", false일 때 "접힘"으로 읽어줌

aria-haspopup="listbox"

  • 이 버튼이 선택 목록을 표시하는 역할임을 명시
  • 스크린 리더가 "목록 상자가 있는 버튼"으로 안내

aria-label={selected ? 선택됨: ${selected} : placeholder}

  • 버튼의 접근 가능한 이름을 제공
  • 선택된 값이 있으면 "선택됨: 주니어(1년~3년)", 없으면 플레이스홀더 텍스트를 읽어줌
      <button
       	type="button"
        onKeyDown={handleKeyBoardEvents}
        disabled={disabled}
        aria-expanded={isOpen}
        aria-haspopup="listbox"
        aria-label={selected ? `선택됨: ${selected}` : placeholder}
        onClick={() => setIsOpen(!isOpen)}
      >