A11y 코드 리뷰 #012 · 오늘 도감에 등록할 태그 —
<select>와<option>, 그리고 ARIA combobox 패턴이번 카드는 디자이너가 가장 싫어하는 태그에 관한 글입니다. native select는 디자인 자유도가 낮지만 — 모바일 UX, a11y, AI 에이전트 호환성 셋 다에서 압도적입니다. 커스텀 dropdown의 진짜 비용을 코드로 짚습니다.
TL;DR
- native
<select>는 데스크탑·모바일 모두에서 잘 작동하는 거의 유일한 dropdown입니다. - 커스텀 dropdown(ARIA combobox)으로 갈아탈 정당한 케이스는 — 검색 가능, 멀티 선택, 옵션 수 1000개 이상으로 가상화 필요 셋 정도입니다.
- ARIA combobox 패턴은 복잡한 구현입니다 — listbox role, aria-activedescendant, 키보드 단축키, 검색 필터, 포커스 관리. 직접 만들면 자주 깨집니다.
- Headless UI Listbox, Radix Select 같은 검증된 라이브러리가 최선의 fallback. 다만 모바일에서 native select만큼 좋지는 않습니다.
- AI 에이전트가 옵션을 선택하는 작업에서 — native select는 한 번에 성공, 커스텀 dropdown은 얼마나 잘 만들었는지에 따라 성공률이 갈립니다.
1. 어디서나 본 그 dropdown
다음 네 패턴은 한국 사이트에서 자주 보는 dropdown 안티패턴입니다.
div + click 토글로 만든 가짜 dropdown
function CustomDropdown({ options, value, onChange }: Props) {
const [open, setOpen] = useState(false);
return (
<div className="dropdown">
<div onClick={() => setOpen(!open)}>{value || "선택"}</div>
{open && (
<div className="options">
{options.map((opt) => (
<div onClick={() => { onChange(opt); setOpen(false); }}>
{opt.label}
</div>
))}
</div>
)}
</div>
);
}
select인데 option에 ARIA 추가
<select>
<option role="option" aria-selected={value === "a"}>A</option>
</select>
드롭다운인데 label 없음
<select>
<option value="">-- 선택 --</option>
<option value="kr">한국</option>
</select>
모바일에서 깨지는 컨트롤된 select
<select value={value} onChange={...}>
{options.map(opt => <option value={opt.id}>{opt.label}</option>)}
</select>
첫 번째 패턴 — 마우스로는 동작하지만 키보드·스크린리더·AI 에이전트 모두 어렵습니다. 두 번째 — native에 ARIA 중복은 No ARIA is better than Bad ARIA. 세 번째 — 라벨 누락. 네 번째 — 모바일에서는 잘 동작하지만 옵션이 많거나 검색 필요하면 한계.
2. 한 가지 질문
이 dropdown이 정말 커스텀이어야 하나요? 디자인 자유도 외에 native select가 못 하는 일이 있나요?
답이 예면 ARIA combobox로 갑니다. 아니오면 native select입니다. 90%의 경우 아니오입니다.
3. select와 combobox — 점진적 해부
3.1 가장 단순한 native select
<label for="country">국가</label>
<select id="country" name="country" autocomplete="country">
<option value="">선택하세요</option>
<option value="kr">대한민국</option>
<option value="us">미국</option>
<option value="jp">일본</option>
</select>
이 8줄이 주는 것:
- 데스크탑: 클릭 시 native dropdown
- iOS: 휠 피커 (사용자에게 친숙)
- Android: full-screen 또는 modal dropdown
- 키보드: 방향키로 옵션 이동, 첫 글자 누르면 그 글자로 시작하는 옵션 점프, Enter 또는 Esc로 닫기
- 스크린리더: "국가, 콤보 상자, 대한민국, 4개 중 1번째"
직접 만들면 키보드 단축키만 30줄 이상이 필요합니다.
3.2 optgroup으로 그룹화
<select id="city" autocomplete="address-level2">
<optgroup label="수도권">
<option value="seoul">서울</option>
<option value="incheon">인천</option>
<option value="gyeonggi">경기</option>
</optgroup>
<optgroup label="강원·충청">
<option value="gangwon">강원</option>
<option value="chungbuk">충북</option>
</optgroup>
</select>
optgroup으로 옵션을 그룹화. 스크린리더가 그룹 이름을 함께 발화합니다 — "수도권, 서울".
3.3 멀티 선택 — multiple 속성
<label for="tags">관심 태그 (여러 개 선택 가능)</label>
<select id="tags" name="tags" multiple size={5}>
<option>SEO</option>
<option>GEO</option>
<option>AEO</option>
<option>A11y</option>
<option>웹 성능</option>
</select>
multiple로 멀티 선택. 다만 — 데스크탑은 Ctrl/Cmd + 클릭, 모바일은 일반 탭. 사용자에게 친숙하지 않은 패턴입니다. 멀티 선택은 보통 체크박스 그룹 또는 커스텀 multi-combobox로 가는 게 일반적.
3.4 ARIA combobox 패턴 — 진짜 비용
ARIA combobox(검색 가능 dropdown)는 다음 구조가 표준입니다.
<label id="city-label">도시</label>
<div role="combobox"
aria-controls="city-listbox"
aria-expanded="true"
aria-haspopup="listbox"
aria-labelledby="city-label">
<input type="text" aria-autocomplete="list" />
</div>
<ul id="city-listbox" role="listbox">
<li role="option" aria-selected="false" id="opt-seoul">서울</li>
<li role="option" aria-selected="true" id="opt-busan">부산</li>
</ul>
이 구조에 더해서:
aria-activedescendant로 키보드 포커스를 가상으로 관리 (실제 포커스는 input에)- 방향키로 listbox 옵션 사이 이동
- Enter로 선택
- Escape로 닫기
- Home/End로 처음·끝 옵션
- 첫 글자 입력으로 검색 필터
- 옵션 클릭 시 input에 값 반영
전체 구현이 최소 200줄입니다. 직접 만들면 한 가지 이상 깨집니다.
3.5 Listbox vs Combobox 차이
<select> → 시맨틱 combobox + listbox 합쳐진 것 (native)
role="listbox" → 옵션 목록만, 검색 입력 없음
role="combobox" → 입력 + 옵션 목록 (검색 가능)
용도 분기:
- 적은 옵션, 검색 불필요 →
<select>또는role="listbox" - 많은 옵션, 검색 필요 → ARIA combobox
- 멀티 선택 + 검색 → multi-combobox (가장 복잡한 패턴)
4. 왜 문제인가 — 3중 영향
4a. 스크린리더 사용자
native select에서:
사용자가 select 포커스
→ NVDA: "국가, 콤보 상자, 대한민국 선택됨, 4개 중 1번째"
→ 사용자가 ↓ 누름
→ NVDA: "미국, 4개 중 2번째"
직관적이고 효율적. 사용자가 전체 옵션 수와 현재 위치를 항상 인지합니다.
커스텀 dropdown에서 — 어떻게 만들었는지에 따라 다릅니다. 잘 만들었으면 native와 비슷, 못 만들었으면 "버튼"만 발화되고 옵션 목록을 통째로 지나갑니다.
4b. 키보드·모바일 사용자
native select의 모바일 UX:
- iOS: 화면 하단에 휠 피커 (사용자에게 익숙한 iOS native UI)
- Android: 화면 중앙에 modal dropdown (Material Design)
커스텀 dropdown은 — 보통 화면 어딘가에 떠 있는 작은 박스. 모바일 화면을 가립니다. 닫기 버튼이 명확하지 않습니다. iOS 휠 피커의 친숙함이 사라집니다.
WebAIM 연구 결과 — 모바일 사용자의 옵션 선택 성공률이 native select는 96%, 평균 커스텀 dropdown은 78%. 18% 차이가 폼 완료율에 직접 영향.
4c. AI 에이전트
native select에서 에이전트의 옵션 선택:
1. select element 식별 (accessibility tree에 combobox role)
2. option들의 텍스트 매칭
3. select.value = "kr" 또는 클릭 + 클릭 시뮬레이션
직접적이고 안정적.
커스텀 dropdown에서:
1. dropdown 트리거 식별 (시맨틱 약함 — div onClick일 수 있음)
2. 클릭으로 열기
3. 옵션 목록이 portal로 다른 DOM 위치에 렌더될 수 있음 — 식별 어려움
4. 옵션 클릭
5. 검색 필요 시 input에 typing 시뮬레이션
각 단계가 추가 추측과 시도. 실패 가능성이 단계마다 누적됩니다.
선행 글 구글이 'AI 검색 최적화 가이드'를 냈는데, 정작 핵심은 접근성 트리였습니다에서 본 사실 — accessibility tree의 명시적 role이 에이전트 성공률의 기반.
5. SEO/GEO/AEO 영향
5.1 SEO — 폼 완료율 신호
dropdown의 모바일 친화성이 폼 완료율에 직접 영향. native select가 커스텀보다 완료율이 평균 18% 높다는 결과는 — 모바일 SEO 신호로 환산하면 적지 않은 차이.
5.2 GEO — 에이전트의 옵션 선택 성공률
GEO에서 AI가 우리 사이트에서 select를 처리할 수 있는가는 중요한 지표. 회원가입 폼의 국가·지역·관심 분야 선택, 결제 폼의 카드 종류 선택 — 모두 select 패턴입니다. native면 성공률이 높고, 커스텀이면 얼마나 잘 만들었는지에 종속됩니다.
5.3 AEO — 옵션 정보의 인덱싱
옵션 목록 안의 텍스트는 DOM의 정적 콘텐츠입니다. native select의 option은 검색엔진이 일반 콘텐츠로 인덱싱합니다. 커스텀 dropdown에서 옵션이 클릭 시점에 동적으로 렌더되면 — 인덱싱이 안 될 수 있습니다.
"이 사이트는 어떤 국가를 지원하나?" 같은 질문에 AI가 답할 때 — native select의 option 목록이 인용 후보. 동적 옵션은 안 보입니다.
6. React에서
6.1 native select with React
function CountrySelect({ value, onChange }: Props) {
return (
<>
<label htmlFor="country">국가</label>
<select
id="country"
name="country"
autoComplete="country"
value={value}
onChange={(e) => onChange(e.target.value)}
>
<option value="">선택하세요</option>
<option value="kr">대한민국</option>
<option value="us">미국</option>
<option value="jp">일본</option>
</select>
</>
);
}
10줄. 모든 a11y 자동.
6.2 Radix Select — native와 비슷한 a11y
import * as Select from "@radix-ui/react-select";
<Select.Root value={value} onValueChange={onChange}>
<Select.Trigger>
<Select.Value placeholder="선택하세요" />
<Select.Icon />
</Select.Trigger>
<Select.Portal>
<Select.Content>
<Select.Viewport>
<Select.Item value="kr">
<Select.ItemText>대한민국</Select.ItemText>
</Select.Item>
<Select.Item value="us">
<Select.ItemText>미국</Select.ItemText>
</Select.Item>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
Radix Select는 데스크탑에서 native와 유사한 a11y를 줍니다. 다만 — 모바일에서는 native만큼 좋지 않습니다. iOS의 휠 피커가 안 뜨고, Android의 modal dropdown 효과가 없습니다.
언제 Radix Select가 정당한가:
- 옵션이 검색 가능해야 할 때
- 옵션에 아이콘이나 풍부한 콘텐츠가 필요할 때
- 디자인이 brand guideline상 native와 달라야 할 때
위 셋이 아니면 — native select가 더 좋습니다.
6.3 Headless UI Listbox/Combobox
import { Combobox } from "@headlessui/react";
const cities = ["서울", "부산", "대구", "인천", "광주", "대전", "울산"];
function CitySearch() {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState("");
const filtered = query === ""
? cities
: cities.filter((c) => c.includes(query));
return (
<Combobox value={selected} onChange={setSelected}>
<Combobox.Input onChange={(e) => setQuery(e.target.value)} />
<Combobox.Options>
{filtered.map((city) => (
<Combobox.Option key={city} value={city}>
{city}
</Combobox.Option>
))}
</Combobox.Options>
</Combobox>
);
}
이게 검색 가능 dropdown의 표준 패턴. Headless UI Combobox가 ARIA combobox의 모든 키보드/포커스 관리를 처리. 직접 만들지 마세요.
6.4 react-aria의 useSelect
Adobe의 react-aria 라이브러리는 headless hook 형태로 select·combobox를 제공합니다. 가장 견고한 a11y이지만 학습 곡선이 큽니다. 대규모 디자인 시스템에 적합.
7. 한국 맥락 — 가볍게
KWCAG 관련 항목:
- 2.1.1 키보드 사용 보장: 모든 dropdown이 키보드로 조작 가능해야 합니다.
- 4.1.2 이름, 역할, 값: dropdown의 현재 선택값과 옵션들이 보조 기술에 노출돼야 합니다.
한국 사이트에서 자주 보는 패턴 — 주소 검색을 위한 시·구·동 단계별 select 3개. 각각이 native select면 — 키보드와 모바일 모두에서 잘 동작합니다. 커스텀으로 갈아탄 사이트들이 입력 이탈률이 더 높은 케이스를 자주 봅니다.
8. "디자이너가 native select를 싫어해요"
자주 듣는 반론들:
"브라우저별로 디자인이 달라요" — 그게 native의 의미입니다. 사용자는 자신의 브라우저 native UI에 익숙합니다. 강제로 통일시키면 사용자가 낯설어합니다.
"화살표 아이콘 디자인 못 바꿔요" — appearance: none으로 native 화살표 제거하고 CSS 배경 이미지로 교체 가능. 디자인 자유도 80%까지는 가능합니다.
"옵션 목록 자체의 디자인이 필요해요" — 옵션 안에 아이콘·서브텍스트·이미지가 필요하면 — 그땐 정당하게 ARIA combobox로 갑니다. Radix Select 또는 Headless UI.
"검색이 필요해요" — 검색은 native가 못 합니다. Headless UI Combobox가 표준.
"native가 너무 구식 같아요" — 사용자가 익숙한 패턴이 가장 좋은 UX입니다. 구식이 아니라 친숙. 모바일 사용자의 96% 옵션 선택 성공률이 그 증거.
9. 체크리스트
- dropdown이 진짜 커스텀이어야 하는 이유가 있는가 (검색·멀티·풍부한 옵션 콘텐츠 등)
- 단순 옵션 선택은 native
<select>로 만들었는가 - select에
<label>이 연결돼 있는가 - select에 적절한
autocomplete표준값이 있는가 (country, address-level1 등) - 옵션이 많을 때
<optgroup>으로 그룹화돼 있는가 - 커스텀 dropdown을 쓴다면 Headless UI/Radix/react-aria 같은 검증된 라이브러리인가
- 모바일 디바이스에서 실제로 옵션 선택이 매끄러운가 (실제 디바이스 검증)
- AI 에이전트가 옵션 선택 시뮬레이션에 성공하는가
마무리
이 글이 16편 시리즈의 열두 번째 도감 카드입니다. 다음 카드는 ARIA tab 패턴 — "탭처럼 보이는 div"가 어떻게 키보드와 AI를 무력화하는지 봅니다.
select의 단 한 줄 규칙 — dropdown은 디자인 자유도가 아닌 데이터의 의미가 정합니다. 단순 선택은 native select, 검색·멀티는 검증된 라이브러리, 직접 만들기는 거의 항상 잘못된 선택입니다.
폼 컴포넌트·디자인 시스템 a11y 컨설팅이 필요하시면 문의하기로 연락 주세요.
codex:
primary_tag: select
secondary_tags: [option, optgroup, combobox, listbox]
aria_roles: [combobox, listbox, option]
aria_attributes: [aria-controls, aria-expanded, aria-haspopup, aria-activedescendant, aria-autocomplete, aria-selected]
related_patterns: [native-select, searchable-dropdown, multi-select, country-picker, address-input]
kwcag: ["2.1.1", "4.1.2"]
wcag: ["2.1.1", "4.1.2", "1.3.5"]
ai_agent_impact: high
seo_geo_impact: medium
difficulty: intermediate
test_envs:
- "Mobile Safari iOS 17"
- "Mobile Chrome Android 14"
- "NVDA 2024.x + Chrome 125"
- "VoiceOver macOS 14 + Safari 17"
references:
- "WAI-ARIA Authoring Practices, Combobox Pattern"
- "WAI-ARIA Authoring Practices, Listbox Pattern"
- "Adrian Roselli, Stop Using Drop-Downs"
- "Headless UI Combobox Documentation"
- "react-aria useSelect / useComboBox"