[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>></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()}>></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">></button>
</div>
<ul
id="downshift-3-menu"
role="listbox"
aria-labelledby="downshift-3-label"
></ul>
전체 코드
마치면서
지금까지 Downshift 라이브러리를 이용하여 React로 간단한 드롭다운 UI 컴포넌트를 구현해보았습니다. Downshift 라이브러리의 가장 큰 장점은 사용자로 하여금 어떤 HTML 엘리먼트와 CSS 속성을 사용할지에 대해서 어떠한 제약도 가하지 않는다는 것입니다. 따라서 본인이 원하는 어떤 모양의 드롭다운 컴포넌트에도 Downshift 라이브러리를 활용할 수 있습니다.
예를 들어, 본 포스팅에서는 최대한 간단한 예제를 위해서 <input>
엘리먼트를 읽기전용 처리하였지만, 상황에 따라 사용자의 입력을 허용하고 자동 완성을 지원할 수도 있습니다.