도입 스토리
"회원가입 전환율이 너무 낮아요." 기획자가 말했습니다. "이탈이 왜 이렇게 많죠?"
박멘토가 폼을 열고 Tab 키로 탐색을 시작했습니다. 스크린리더 음성이 들렸습니다.
"편집 텍스트. 편집 텍스트. 편집 텍스트."
레이블이 없었습니다. 어느 입력란이 이름이고, 어느 것이 이메일인지 알 수 없었습니다.
"전환율 문제만이 아니에요." 박멘토가 말했습니다. "스크린리더 사용자는 아예 폼을 완료할 수가 없어요. 그리고 이메일 형식 오류가 났을 때 오류 메시지가 어느 필드 것인지도 연결이 안 되어 있어요."
핵심 개념 설명
레이블과 입력 연결 — WCAG 1.3.1, 4.1.2
모든 입력 요소에는 레이블이 있어야 하고, 프로그래밍적으로 연결되어야 합니다.
<!-- 방법 1: for-id 연결 (권장) --><label for="email">이메일</label><input type="email" id="email" name="email"><!-- 방법 2: 감싸기 방식 --><label> 이메일 <input type="email" name="email"></label><!-- 잘못된 예: 시각적 레이블이 프로그래밍 연결 없음 --><p>이메일</p><input type="email" name="email"><!-- 스크린리더: "편집 텍스트" — 어느 필드인지 모름 -->
placeholder는 레이블을 대체할 수 없습니다.
<!-- 잘못된 예: placeholder만 사용 --><input type="text" placeholder="이름을 입력하세요"><!-- 타이핑 시작하면 힌트가 사라져서 무엇을 입력하는지 모름 --><!-- 올바른 예: label + placeholder 함께 --><label for="name">이름</label><input type="text" id="name" placeholder="홍길동" autocomplete="name">
필수 입력란 표시
<!-- 잘못된 예: 색깔로만 표시 --><label for="email" style="color: red;">이메일</label><!-- 올바른 예: 텍스트 + aria-required --><label for="email"> 이메일 <span aria-label="필수" class="required-mark">*</span></label><input type="email" id="email" required aria-required="true"><!-- 폼 상단에 안내 문구 --><p> <span aria-hidden="true">*</span> 표시된 항목은 필수 입력입니다.</p>
입력 도움말 연결
추가 설명이 필요한 필드는 aria-describedby로 연결합니다.
<label for="password">비밀번호</label><input type="password" id="password" aria-describedby="password-hint" aria-required="true"><p id="password-hint" class="field-hint"> 영문 대소문자, 숫자, 특수문자 포함 8자 이상</p>
스크린리더가 입력란에 포커스할 때 레이블 → 힌트 순서로 읽어줍니다. "비밀번호. 영문 대소문자, 숫자, 특수문자 포함 8자 이상. 편집 텍스트."
오류 메시지 — WCAG 3.3.1, 3.3.3
오류가 발생했을 때 명확한 오류 메시지를 제공해야 합니다.
<!-- 오류 상태 필드 --><label for="email">이메일</label><input type="email" id="email" aria-invalid="true" aria-describedby="email-hint email-error" class="input-error"><p id="email-hint" class="field-hint">예: [email protected]</p><p id="email-error" class="error-message" role="alert"> 올바른 이메일 형식을 입력해 주세요.</p>
aria-invalid="true": 현재 값이 유효하지 않음을 알림aria-describedby: 힌트와 오류 메시지 모두 연결 가능 (공백으로 구분)role="alert": 오류 메시지가 나타나면 스크린리더가 즉시 읽어줌
오류 요약 — WCAG 3.3.3
여러 필드에 오류가 있을 때, 폼 제출 후 오류 목록을 상단에 표시합니다.
<div role="alert" aria-labelledby="error-summary-title" id="error-summary"> <h2 id="error-summary-title"> 입력 오류 2건을 수정해 주세요. </h2> <ul> <li><a href="#email">이메일: 올바른 형식이 아닙니다.</a></li> <li><a href="#phone">전화번호: 필수 입력입니다.</a></li> </ul></div>
오류 항목을 클릭하면 해당 필드로 포커스가 이동합니다. 오류 요약 자체로도 포커스를 이동시킵니다.
function showErrorSummary(errors) { const summary = document.getElementById('error-summary'); // 오류 목록 렌더링... summary.removeAttribute('hidden'); // 오류 요약으로 포커스 이동 summary.setAttribute('tabindex', '-1'); summary.focus();}
자동완성 — WCAG 1.3.5
개인 정보 입력 필드에 autocomplete 속성을 지정하면 브라우저와 보조기기가 자동으로 값을 채워줍니다.
<input type="text" id="name" autocomplete="name"><input type="email" id="email" autocomplete="email"><input type="tel" id="phone" autocomplete="tel"><input type="text" id="address" autocomplete="street-address"><input type="text" id="city" autocomplete="address-level2"><input type="password" id="current-pw" autocomplete="current-password"><input type="password" id="new-pw" autocomplete="new-password">
운동 장애가 있는 사용자는 타이핑 자체가 어렵습니다. 자동완성으로 입력 부담을 크게 줄일 수 있습니다.
단계별 실습
따라하기: 폼 레이블 검사하기
// label 없는 입력 요소 찾기document.querySelectorAll('input, select, textarea').forEach(input => { const id = input.id; const ariaLabel = input.getAttribute('aria-label'); const ariaLabelledBy = input.getAttribute('aria-labelledby'); const hasLabel = id && document.querySelector(`label[for="${id}"]`); const hasAriaLabel = ariaLabel || ariaLabelledBy; if (!hasLabel && !hasAriaLabel) { console.error('레이블 없음:', input); }});
변형하기: 실시간 유효성 검사 개선
const emailInput = document.getElementById('email');const emailError = document.getElementById('email-error');emailInput.addEventListener('blur', () => { const isValid = emailInput.validity.valid; emailInput.setAttribute('aria-invalid', !isValid); if (!isValid) { emailError.textContent = '올바른 이메일 형식을 입력해 주세요. (예: [email protected])'; emailError.removeAttribute('hidden'); } else { emailError.setAttribute('hidden', ''); emailError.textContent = ''; }});
blur 이벤트(포커스를 잃을 때)에 검사합니다. input 이벤트(타이핑 중)에 오류를 표시하면 스크린리더가 타이핑할 때마다 오류를 읽어 방해가 됩니다.
정리와 확인
핵심 내용 요약
- 레이블 연결(WCAG 1.3.1):
<label for="id">— placeholder는 대체 불가 aria-required="true": 필수 필드 명시aria-describedby: 힌트와 오류를 입력란에 연결aria-invalid="true": 오류 상태 표시role="alert": 오류 메시지 즉시 읽어주기autocomplete: 개인 정보 입력 편의 제공
확인 문제
문제 1. placeholder만으로 레이블을 대체하면 안 되는 이유는?
타이핑을 시작하면 placeholder가 사라져서 무엇을 입력하는 필드인지 알 수 없습니다.
또한 스크린리더 지원이 불완전하고, 색상 대비 요건도 충족하기 어렵습니다.
문제 2. aria-describedby에 여러 id를 연결하는 방법은?
<input aria-describedby="hint error"><!-- 공백으로 구분하여 여러 id 연결 가능 --><!-- 스크린리더가 hint → error 순서로 읽어줌 -->
다음 챕터에서는 WCAG의 네 번째 원칙인 "견고성(Robust)"을 다룹니다. 현재와 미래의 보조기기에서 작동하는 코드를 만드는 방법을 알아봅니다.