iBetter Books
수정

도입 스토리

"회원가입 전환율이 너무 낮아요." 기획자가 말했습니다. "이탈이 왜 이렇게 많죠?"

박멘토가 폼을 열고 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)"을 다룹니다. 현재와 미래의 보조기기에서 작동하는 코드를 만드는 방법을 알아봅니다.