들어가며#
“회원가입 폼인데, 뭐 어렵겠어?” 라고 생각하셨다면… 아마 접근성은 한 번도 테스트 안 해보셨을 거예요.
폼(Form)은 웹에서 사용자가 직접 데이터를 입력하는 가장 중요한 인터페이스입니다. 로그인, 결제, 검색, 설문… 사실상 웹의 모든 핵심 기능은 폼을 통해 이루어집니다.
그런데 이 폼이 수많은 사람들에게 완전한 장벽이 되고 있습니다.
- 스크린 리더를 쓰는 사람은 입력 필드가 뭘 요구하는지 알 수가 없고
- 키보드만 쓰는 사람은 날짜 선택기 앞에서 멈추고
- 인지 장애가 있는 사람은 에러 메시지를 봐도 뭘 어떻게 고쳐야 할지 모릅니다

사진: Unsplash의 Susan Q Yin
이번 글에서는 WCAG 2.2 기준으로 폼 접근성을 처음부터 끝까지 살펴봅니다. 이론만 늘어놓는 게 아니라, 제가 직접 만든 데모 페이지와 함께 실전에서 바로 쓸 수 있는 코드를 중심으로 설명해 드릴게요.
🔗 데모 페이지: https://isaaceryn.github.io/demo_codes/form-accessibility-mastery/
“접근성 없는 폼"과 “접근성 있는 폼"을 나란히 두고, 키보드와 스크린 리더로 직접 비교해볼 수 있습니다.
라벨 연결: 이름표 없는 입력 필드의 비극#
WCAG 2.2 성공 기준 1.3.1 (정보와 관계), 3.3.2 (레이블 또는 지시사항) 이 가장 기본으로 요구하는 건 이겁니다. 모든 입력 필드에 프로그래밍적으로 연결된 라벨을 제공할 것.
placeholder만으로는 안 됩니다#
한 번쯤은 이런 폼 만들어보셨죠? 저도요.
<!-- ❌ 이렇게 하면 안 됩니다 -->
<input type="text" placeholder="이름을 입력하세요">placeholder는 시각적으로 힌트처럼 보이지만 라벨이 아닙니다. 사용자가 입력을 시작하면 사라지고, 스크린 리더는 이걸 라벨로 인식하지 않는 경우도 많습니다. 색상 대비도 대부분 WCAG 기준 미달이에요.
올바른 라벨 연결 방법#
방법 1: <label for> — 가장 기본, 가장 확실
<!-- ✅ for 속성으로 명시적 연결 -->
<label for="user-name">이름</label>
<input type="text" id="user-name" name="name">for와 id가 일치하면 클릭 영역도 넓어집니다. 라벨을 클릭해도 입력 필드에 포커스가 가죠. 작은 체크박스를 정확하게 누르느라 고생해본 적 있으시죠? 이게 얼마나 친절한 UX인지 체감이 되실 거예요.
방법 2: aria-label — 라벨 텍스트를 시각적으로 숨기고 싶을 때
<!-- ✅ 검색창처럼 시각적 라벨이 없는 경우 -->
<input type="search" aria-label="사이트 내 검색">
<button type="submit">
<svg aria-hidden="true"><!-- 돋보기 아이콘 --></svg>
<span class="sr-only">검색</span>
</button>방법 3: aria-labelledby — 이미 페이지에 있는 텍스트를 라벨로 활용
<!-- ✅ 기존 제목 요소를 라벨로 참조 -->
<h2 id="billing-section">결제 정보</h2>
<input type="text" id="card-number" aria-labelledby="billing-section card-number-label">
<span id="card-number-label">카드 번호</span>aria-labelledby는 여러 id를 공백으로 이어 붙일 수 있어서, 복잡한 폼에서 맥락을 조합하는 데 유용합니다.
필수 입력 표시: 별표(*)만으로는 부족합니다#
WCAG 2.2 3.3.2 (레이블 또는 지시사항) 은 필수 필드를 명확하게 표시하도록 요구합니다.
<!-- ❌ 시각적으로만 표시 -->
<label for="email">이메일 <span style="color:red">*</span></label>
<input type="email" id="email">별표가 “필수"를 의미한다는 건 시각적 관례일 뿐입니다. 스크린 리더는 이걸 그냥 “별표"라고 읽거나 아예 건너뛸 수 있어요.
<!-- ✅ 프로그래밍적으로 필수 표시 -->
<label for="email">
이메일
<span aria-hidden="true">*</span>
<span class="sr-only">(필수)</span>
</label>
<input type="email" id="email" required aria-required="true">required는 브라우저 기본 검증을 활성화하고, aria-required="true"는 스크린 리더에 명시적으로 알립니다. aria-hidden="true"를 붙인 별표는 시각적으로만 보이고, 스크린 리더엔 “(필수)“라는 텍스트가 읽힙니다.
아직도 required 기능을 javascript로 구현하는 경우를 종종 볼 수 있는데, 브라우저의 기본 검증을 활용하는게 좋습니다.
폼 상단에 안내 문구를 두는 것도 좋은 방법입니다.
<p id="required-notice">
<span aria-hidden="true">*</span> 표시가 있는 항목은 필수 입력입니다.
</p>입력 형식 안내: aria-describedby의 힘#
입력 필드 아래 힌트 텍스트(“8자 이상 입력해주세요”)는 어떻게 연결해야 할까요?
<!-- ✅ aria-describedby로 설명 텍스트 연결 -->
<label for="password">비밀번호 <span class="sr-only">(필수)</span></label>
<input
type="password"
id="password"
required
aria-describedby="password-hint"
>
<p id="password-hint" class="field-hint">
영문, 숫자, 특수문자를 포함해 8자 이상 입력해주세요.
</p>aria-describedby는 라벨(aria-labelledby)과 달리, 추가적인 설명 을 연결합니다. 스크린 리더는 필드에 포커스가 가면 라벨을 먼저 읽고, 잠시 후 설명 텍스트도 읽어줍니다.
여러 설명이 있다면 공백으로 이어 붙입니다.
<input
type="password"
id="password"
aria-describedby="password-hint password-strength"
>
<p id="password-hint">8자 이상 입력해주세요.</p>
<p id="password-strength">강도: 보통</p>
사진: Unsplash의 Toa Heftiba
에러 처리: “빨간 테두리"만으론 아무것도 모릅니다#
그런데, 에러 메시지를 곁들인..

입력 검증 실패 시 가장 많이 보이는 패턴은 입력 필드 테두리를 빨갛게 바꾸는 겁니다. 시각적으로는 확실하지만, 스크린 리더 사용자는 아무것도 알 수 없습니다.
WCAG 2.2 3.3.1 (오류 식별), 3.3.3 (오류 제안) 이 요구하는 건 세 가지입니다:
- 오류가 발생했음을 알릴 것
- 어느 필드에서 오류가 났는지 알릴 것
- 어떻게 고치면 되는지 제안할 것
aria-invalid + aria-describedby 패턴#
<!-- 에러 발생 후 동적으로 추가되는 상태 -->
<label for="email">이메일 <span class="sr-only">(필수)</span></label>
<input
type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error"
>
<p id="email-error" class="field-error" role="alert">
올바른 이메일 형식으로 입력해주세요. 예: [email protected]
</p>aria-invalid="true"는 스크린 리더에 “이 필드에 오류가 있습니다"라고 알립니다. role="alert"는 DOM에 추가되는 순간 스크린 리더가 즉시 읽어줍니다.
오류가 없을 때는 aria-invalid를 아예 제거하거나 "false"로 설정하세요.
// 검증 통과 시
input.removeAttribute('aria-invalid');
errorEl.textContent = '';
// 검증 실패 시
input.setAttribute('aria-invalid', 'true');
errorEl.textContent = '올바른 이메일 형식으로 입력해주세요.';폼 제출 후 여러 에러 처리#
여러 필드에 에러가 동시에 발생했을 때는 폼 상단에 요약 안내를 제공하고, 해당 필드로 바로 이동할 수 있는 링크를 제공하는 게 좋습니다. (WCAG 2.2 3.3.1 오류 식별 / 3.3.3 오류 제안)
<div role="alert" aria-labelledby="error-summary-title" class="error-summary">
<h2 id="error-summary-title">3개의 입력 오류가 있습니다</h2>
<ul>
<li><a href="#email">이메일: 올바른 형식으로 입력해주세요</a></li>
<li><a href="#password">비밀번호: 8자 이상 입력해주세요</a></li>
<li><a href="#phone">전화번호: 숫자만 입력해주세요</a></li>
</ul>
</div>데모에서 직접 확인하기: form-a11y-demo — 에러 처리 섹션 스크린 리더를 켜고 제출 버튼을 눌러보세요. 에러가 어떻게 안내되는지 직접 들을 수 있습니다.
그룹화: fieldset과 legend#
라디오 버튼이나 체크박스가 여러 개 있을 때, 각각의 라벨만으로는 맥락이 부족합니다.
<!-- ❌ 그룹 맥락 없음 -->
<input type="radio" id="card" name="payment"> <label for="card">신용카드</label>
<input type="radio" id="transfer" name="payment"> <label for="transfer">계좌이체</label>스크린 리더 사용자는 “신용카드"라는 라벨만 들을 뿐, 이게 “결제 수단 선택” 항목인지 알 방법이 없습니다.
<!-- ✅ fieldset + legend으로 그룹 맥락 제공 -->
<fieldset>
<legend>결제 수단 <span class="sr-only">(필수, 1개 선택)</span></legend>
<div class="radio-group">
<input type="radio" id="card" name="payment" value="card">
<label for="card">신용카드</label>
</div>
<div class="radio-group">
<input type="radio" id="transfer" name="payment" value="transfer">
<label for="transfer">계좌이체</label>
</div>
<div class="radio-group">
<input type="radio" id="phone-pay" name="payment" value="phone">
<label for="phone-pay">휴대폰 결제</label>
</div>
</fieldset>스크린 리더는 라디오 버튼에 포커스가 가면 “결제 수단, 신용카드, 라디오 버튼, 1/3"처럼 그룹 컨텍스트를 함께 읽어줍니다.
<fieldset>은 긴 폼의 섹션을 구분할 때도 활용할 수 있습니다.
키보드 내비게이션: Tab 순서와 포커스 관리#
WCAG 2.2 2.1.1 (키보드), 2.4.3 (포커스 순서) 는 모든 폼 컨트롤이 키보드로 조작 가능해야 하고, 포커스 순서가 논리적이어야 한다고 요구합니다.
tabindex는 신중하게#
<!-- ❌ 탭 순서를 강제로 바꾸면 혼란스럽습니다 -->
<input tabindex="3" ...>
<input tabindex="1" ...>
<input tabindex="2" ...>tabindex로 순서를 억지로 바꾸기보다 HTML 구조 자체가 논리적인 순서를 갖도록 설계하는 게 맞습니다.
tabindex="0"은 원래 포커스를 받지 못하는 요소(예: <div>)를 탭 순서에 포함시킬 때만 씁니다.
<!-- ✅ 커스텀 컴포넌트에 키보드 접근성 부여 -->
<div
role="combobox"
tabindex="0"
aria-expanded="false"
aria-haspopup="listbox"
>
국가 선택
</div>제출 버튼과 키보드 동작#
폼 제출은 Enter 키로 동작해야 합니다. <button type="submit">을 쓰면 브라우저가 자동으로 처리해줍니다.
<!-- ✅ 표준 버튼 사용 -->
<button type="submit">회원가입</button>
<button type="button" onclick="cancelForm()">취소</button><div onclick="...">으로 만든 가짜 버튼은 키보드로 활성화할 수 없고, Enter 키도 작동하지 않습니다. 접근성 측면에서 최악의 선택이에요.
실전 데모: 직접 확인해보세요#
말보다 직접 경험하는 게 최고입니다.
데모 페이지는 두 개의 같은 회원가입 폼을 나란히 보여줍니다.
| 접근성 없는 폼 | 접근성 있는 폼 | |
|---|---|---|
| 라벨 | placeholder만 사용 | <label> 명시적 연결 |
| 필수 표시 | 시각적 별표만 | aria-required + 숨김 텍스트 |
| 에러 메시지 | 빨간 테두리만 | aria-invalid + role="alert" |
| 그룹화 | <div> 묶음 | <fieldset> + <legend> |
| 키보드 | 일부 컨트롤 불가 | 전체 키보드 조작 가능 |
테스트 방법:
Tab키로 폼을 이동하며 포커스가 어디로 가는지 확인- macOS VoiceOver(
Cmd + F5) 또는 NVDA를 켜고 각 필드에서 무엇이 읽히는지 비교 - 일부러 오류를 내고 제출 — 에러 안내가 어떻게 다른지 확인

직접 만든 데모 페이지 - 두 폼의 차이를 키보드와 스크린 리더로 체험할 수 있습니다
보너스: WCAG 3.0에서는 어떤 점을 주의해야 할까?#
WCAG 3.0은 아직 Draft 단계이지만, 폼 접근성에서 몇 가지 방향 변화가 예고되어 있습니다.
💡 WCAG 3.0의 전반적인 변화가 궁금하다면, 이 블로그에서 연재 중인 WCAG 3.0 시대 준비하기 시리즈 를 함께 읽어보세요.

사진: Unsplash의 Devon Beard
1. 인지 접근성이 더 중요해집니다#
WCAG 3.0은 Cognitive Accessibility를 본격적으로 다룹니다. 폼 설계에서 이 의미는:
- 에러 메시지가 사람이 이해할 수 있는 언어로 작성되어야 한다 (코드나 기술 용어 금지)
- 복잡한 폼은 **여러 단계(multi-step)**로 나눠야 한다
- 입력 도중 자동 저장 또는 재입력 방지를 제공해야 한다
지금도 좋은 UX이지만, 3.0에서는 테스트 가능한 기준으로 명시될 가능성이 높습니다.
2. 성공 기준이 아닌 “Outcome” 기반 평가#
WCAG 2.2는 각 성공 기준을 개별적으로 Pass/Fail로 평가합니다. WCAG 3.0은 사용자 목표(Outcome) 달성을 기준으로 전체적인 경험을 평가하는 방향으로 바뀝니다.
폼에 대입하면: “사용자가 이 폼을 오류 없이 완성할 수 있는가?“가 핵심 Outcome이 됩니다. 기술적으로 aria-invalid가 붙어 있어도, 실제 사용자가 오류를 이해하고 수정하지 못한다면 기준을 충족하지 못할 수 있어요.
3. 지금 준비할 수 있는 것#
| 현재 (WCAG 2.2) | WCAG 3.0 대비 |
|---|---|
aria-invalid + 에러 텍스트 | 에러 메시지를 명확한 일상 언어로 작성 |
| 필수 필드 표시 | 입력 예시(example) 제공 확대 |
| 포커스 관리 | 폼 완료 후 성공 안내까지 포커스 흐름 설계 |
| 자동 검증 | 실시간 인라인 검증 (너무 이른 에러는 피하기) |
WCAG 3.0이 정식 표준이 되려면 아직 시간이 더 필요합니다. 하지만 위 방향으로 지금부터 준비해두면, 표준이 바뀌어도 큰 수정 없이 대응할 수 있을 거예요.
마무리: 폼 접근성 체크리스트#
구현한 폼이 접근성 기준을 갖추고 있는지, 이 리스트로 빠르게 확인해보세요.
라벨 연결 (WCAG 1.3.1 / 3.3.2)
- 모든 입력 필드에
<label for>,aria-label,aria-labelledby중 하나 적용 - placeholder를 라벨 대용으로 사용하지 않음
필수 표시 (WCAG 3.3.2)
- 필수 필드에
required+aria-required="true"적용 - 시각적 필수 표시(
*)가 스크린 리더에 의미 있게 전달됨
에러 처리 (WCAG 3.3.1 / 3.3.3)
- 오류 시
aria-invalid="true"설정 - 에러 메시지가
aria-describedby로 연결됨 - 에러 메시지가 즉시 스크린 리더에 안내됨 (
role="alert"또는 live region) - 에러 메시지에 수정 방법이 구체적으로 포함됨
그룹화 (WCAG 1.3.1)
- 라디오/체크박스 그룹에
<fieldset>+<legend>사용
키보드 접근성 (WCAG 2.1.1 / 2.4.3)
- 모든 폼 컨트롤이 Tab으로 접근 가능
-
tabindex로 억지로 순서를 바꾸지 않음 - Enter 키로 폼 제출 가능
- 커스텀 컨트롤에 적절한 키보드 동작 구현
폼 하나를 제대로 만드는 게 생각보다 품이 많이 들지만, 그만큼 가치가 있는 작업입니다. 여러분이 만든 폼을 통해 누군가가 처음으로 원하는 서비스를 신청할 수 있게 된다면, 그게 바로 접근성의 의미 아닐까요.
데모 페이지에서 직접 테스트해보시고, 궁금한 점은 댓글로 남겨주세요!
