접근 가능한 입력 양식 만들기
폼은 사용자가 실제로 무언가를 이루려는 지점입니다. 로그인, 결제, 문의, 예약. 여기서 접근성이 무너지면 "불편"이 아니라 "차단"이 됩니다. 다행히 폼 접근성의 90%는 몇 가지 기본기 — 레이블 연결, 오류 안내, 자동완성 — 로 해결됩니다.
1. 모든 입력칸에 레이블
화면낭독기 사용자가 입력칸에 도착하면 "무엇을 입력하는 칸인지"를 들어야 합니다.
그 이름을 주는 것이 <label>입니다. 연결 방법은 두 가지입니다.
<!-- 방법 A: for-id 연결 (권장) -->
<label for="email">이메일 주소</label>
<input type="email" id="email" name="email">
<!-- 방법 B: 감싸기 -->
<label>
이메일 주소
<input type="email" name="email">
</label>
레이블을 화면에 보이기 싫은 검색창 같은 경우에도 레이블은 존재해야 합니다.
시각적으로만 숨기세요(display:none은 화면낭독기도 못 읽으니 금지).
<label for="q" class="visually-hidden">검색어</label>
<input type="search" id="q">
<button>검색</button>
.visually-hidden {
position: absolute; width: 1px; height: 1px;
padding: 0; margin: -1px; overflow: hidden;
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
체크박스·라디오는 레이블을 입력칸 뒤에 두고, 레이블 전체가 클릭 영역이 되게 합니다.
아이콘 버튼만 있는 경우엔 aria-label로 이름을 줍니다.
2. placeholder는 레이블이 아니다
흔한 착각입니다. placeholder만 두고 레이블을 없애면 이런 문제가 생깁니다.
- 입력을 시작하면 사라집니다. 중간에 "이 칸이 뭐였지?"를 확인할 방법이 없습니다.
- placeholder 텍스트는 대비가 낮아 저시력 사용자가 못 읽는 경우가 많습니다.
- 일부 화면낭독기·음성 입력 도구는 placeholder를 이름으로 인식하지 않습니다.
- 자동완성·비밀번호 관리자가 필드를 잘못 판단할 수 있습니다.
placeholder는 형식 예시("예: hong@example.com") 정도로만 쓰고, 레이블은 항상 따로 둡니다.
"떠 있는 레이블(floating label)"은 입력 시 placeholder 위치에서 위로 올라가 레이블로 남는 방식이라 괜찮습니다 —
실제 <label> 요소를 쓰는지만 확인하세요.
3. 필수 입력 표시
<label for="name">이름 <span aria-hidden="true">*</span></label>
<input id="name" name="name" required aria-required="true">
<p class="form-hint">* 표시는 필수 항목입니다</p>
required속성을 실제로 붙입니다(브라우저 검증 + 보조기술 인식).- 별표(*)만으로 표시하지 말고 어딘가에 "* = 필수"라는 설명을 둡니다(색만/기호만 금지 — 1.4.1, 1.3.3).
- 선택 항목이 더 적다면 반대로 "(선택)"을 붙이는 편이 명확할 때도 있습니다.
4. 오류 메시지 — 보이게 + 들리게 + 연결되게
WCAG 3.3.1은 오류를 글로, 그리고 어느 칸인지 알리라고 요구합니다. 테두리를 빨갛게 하는 것만으로는 부족합니다.
<label for="pw">비밀번호</label>
<input id="pw" type="password"
aria-describedby="pw-hint pw-err"
aria-invalid="true">
<p id="pw-hint" class="form-hint">8자 이상, 숫자와 영문 포함</p>
<p id="pw-err" class="field-error">
오류: 비밀번호가 8자 미만입니다
</p>
aria-describedby로 힌트·오류 문구를 입력칸에 연결하면, 포커스 시 함께 읽힙니다.- 오류가 있는 칸에
aria-invalid="true"를 넣습니다(정상으로 돌아오면 제거하거나false). - 오류 문구는 무엇이 왜 틀렸고 어떻게 고치는지까지. "잘못된 입력"은 도움이 안 됩니다.
- 제출 후 여러 오류가 나오면, 폼 상단에 오류 요약을 두고 각 항목을 해당 칸으로 링크합니다. 포커스를 그 요약으로 옮기면 사용자가 바로 인지합니다.
동적으로(제출 없이) 뜨는 오류는 실시간으로 알려야 합니다.
<div id="form-status" role="alert" aria-live="assertive"></div>
<!-- JS로 이 안에 "이메일 형식이 올바르지 않습니다" 삽입 시 즉시 낭독됨 -->
실시간 검증은 사용자가 그 칸을 떠난 뒤(blur) 하는 것이 좋습니다.
타이핑 중간마다 오류를 외치면 정신없습니다.
5. autocomplete — 입력 부담 줄이기 (1.3.5)
표준 autocomplete 토큰을 넣으면 브라우저·비밀번호 관리자가 값을 채워 줍니다.
인지장애 사용자나 매번 타이핑이 힘든 사용자에게 특히 중요합니다.
<input autocomplete="name" ... > <!-- 이름 -->
<input autocomplete="email" ... >
<input autocomplete="tel" ... >
<input autocomplete="street-address"... >
<input autocomplete="postal-code" ... >
<input autocomplete="current-password" ...>
<input autocomplete="one-time-code" ... > <!-- OTP -->
6. 관련 입력끼리 묶기 — fieldset / legend
<fieldset>
<legend>배송 방법</legend>
<label><input type="radio" name="ship" value="normal"> 일반 배송</label>
<label><input type="radio" name="ship" value="fast"> 빠른 배송</label>
</fieldset>
<legend>이 없으면 화면낭독기 사용자는 "일반 배송" 라디오만 듣고
무엇에 대한 선택인지 모릅니다. 주소 한 세트, 카드 정보 한 세트처럼
논리적으로 묶이는 필드 그룹에 씁니다.
7. 그 밖에
- 입력 형식을 너무 엄격하게 강제하지 않기 — 전화번호에서 하이픈·공백은 서버에서 정리하면 됩니다. "숫자만 입력하세요"로 튕겨내면 붙여넣기 사용자가 막힙니다(3.3.2).
- 시간 제한 — 세션 만료로 입력이 날아가면 2.2.1 위반입니다. 연장 수단을 제공하거나 임시 저장합니다.
- 제출 버튼은
<button type="submit">—<div>제출 버튼은 Enter 제출이 안 됩니다. - 자동 제출 금지 — 마지막 칸을 채우면 자동으로 넘어가는 UX는 되돌리기 어렵고 예측 불가능합니다(3.2.2).
- 중요한 동작(결제·삭제)은 확인 단계나 되돌리기를 둡니다(3.3.4).
8. 점검 체크리스트
- 모든 입력칸에 연결된
<label>이 있는가? (개발자도구에서 칸 클릭 → Accessibility 패널의 "Name" 확인) - placeholder에만 의존한 칸이 없는가?
- 일부러 틀리게 제출해 본다 — 오류가 글로 뜨고, 어느 칸인지 알 수 있고, 화면낭독기가 읽는가?
- 이름·이메일·주소 칸에
autocomplete가 있는가? - 라디오·체크박스 그룹이
fieldset/legend로 묶여 있는가? - 키보드만으로 처음부터 제출까지 완주되는가?
이 도구의 자동 검사는 1·2·5번(레이블 없는 입력, placeholder 의존, 묶이지 않은 그룹)을 잡아 줍니다. 3번 오류 흐름은 실제로 틀리게 제출해 보며 확인하세요.