"폼은 계약이야. 모든 필드는 질문, 모든 label 은 질문 텍스트, 모든 input 은 사용자의 답."
<form> Element 그 자체
<form> 은 같이 제출되는 input 그룹을 감싸. 중요한 속성:
action="/api/signup"— 폼 데이터가 보내질 곳.method="post"— 상태를 바꾸는 폼은 거의 항상post; 검색 스타일 (읽기) 폼만get.autocomplete="on" | "off"— 브라우저 autofill 제어. 기본 on. 사이트 전체에 off 는 사용자한테 적대적 행동; 진짜 이유 있을 때만 (one-time PIN, OTP) 필드 단위로 off.novalidate— 브라우저 내장 HTML5 검증 비활성. Custom 검증 중이지만 브라우저가 submit 에 폼을 보내게 하고 싶을 때 유용.
<label> + <input>: 원자 페어
이 트랙 전체에서 가장 중요한 패턴이야. 모든 input 에 label 필요. 연결하는 두 방법:
- 명시적 (선호):
<label for="email">Email</label> <input id="email" type="email" />. Label 의for속성이 input 의id와 매치. - 암시적:
<label>Email <input type="email" /></label>. Label 로 input 을 감싸. id 필요 없음.
둘 다 screen reader 가 announce 할 접근성 이름을 input 에 주고, 둘 다 label 텍스트를 클릭 가능하게 만들어 (input 에 focus 가거나 toggle). Label 빼면 폼이 WCAG 떨어지고, Lighthouse 떨어지고, screen reader 가 "편집 텍스트" 라고만 묘사하고 문맥 없는 input 을 출시해.
name 속성
name 속성이 백엔드 (또는 JavaScript) 가 받는 거. <input name="email" /> 는 폼 제출이 email=user@example.com 을 보낸다는 뜻. name 없으면 input 이 폼 제출에 사실상 보이지 않아 — 값은 있는데 아무것도 안 보내짐. 흔한 버그: 폼 만들고, 백엔드가 왜 아무것도 못 받지 고민하다, input 에 name 없는 거 깨달음.
<fieldset> + <legend>: 관련 필드 그룹화
여러 input 이 함께 속할 때 — 청구 주소 (도로, 도시, 우편번호), radio 그룹 ("플랜 골라"), 체크박스 그룹 ("~에 대한 이메일 받기") — <fieldset> 으로 감싸고 그룹 이름인 <legend> 같이. Screen reader 가 필드 읽기 전에 "청구 주소, 그룹, 5 항목" 이라고 announce.
Radio 버튼이 canonical 케이스: 그룹으로만 말이 돼. name 속성이 묶어 (하나만 체크 가능); <fieldset> + <legend> 이 그룹이 뭘 의미하는지 사용자한테 알려.
상태 속성: required, disabled, readonly
required— 이거 없으면 폼 제출 안 됨. 브라우저가 사용자 언어로 내장 검증 메시지 보여 줘.disabled— input 이 focus 불가, 시각적으로 흐릿함, 폼과 함께 제출 안 됨. 현재 흐름에 적용 안 되는 필드에 사용.readonly— input 이 값 표시하지만 사용자가 편집 불가. 폼과 함께 제출 됨. 값이 계산된 확인 필드에 사용.
미묘한 구분: disabled 는 제출에서 제외, readonly 는 포함. 서버가 뭘 봐야 하는지를 기준으로 골라.
<button>: type 속성이 중요해
<button> 에 항상 type 지정. 기본값이 type="submit" 이라, 폼 안의 어떤 <button> 도 클릭하면 폼을 제출해. 'Sign up' 버튼 옆에 'Reset password' 버튼이 있으면, non-submit 인 거에 type="button" 안 놓으면 둘 다 제출돼. 모든 개발자가 적어도 한 번은 잡혀.세 가지 type:
type="submit"— 부모 폼 제출 (기본값).type="reset"— 모든 폼 필드를 초기값으로 클리어. 거의 원하는 적 없음; 실수 클릭 한 번이 사용자 데이터 파괴.type="button"— 기본적으로 아무것도 안 함. JavaScript 돌리는 버튼용 (가시성 토글, modal 열기).
Native 검증: 그냥 요청만 하면 공짜
HTML5 검증이 JavaScript 없이 브라우저에서 돌아가: required, type="email", type="url", pattern="...", min, max, minlength, maxlength. 유효하지 않은 데이터로 폼 제출 → 브라우저가 로컬라이즈된 에러 메시지 보여 주고 첫 유효하지 않은 필드에 focus. CSS 의 :invalid 로 유효하지 않은 필드 스타일링. 졸업했을 때 noValidate + custom UI 로 error 이벤트 캐치. 바퀴가 데려갈 수 없는 자리까지는 바퀴를 다시 발명하지 마.
피파의 노트
required, type="email", minlength, pattern, 그리고 약간의 :invalid CSS. 에러 메시지는 브라우저가 사용자 로케일로 줘 (한국어는 한국어 에러, 영어는 영어 에러). 코드 적게, 더 맞는 행동, 번역 공짜.