왜 콤보박스 자동완성에 ARIA 1.2가 필요한가
현대 폼에서는 검색형 드롭다운이 흔하지만, 보조기기 사용자에게는 입력창과 목록의 관계가 깨져 의도한 선택 흐름이 전달되지 않기 쉽습니다. 키보드로 이동은 되는데 스크린 리더는 어디에 초점이 있는지, 무엇을 선택하는지 모호하게 읽는 경우가 많습니다.
이 글은 ARIA 1.2 콤보박스의 핵심 속성인 aria-autocomplete, listbox, activedescendant를 기준으로, 최소한의 구현 체크리스트를 제공합니다. 초보자도 “입력창 → 제안 목록 → 현재 강조 항목” 흐름을 정확히 연결하는 방법을 바로 적용할 수 있습니다.
실무에서는 프레임워크나 디자인 시스템이 있어도 콤보박스의 포커스, 역할, 상태를 명시하지 않으면 접근성 검증을 통과하기 어렵습니다. 한두 가지 속성 누락만으로도 자동완성이 아닌 일반 입력창처럼 인식돼 검색 의도 매칭률이 떨어질 수 있습니다.
> 안티패턴: role만 맞추고 aria-activedescendant를 생략하면 키보드 하이라이트와 스크린 리더의 “현재 항목” 읽기가 불일치합니다.
자동완성 핵심 속성 빠르게 이해하기
콤보박스 자동완성은 입력창, 제안 목록, 현재 강조 항목이 함께 움직여야 합니다. 세 ARIA 속성으로 이 흐름을 명확히 해야 스크린 리더와 키보드 사용자가 같은 맥락을 봅니다.
aria-autocomplete는 입력 중 어떤 피드백을 줄지 알립니다. 추천 글자가 입력창에 채워지는지(채움), 목록만 보이는지(목록)처럼 동작을 선언해 예측 가능하게 합니다.
listbox는 제안 목록의 역할을 지정합니다. DOM이 ul/li가 아니어도 role="listbox"와 role="option"을 쓰면 동일한 키보드 이동을 제공합니다.
aria-activedescendant는 “키보드로 가리키는 항목”의 id를 입력창과 연결합니다. 흐름은 입력 변경 → 목록 필터링 → 강조 항목 id를 input의 aria-activedescendant에 설정입니다.
> 안티패턴: 포커스를 목록 항목으로 실제 이동시키면 타이핑이 끊기고 읽기 순서가 어긋납니다. 입력 포커스는 input에 두고, 강조만 activedescendant로 전달하세요.
- 처음에는 aria-autocomplete="list"로 시작해 목록 제안만 구현합니다.
- 목록에는 role="listbox", 항목에는 role="option"과 고유 id를 부여합니다.
- 화살표 이동 시 aria-activedescendant를 현재 항목 id로 갱신합니다.
- 선택 후 aria-selected로 선택 상태를 남깁니다.
바로 쓰는 접근성 콤보박스 체크
입력창 → 제안 목록 → 강조 항목 순서만 지키면 됩니다. ARIA 역할과 키보드 동작을 같은 흐름으로 점검하면 혼동이 줄어듭니다.
- input: role="combobox", aria-expanded, aria-controls, aria-autocomplete
- 목록: role="listbox", 고유 id, 항목은 role="option"
- 키 이동: aria-activedescendant에 현재 옵션 id 유지
- Enter/클릭 선택, Esc 닫기, Tab은 기본 포커스 흐름 유지
- 필터링 시 첫 옵션 강조 또는 이전 강조 유지
- 목록이 비면 expanded=false, activedescendant 제거
이벤트는 단순할수록 스크린 리더가 상태를 정확히 읽습니다. 입력 변경 → 목록 필터 → 강조 갱신 → 선택/취소 흐름이면 안정적입니다.
- input: input, keydown(ArrowUp/Down, Enter, Esc, Tab)
- listbox: mousedown 또는 click으로 선택
- focusout: 바깥 클릭 시 닫기/초기화
아래 코드는 최소 구조 예시입니다. 실제 데이터 연결 전, 개발·QA 공통 기준으로 확인하기 좋습니다.
<input id="city" role="combobox"
aria-autocomplete="list"
aria-expanded="false"
aria-controls="city-list"
aria-activedescendant="">
<ul id="city-list" role="listbox" hidden>
<li id="opt-0" role="option">Seoul</li>
<li id="opt-1" role="option">Busan</li>
</ul>
<script>
const input = document.getElementById('city');
const list = document.getElementById('city-list');
let activeId = '';
function openList() {
list.hidden = false;
input.setAttribute('aria-expanded', 'true');
}
function closeList() {
list.hidden = true;
input.setAttribute('aria-expanded', 'false');
input.removeAttribute('aria-activedescendant');
activeId = '';
}
function setActive(el) {
activeId = el.id;
input.setAttribute('aria-activedescendant', activeId);
}
input.addEventListener('input', () => {
const first = list.querySelector('[role="option"]');
if (first) { openList(); setActive(first); } else { closeList(); }
});
input.addEventListener('keydown', (e) => {
const opts = [...list.querySelectorAll('[role="option"]')];
if (!opts.length) return;
const idx = Math.max(0, opts.findIndex(o => o.id === activeId));
if (e.key === 'ArrowDown') { e.preventDefault(); setActive(opts[Math.min(idx+1, opts.length-1)]); }
if (e.key === 'ArrowUp') { e.preventDefault(); setActive(opts[Math.max(idx-1, 0)]); }
if (e.key === 'Enter') { e.preventDefault(); input.value = document.getElementById(activeId)?.textContent || input.value; closeList(); }
if (e.key === 'Escape') { e.preventDefault(); closeList(); }
});
list.addEventListener('mousedown', (e) => {
const opt = e.target.closest('[role="option"]');
if (opt) { input.value = opt.textContent; closeList(); }
});
</script>
> 포커스는 input에 두고, 강조 이동만 aria-activedescendant로 처리하세요. 옵션으로 포커스를 옮기면 타이핑이 끊기고 읽기 순서가 흐트러질 수 있습니다.
자동완성 콤보박스 연결 체크리스트
자동완성 콤보박스의 핵심은 속성보다 요소 간 연결 상태입니다. 입력창·목록·강조 항목의 참조가 끊기면 접근성이 바로 무너집니다.
- role만 있고 aria-controls/aria-expanded가 없으면 목록 존재를 알 수 없습니다.
- 포커스를 option으로 실제 이동시키면 타이핑이 끊기고 읽기 순서가 흔들립니다.
- aria-activedescendant가 가리키는 id가 DOM에서 사라지면 스크린 리더가 현재 항목을 잃습니다.
- 빈 목록인데 expanded=true면 선택지가 있는 것처럼 보입니다.
선택 기준은 예측 가능한 동작입니다. 비슷한 컴포넌트와 비교할 때 아래를 점검하세요.
- 검색 제안형: aria-autocomplete="list" 또는 "both", 단순 선택형: "none".
- 자유 입력+선택(하이브리드): activedescendant로 강조하되 Enter 시 입력값과 동기화.
- 터치 우선: hover 대신 키 이동(Up/Down)과 명시 선택(Enter/클릭) 중심.
> 안티패턴: 화살표 키 입력마다 목록을 재생성해 option id가 바뀌는 구조. id가 바뀌면 activedescendant 내비게이션이 끊기고, 스크린 리더 읽기가 매번 초기화됩니다.
실무 팁은 고정된 상태 전이입니다. 입력 변경 -> 목록 필터 -> 강조 유지/초기화 -> expanded 토글 순서를 지키고, 비동기 검색 중에는 activedescendant를 비워 혼선을 줄이세요.
오늘 적용할 체크포인트 정리
자동완성 콤보박스는 입력창·목록·강조 항목을 일관되게 연결하는지가 핵심입니다. role·aria-controls·aria-activedescendant를 한 흐름으로 유지하면 스크린 리더와 키보드 사용자가 같은 결과를 얻습니다.
바로 적용할 실행 순서는 다음과 같습니다.
- input에 role="combobox", aria-expanded, aria-controls, aria-autocomplete 지정
- listbox에 고유 id 부여, option마다 id와 role="option" 설정
- 키 이동 시 강조 option의 id를 input의 aria-activedescendant로 갱신
- 목록 열리고 닫힐 때 expanded 토글, 비면 activedescendant 제거
- Enter/클릭으로 선택 확정, Esc는 목록 닫기, Tab은 기본 포커스 흐름 유지
검증 단계로 a) 키보드만으로 검색·이동·선택이 가능한지, b) 스크린 리더가 현재 강조 항목을 읽는지, c) 목록이 비었을 때 상태가 즉시 반영되는지를 점검하세요.
> 실무 팁: 포커스를 option으로 실제 이동시키지 말고, 입력창에 두고 aria-activedescendant만 바꾸면 타이핑 끊김과 읽기 순서 혼선을 피할 수 있습니다.
입력 변화에 맞춘 강조 항목 갱신
입력이 바뀌면 목록을 다시 걸러서 첫 항목을 강조하고, 그 항목의 id를 input의 aria-activedescendant에 연결합니다. 순서는 입력 변경 → 필터링 → 강조 이동 → 속성 갱신입니다.
option마다 고유 id를 만듭니다. 강조가 바뀔 때 aria-selected와 aria-activedescendant를 함께 바꿉니다.
- 목록이 비면 aria-expanded=false로 닫고, aria-activedescendant는 빈 값으로 둡니다.
- 키보드 이동은 위/아래 화살표로 인덱스를 순환시키면 됩니다.
- 필터 결과가 있으면 active를 0, 없으면 -1로 둡니다.
아래 코드는 구조를 보여주는 예시입니다. keydown과 input에서 강조 인덱스를 계산해 aria-activedescendant를 설정합니다.
<input id="city" role="combobox" aria-controls="city-list" aria-expanded="false"
aria-autocomplete="list" aria-activedescendant="" />
<ul id="city-list" role="listbox"></ul>
<script>
const input = document.getElementById('city');
const list = document.getElementById('city-list');
let options = [], active = -1;
function render(items) {
list.innerHTML = items.map((v, i) =>
`<li id="opt-${i}" role="option" aria-selected="${i===active}">${v}</li>`
).join('');
input.setAttribute('aria-expanded', items.length ? 'true' : 'false');
const id = items.length && active >= 0 ? `opt-${active}` : '';
input.setAttribute('aria-activedescendant', id);
}
input.addEventListener('input', () => {
const q = input.value.toLowerCase();
options = ALL_CITIES.filter(v => v.toLowerCase().includes(q));
active = options.length ? 0 : -1;
render(options);
});
input.addEventListener('keydown', (e) => {
if (!options.length) return;
if (e.key === 'ArrowDown') { e.preventDefault(); active = (active + 1) % options.length; render(options); }
if (e.key === 'ArrowUp') { e.preventDefault(); active = (active - 1 + options.length) % options.length; render(options); }
if (e.key === 'Escape') { options = []; active = -1; render(options); }
});
</script>
> 팁: option에 실제 포커스를 주지 말고, 입력창 포커스를 유지한 채 activedescendant로만 가리키면 타이핑이 끊기지 않습니다.
'프로그래밍' 카테고리의 다른 글
| HTML autocomplete 토큰 완전정복: 주소·결제 폼 WCAG 2.2 대응 실무 가이드 (0) | 2026.10.23 |
|---|---|
| 실시간 운영 대시보드 카드·스파크라인 디자인: 갱신 주기 설정과 안정성 확보 가이드 (0) | 2026.10.20 |
| 모바일 대시보드 터치 우선 패턴: 카드·스파크라인·탭 존 실무 입문 가이드 (0) | 2026.10.20 |
| 대시보드 온보딩 투어·마이크로카피 설계 가이드: 초보 실무자를 위한 단계별 체크리스트 (0) | 2026.10.20 |
| 초보자를 위한 데이터 신뢰도 대시보드 UX: 신선도·라인리지·품질 지표 설계 핵심 가이드 (0) | 2026.10.20 |