Logo

[React] Downshift로 드롭다운(dropdown) 구현

웹 접근성(accessibility)을 준수하는 드롭다운(dropdown)를 구현하는 것은 생각보다 쉽지 않은 일입니다.

사실 가장 쉬운 방법은 지난 포스팅에서 소개했던 것처럼 HTML의 <select> 엘리먼트를 사용하는 것인데요. <select> 엘리먼트를 사용하면 내부에 있는 <option> 엘리먼트에 커스텀 스타일을 적용할 방법이 없기 때문에 스타일링에 한계가 있습니다.

그래서 여러 가지 엘리먼트를 이용해서 직접 드롭다운를 만드는 경우가 많은데요. 이 때, 시각적으로는 원하는 모습의 UI를 얻을지 몰라도, 웹 접근성 측면에서는 부족한 부분이 생기는 경우를 많이 보게 됩니다. 예를 들어, 웹 접근성을 준수하는 드롭다운는 키보드로도 조작이 가능해야하며, 스크린리더를 위해 ARIA 속성도 적지적소에 설정이 되어있어야 합니다.

Downshift는 이렇게 까다로운 드롭다운를 구현을 쉽게 할 수 있도록 도와주는 React 라이브러리입니다.

React 컴포넌트 작성

먼저 React로 <label>, <input>, <button>, <ul>, <li> 엘리먼트로 이루어진 드롭다운 UI의 기본 골격을 잡아보겠습니다. 각 HTML 엘리먼트는 레이블, 입력란, 토글 버튼, 선택 목록, 선택 항목을 나타내게 됩니다.

import React from "react";

function Combobox({ label, placeholder, items }) {
  return (
    <>
      <label>{label}</label>
      <div>
        <input readOnly placeholder={placeholder} />
        <button>&gt;</button>
      </div>
      <ul>
        {items.map((item) => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </>
  );
}

export default Combobox;

위와 같이 HTML 마크업을 하면 레이블과 입력란, 토글 버튼 아래에 선택 목록이 정적으로 표시될 것입니다. 웹 접근성 스팩에 따르면 <input> 엘리먼트와 <button> 엘리먼트를 보통 묶어서 combobox의 역할을 갖게 되며, 다수의 <li> 엘리먼트로 이뤄진 <ul> 엘리먼트는 listbox의 역할을 갖게 됩니다.

Downshift 설치

본인의 React 프로젝트에 Downshift를 설치합니다.

$ npm i downshift

Downshift 구조

Downshift 라이브러리는 Render Prop과 Hooks 방식을 모두 지원한는데요. 본 포스팅에서는 최근 트랜드에 맞춰 후자 방식으로 사용해보겠습니다. Downshift의 useCombobox hook 함수에 선택 가능한 값 목록을 넘기면 다양한 상태값과 유틸리티 함수를 반환합니다.

import { useCombobox } from "downshift"

function Combobox({ label, placeholder, items }) {
  const { /* 상태값, 유틸리티 함수 */ } = useCombobox({ items });

  return ( /* 생략 */ )
}

Downshift 적용

이제 위에서 작성한 React 컴포넌트에 Downshift를 적용해보도록 하겠습니다.

isOpen 상태값은 선택 목록을 선택적으로 보이게 하기 위해서 사용하고, highlightedIndex 상태값은 각 선택 항목에 하이라이트 효과를 주기위해서 사용합니다.

그리고 get으로 시작하는 유틸리티 함수들은 React prop을 반환하기 때문에, 스프레드 연산자(...)를 이용해서 각 HTML 컴포넌트에 적용해줍니다.

import React from "react";
import { useCombobox } from "downshift";

function Combobox({ label, placeholder, items }) {
  const {
    isOpen,
    highlightedIndex,
    getLabelProps,
    getComboboxProps,
    getInputProps,
    getToggleButtonProps,
    getMenuProps,
    getItemProps,
  } = useCombobox({
    items,
  });

  return (
    <>
      <label {...getLabelProps()}>{label}</label>
      <div {...getComboboxProps()}>
        <input readOnly placeholder={placeholder} {...getInputProps()} />
        <button {...getToggleButtonProps()}>&gt;</button>
      </div>
      <ul {...getMenuProps()}>
        {isOpen &&
          items.map((item, index) => (
            <li
              {...getItemProps({ item, index })}
              key={item}
              style={{ background: index === highlightedIndex && "lightgray" }}
            >
              {item}
            </li>
          ))}
      </ul>
    </>
  );
}

이제 토글 버튼을 클릭하거나 키보드의 방향키를 위나 아래로 눌러보면 선택 목록이 화면에 표시될 것입니다. 또한, 특정 선택 항목을 클릭하거나 키보드 방향키로 이동 후 엔터 버튼을 누르면 해당 항목이 선택이 될 것입니다.

뿐만 아니라, Downshift는 웹 접근성 스팩에 따라 <div> 엘리먼트의 role 속성을 combobox 설정해주고, <ul> 엘리먼트의 role 속성을 listbox로 설정해줍니다. 그 밖에도 일일이 신경쓰기 어려운 ARIA 속성들도 적지적소에 알아서 설정을 해줍니다.

브라우저에서 소스 코드 보기를 해보면 Downshift는가 자동으로 추가해주는 속성들을 쉽게 확인해볼 수 있습니다.

<label id="downshift-3-label" for="downshift-2-input">예약 시간</label>
<div
  role="combobox"
  aria-haspopup="listbox"
  aria-owns="downshift-3-menu"
  aria-expanded="false"
>
  <input
    readonly=""
    placeholder="--:--"
    id="downshift-2-input"
    aria-autocomplete="list"
    aria-controls="downshift-3-menu"
    aria-labelledby="downshift-3-label"
    autocomplete="off"
    value=""
  /><button id="downshift-3-toggle-button" tabindex="-1">&gt;</button>
</div>
<ul
  id="downshift-3-menu"
  role="listbox"
  aria-labelledby="downshift-3-label"
></ul>

전체 코드

마치면서

지금까지 Downshift 라이브러리를 이용하여 React로 간단한 드롭다운 UI 컴포넌트를 구현해보았습니다. Downshift 라이브러리의 가장 큰 장점은 사용자로 하여금 어떤 HTML 엘리먼트와 CSS 속성을 사용할지에 대해서 어떠한 제약도 가하지 않는다는 것입니다. 따라서 본인이 원하는 어떤 모양의 드롭다운 컴포넌트에도 Downshift 라이브러리를 활용할 수 있습니다.

예를 들어, 본 포스팅에서는 최대한 간단한 예제를 위해서 <input> 엘리먼트를 읽기전용 처리하였지만, 상황에 따라 사용자의 입력을 허용하고 자동 완성을 지원할 수도 있습니다.