Contents
see List왜 폼 접근성을 먼저 설계해야 하는가
문의 폼, 회원가입 폼, 견적 요청 폼은 겉보기에는 단순하지만 실제 운영에서는 전환율, 고객 응대 속도, 개인정보 품질에 직접 영향을 준다. 사용자가 어떤 값을 넣어야 하는지 추측해야 하거나, 오류가 발생했는데 어디를 고쳐야 하는지 알 수 없거나, 모바일 키보드가 매번 엉뚱하게 열리면 폼 이탈이 늘어난다. 더 큰 문제는 스크린 리더, 키보드 사용자, 자동 완성 기능을 쓰는 사용자에게 폼 구조가 제대로 전달되지 않는 경우다. 이때 필요한 것은 화려한 자바스크립트가 아니라 HTML이 이미 제공하는 의미 구조를 정확히 사용하는 것이다.
실무 폼의 기본 목표는 세 가지다. 첫째, 각 입력란의 목적이 브라우저와 보조기술에 명확해야 한다. 둘째, 검증 규칙은 서버와 클라이언트가 같은 의도를 가져야 한다. 셋째, 오류 메시지는 시각적으로만 보이지 않고 입력 컨트롤과 프로그램적으로 연결되어야 한다. 아래 패턴은 신규 구축뿐 아니라 기존 폼을 점검할 때도 바로 적용할 수 있다.
기본 구조: label, fieldset, legend
텍스트 입력에는 보이는 label을 연결하고, 라디오 버튼이나 체크박스처럼 하나의 질문에 여러 선택지가 있는 영역은 fieldset과 legend로 묶는다. label 텍스트를 placeholder로 대체하면 값 입력 후 목적이 사라지고, 번역이나 자동 채우기에서도 손해를 본다. placeholder는 예시나 보조 힌트로만 사용한다.
- 모든 input, select, textarea에는 고유한 id와 대응되는 label for를 둔다.
- 주소, 연락처, 결제 정보처럼 논리적으로 묶이는 항목은 fieldset으로 그룹화한다.
- legend에는 그룹 질문을 짧고 구체적으로 적는다. 예: 연락 받을 방법, 사업자 유형, 희망 상담 시간.
- 도움말과 오류 메시지는 aria-describedby로 입력란에 연결한다.
자동 완성과 모바일 입력 최적화
autocomplete는 단순히 사용자의 입력 시간을 줄이는 기능이 아니다. 브라우저와 비밀번호 관리자, 모바일 OS가 필드의 의미를 파악하도록 돕는 표준 힌트다. 이름은 given-name, family-name으로 나누고, 이메일은 email, 전화번호는 tel, 우편번호는 postal-code처럼 목적에 맞는 토큰을 쓴다. 청구지와 배송지처럼 같은 종류의 필드가 두 번 나오면 billing, shipping 토큰으로 구분한다. 여러 명의 담당자 정보를 받는 화면이라면 section-main, section-manager처럼 section-* 토큰을 붙여 자동 완성 충돌을 줄인다.
inputmode는 값의 형식을 바꾸지 않고 모바일 키보드만 힌트로 제공한다. 사업자등록번호, 인증번호, 우편번호처럼 숫자로 입력하지만 앞자리 0이 의미 있는 값은 type=number보다 type=text와 inputmode=numeric 조합이 안전하다. type=number는 스핀 버튼, 지수 표기, 지역별 소수점 처리 등 의도하지 않은 동작이 생길 수 있기 때문이다.
검증은 HTML 속성으로 먼저 선언한다
required, minlength, maxlength, pattern, type=email, type=url 같은 기본 제약은 HTML에 먼저 선언한다. 이렇게 하면 브라우저의 Constraint Validation API가 같은 규칙을 인식하고, CSS의 :invalid, :valid 상태도 활용할 수 있다. 다만 pattern은 과도하게 복잡하게 만들지 않는 것이 좋다. 예를 들어 이메일 정규식을 직접 완벽하게 구현하려고 하기보다 type=email을 쓰고, 회사 정책상 필요한 제한만 추가한다.
오류 메시지는 alert 창으로 띄우는 방식보다 입력란 근처에 고정된 영역을 두는 편이 낫다. 사용자가 수정 중에도 문제를 다시 볼 수 있고, 스크린 리더가 어떤 입력란의 설명인지 추적하기 쉽다. 자바스크립트는 HTML 검증을 대체하는 도구가 아니라, 메시지를 더 명확하게 갱신하고 서버 오류를 같은 UI에 연결하는 보조 수단으로 사용한다.
실전 예제: 상담 문의 폼
<form class="contact-form" method="post" action="/api/contact" novalidate>
<div class="field">
<label for="company">회사명</label>
<input id="company" name="company" autocomplete="organization" required minlength="2" aria-describedby="company-help company-error">
<p id="company-help">사업자명 또는 서비스명을 입력하세요.</p>
<p id="company-error" class="error"></p>
</div>
<div class="field">
<label for="email">담당자 이메일</label>
<input id="email" name="email" type="email" autocomplete="email" required aria-describedby="email-error">
<p id="email-error" class="error"></p>
</div>
<div class="field">
<label for="phone">연락처</label>
<input id="phone" name="phone" type="text" inputmode="tel" autocomplete="tel" pattern="[0-9\- ]{9,15}" aria-describedby="phone-help phone-error">
<p id="phone-help">숫자와 하이픈만 입력할 수 있습니다.</p>
<p id="phone-error" class="error"></p>
</div>
<fieldset>
<legend>희망 상담 방식</legend>
<label><input type="radio" name="contactType" value="phone" required> 전화</label>
<label><input type="radio" name="contactType" value="email"> 이메일</label>
<label><input type="radio" name="contactType" value="meeting"> 미팅</label>
</fieldset>
<button type="submit">문의 보내기</button>
</form>
<script>
const form = document.querySelector('.contact-form');
function messageFor(input) {
if (input.validity.valueMissing) return '필수 항목입니다.';
if (input.validity.typeMismatch) return '형식에 맞게 입력하세요.';
if (input.validity.tooShort) return input.minLength + '자 이상 입력하세요.';
if (input.validity.patternMismatch) return '허용된 형식과 다릅니다.';
return '';
}
form.addEventListener('submit', (event) => {
const invalid = [...form.elements].filter((el) => el.willValidate && !el.checkValidity());
form.querySelectorAll('.error').forEach((node) => node.textContent = '');
if (invalid.length === 0) return;
event.preventDefault();
for (const input of invalid) {
const error = document.getElementById(input.id + '-error');
if (error) error.textContent = messageFor(input);
}
invalid[0].focus();
});
</script>
서버 오류까지 같은 방식으로 연결한다
클라이언트 검증은 편의 기능이고, 최종 판단은 서버가 해야 한다. 서버에서 중복 이메일, 금지된 도메인, 파일 크기 초과 같은 오류가 내려오면 같은 오류 영역에 메시지를 넣고 해당 입력란에 aria-invalid=true를 부여한다. 이렇게 하면 사용자는 화면 상단의 일반 오류 문구만 보고 헤매지 않아도 된다. 서버 응답 형식도 필드명과 메시지를 분리해 두면 프론트엔드에서 안정적으로 매핑할 수 있다.
- 서버는 { field: "email", message: "이미 등록된 이메일입니다." }처럼 필드 기준 오류를 반환한다.
- 프론트엔드는 해당 필드의 오류 요소에 메시지를 넣고 focus를 이동한다.
- 성공 후에는 오류 텍스트와 aria-invalid 상태를 초기화한다.
- 보안상 중요한 검증은 클라이언트 검증 통과 여부와 관계없이 서버에서 다시 수행한다.
운영 점검 체크리스트
- 키보드만으로 첫 입력부터 제출까지 이동할 수 있는지 확인한다.
- label을 클릭했을 때 정확한 입력란에 포커스가 가는지 확인한다.
- 필수값 누락, 이메일 형식 오류, 짧은 글자 수, 서버 오류가 각각 입력란 옆에 표시되는지 확인한다.
- 모바일에서 이메일, 전화번호, 숫자 입력 키보드가 기대한 형태로 열리는지 확인한다.
- 브라우저 자동 완성으로 채운 값도 제출과 검증에 정상 반영되는지 확인한다.
- placeholder가 사라져도 사용자가 입력 목적을 알 수 있는지 확인한다.
폼 접근성은 별도 프로젝트로 크게 고치는 일이 아니라, 화면을 만들 때마다 HTML 의미 구조를 정확히 쌓는 습관에 가깝다. label, fieldset, autocomplete, inputmode, Constraint Validation API를 일관되게 적용하면 입력 오류가 줄고, 모바일 사용성이 좋아지며, 보조기술 사용자도 같은 흐름으로 서비스를 이용할 수 있다.