드롭다운의 정체를 밝혀라 — Combobox, Multiselect, Listbox 완전 해부

10 min read
UI컴포넌트접근성ARIAComboboxListbox
드롭다운의 정체를 밝혀라 — Combobox, Multiselect, Listbox 완전 해부

드롭다운이 필요하다. 검색도 되어야 하고, 여러 개를 동시에 선택할 수도 있어야 한다. "Combobox? Multiselect? 아니면 그냥 Listbox?" — 세 단어가 머릿속에서 뒤엉킨다. 셋 다 "목록에서 뭔가를 고르는" 컴포넌트인데, 용도와 접근성 요구사항이 완전히 다르다. 여기서 갈림길을 잘못 타면 키보드 사용자 경험이 통째로 무너진다.


네 가지 컴포넌트, 각자의 영역

Combobox — 검색창과 드롭다운의 결합

텍스트 입력 필드 아래로 목록이 열리고, 타이핑하면 필터링된다. 선택은 하나만 가능하다. 검색엔진의 자동완성 바를 떠올리면 정확하다. 입력 → 필터링 → 선택이라는 흐름이 명쾌하다.

combobox-basic.html
<label for="country-select">국가 선택</label>
<div role="combobox" aria-expanded="false" aria-haspopup="listbox">
  <input
    type="text"
    id="country-select"
    aria-autocomplete="list"
    aria-controls="country-listbox"
  />
</div>
<ul id="country-listbox" role="listbox" aria-label="국가 목록">
  <li role="option" id="kr">대한민국</li>
  <li role="option" id="us">미국</li>
  <li role="option" id="jp">일본</li>
</ul>

[💡 잠깐! 이 용어는?] role="combobox": WAI-ARIA에서 정의한 역할(role)로, 텍스트 입력과 팝업 목록을 결합한 복합 위젯을 나타낸다. 스크린 리더는 이 역할을 통해 "검색 가능한 선택 목록"이라는 맥락을 전달한다.

Multiselect — 태그가 쌓이는 선택창

Combobox와 구조가 비슷하지만 여러 항목을 선택할 수 있다. 선택된 항목은 입력 필드 위에 태그(칩)로 쌓인다. 이메일 작성 시 수신자를 추가하는 필드가 대표적인 예다.

multiselect-basic.html
<label for="tag-input">기술 스택</label>
<div class="multiselect" role="combobox" aria-expanded="false" aria-haspopup="listbox">
  <div class="selected-tags" aria-live="polite">
    <span class="tag" role="option" aria-selected="true">React</span>
    <span class="tag" role="option" aria-selected="true">TypeScript</span>
  </div>
  <input
    type="text"
    id="tag-input"
    aria-autocomplete="list"
    aria-controls="tech-listbox"
  />
</div>
<ul id="tech-listbox" role="listbox" aria-multiselectable="true" aria-label="기술 스택 목록">
  <li role="option" aria-selected="false">Vue</li>
  <li role="option" aria-selected="false">Svelte</li>
  <li role="option" aria-selected="false">Angular</li>
</ul>

Listbox — 전체 선택지가 벽에 걸린 메뉴판

드롭다운 없이 모든 옵션이 처음부터 펼쳐져 있다. 스크롤로 탐색하며, 단일 또는 복수 선택이 가능하다. 서랍 속에 넣어둔 것이 아니라 벽에 걸어놓은 메뉴판이다. 한눈에 모든 선택지를 비교할 수 있어서, 사용자가 전체 옵션을 파악해야 할 때 힘을 발휘한다.

listbox-basic.html
<label id="font-label">폰트 선택</label>
<ul role="listbox" aria-labelledby="font-label" tabindex="0">
  <li role="option" aria-selected="false">Pretendard</li>
  <li role="option" aria-selected="true">Noto Sans KR</li>
  <li role="option" aria-selected="false">IBM Plex Sans KR</li>
  <li role="option" aria-selected="false">Spoqa Han Sans Neo</li>
</ul>

Dual Listbox — 좌우 이동의 미학

두 개의 Listbox가 나란히 놓여 있고, 항목을 왼쪽에서 오른쪽으로(또는 반대로) 이동시키는 패턴이다. "사용 가능"과 "선택됨"을 시각적으로 분리해서 보여준다.

dual-listbox.html
<div class="dual-listbox">
  <div>
    <label id="available-label">사용 가능</label>
    <ul role="listbox" aria-labelledby="available-label" aria-multiselectable="true">
      <li role="option">권한 A</li>
      <li role="option">권한 B</li>
      <li role="option">권한 C</li>
    </ul>
  </div>
  <div class="controls">
    <button aria-label="선택 항목 추가">&rarr;</button>
    <button aria-label="선택 항목 제거">&larr;</button>
  </div>
  <div>
    <label id="selected-label">선택됨</label>
    <ul role="listbox" aria-labelledby="selected-label" aria-multiselectable="true">
      <li role="option">권한 D</li>
    </ul>
  </div>
</div>

[💡 잠깐! 이 용어는?] aria-multiselectable: Listbox에서 복수 선택 가능 여부를 스크린 리더에 알리는 ARIA 속성. true로 설정하면 사용자에게 "여러 항목을 선택할 수 있다"는 맥락이 전달된다.


키보드 내비게이션 — 같은 키, 다른 동작

접근성의 핵심은 마우스 없이 키보드만으로 모든 조작이 가능해야 한다는 것이다. 네 컴포넌트가 같은 키에 대해 서로 다르게 반응한다.

ComboboxMultiselectListboxDual Listbox
/ 목록 탐색목록 탐색목록 탐색목록 탐색
Enter선택 후 닫기선택 (닫지 않음)선택해당 없음
Space텍스트 입력텍스트 입력선택 토글선택 토글
Escape팝업 닫기팝업 닫기해당 없음해당 없음
Shift + ↓해당 없음범위 선택범위 선택범위 선택
Tab다음 요소로 이동다음 요소로 이동다음 요소로 이동반대쪽 Listbox로 이동
문자 입력필터링필터링해당 문자로 이동해당 문자로 이동

핵심 차이: Combobox와 Multiselect에서 Space는 텍스트 입력에 쓰이지만, Listbox에서는 선택 토글에 쓰인다. 이 하나를 놓치면 키보드 사용자 경험이 완전히 깨진다.


선택 의사결정 가이드

기준은 두 가지다. 옵션의 수선택 방식.

조건추천 컴포넌트
옵션 5개 미만라디오 버튼 또는 체크박스
옵션 5~20개, 단일 선택Combobox 또는 <select>
옵션 5~20개, 복수 선택Listbox (aria-multiselectable)
옵션 20개 이상, 단일 선택Combobox (검색 필터 필수)
옵션 20개 이상, 복수 선택Multiselect (검색 + 태그)
선택/미선택 상태를 명확히 분리Dual Listbox
옵션 순서 재배열 필요Dual Listbox (드래그앤드롭 대체)

옵션이 5개도 안 되는데 드롭다운을 쓰는 건, 3층짜리 건물에 엘리베이터를 설치하는 것과 같다. 계단이 더 빠르다. 라디오 버튼이나 체크박스가 클릭 한 번으로 선택을 끝내므로 훨씬 효율적이다.

[💡 잠깐! 이 용어는?] Dual Listbox: 두 개의 Listbox를 나란히 배치해 항목을 이동시키는 UI 패턴. 드래그앤드롭보다 접근성이 뛰어나고, 키보드만으로 조작 가능하며, 현재 선택 상태를 시각적으로 명확히 구분할 수 있다.


자주 저지르는 실수 세 가지

1. <select multiple> 남용

네이티브 <select multiple>은 대부분의 사용자가 조작법을 모른다. Ctrl/Cmd를 누른 채 클릭해야 복수 선택이 된다는 사실을 직관적으로 알기 어렵다. Listbox나 Multiselect로 대체하는 것이 맞다.

2. 드래그앤드롭으로 순서 변경

드래그앤드롭은 시각적으로 직관적이지만, 키보드 사용자와 스크린 리더 사용자에게 접근성 문제를 일으킨다. 순서 변경이 필요하다면 Dual Listbox에 "위로/아래로" 버튼을 조합하는 것이 더 접근 가능하다.

3. 포커스 관리 누락

Combobox에서 항목을 선택하면 포커스가 입력 필드로 돌아와야 한다. Multiselect에서 태그를 삭제하면 이전 태그 또는 입력 필드로 포커스가 이동해야 한다. aria-activedescendant로 시각적 포커스와 실제 DOM 포커스를 분리하는 것이 핵심이다.

focus-management.css
[role="listbox"]:focus [aria-selected="true"] {
  outline: 2px solid #4a90d9;
  outline-offset: -2px;
}
 
[role="option"]:hover {
  background-color: #f0f0f0;
}
 
[role="option"][aria-selected="true"] {
  background-color: #e0e7ff;
  font-weight: 600;
}

마무리

Combobox, Multiselect, Listbox는 모두 "목록에서 고르기"라는 같은 문제를 풀지만, 옵션 수, 선택 방식, 표시 형태가 전부 다르다. 원칙은 단순하다 — 옵션이 적으면 단순한 UI를, 많으면 검색 가능한 UI를 쓴다. 어떤 컴포넌트를 선택하든 키보드 내비게이션과 ARIA 속성은 옵션이 아니라 필수다. 라이브러리를 가져다 쓰더라도 키보드로 한 번쯤 직접 조작해보는 습관이 접근성 문제를 예방하는 가장 확실한 방법이다.


참고:

관심 있을 만한 포스트

OHP 필름을 겹치듯 — conic-gradient와 attr()로 순수 CSS 파이 차트 만들기

conic-gradient, CSS 커스텀 프로퍼티, 새로운 attr() 타입 구문을 활용해 JavaScript 없이 시맨틱한 파이 차트를 구현하는 방법을 다룬다.

CSSconic-gradient

배경색이 바뀌면 글자색도 따라가야 한다 — contrast-color() 없이 살아남는 법

브라우저 지원이 부족한 CSS contrast-color()를 color-mix(), relative color syntax, custom properties로 근사 구현하는 방법을 정리한다.

CSScontrast-color

AI 코딩의 맹점 — Artifacts 없이 에이전트는 기억을 잃는다

PRD, ADR, TDD가 AI 코딩 워크플로우에서 왜 선택이 아닌 필수인지, 실전 구조와 함께 살펴본다.

AI 코딩Artifacts

Next-Translate 3.0 — Turbopack과 App Router를 위한 i18n 재건

1년간 공백 후 돌아온 Next-Translate 3.0이 Turbopack 지원, 비동기 params, App Router 안정화를 한 번에 처리하는 방법.

Next.jsi18n

V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법

V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.

V8WebAssembly

Vinext — Vite 위에서 Next.js를 1주일 만에 다시 만든 이야기

Cloudflare가 AI와 함께 단 일주일, $1,100의 API 비용으로 Next.js 호환 프레임워크를 Vite 위에 구축한 과정.

VinextNext.js

Tsonic — TypeScript를 네이티브 바이너리로 컴파일하는 실험

TypeScript → C# → NativeAOT 파이프라인으로 네이티브 실행 파일을 만드는 Tsonic. 어떻게 동작하고, 어떤 한계가 있는지 살펴봤다.

TypeScriptNativeAOT

VS Code 팀의 AI 에이전트 병렬화 — 월간 릴리스를 주간으로 만든 워크플로우

VS Code 팀이 월간 릴리스에서 주간 릴리스로 전환한 비결. 에이전트 세션 병렬화, 자동화 파이프라인, 품질 게이트 설계 전반을 공개했다.

VS CodeAI