C.W.K.
Stream
Lesson 01 of 04 · published

폼 해부학: <form>, <fieldset>, <label>, <input>, <button>

~12 min · forms, label, fieldset, anatomy

Level 0Markup Novice
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"폼은 계약이야. 모든 필드는 질문, 모든 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 필요. 연결하는 두 방법:

  1. 명시적 (선호): <label for="email">Email</label> <input id="email" type="email" />. Label 의 for 속성이 input 의 id 와 매치.
  2. 암시적: <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 이벤트 캐치. 바퀴가 데려갈 수 없는 자리까지는 바퀴를 다시 발명하지 마.

피파의 노트

Cwk-site 의 auth 흐름이 native HTML5 검증을 전적으로 써 — custom validator 라이브러리 없이. required, type="email", minlength, pattern, 그리고 약간의 :invalid CSS. 에러 메시지는 브라우저가 사용자 로케일로 줘 (한국어는 한국어 에러, 영어는 영어 에러). 코드 적게, 더 맞는 행동, 번역 공짜.

Code

Fieldset, label, 의도된 button type 으로 만든 풀 폼·html
<!-- Canonical 폼 해부 -->
<form action="/api/signup" method="post">
  <fieldset>
    <legend>계정 만들기</legend>

    <div class="field">
      <label for="email">Email</label>
      <input
        id="email"
        name="email"
        type="email"
        required
        autocomplete="email"
      />
    </div>

    <div class="field">
      <label for="password">Password</label>
      <input
        id="password"
        name="password"
        type="password"
        required
        minlength="8"
        autocomplete="new-password"
      />
    </div>
  </fieldset>

  <fieldset>
    <legend>플랜 골라</legend>
    <label><input type="radio" name="plan" value="free" required /> Free</label>
    <label><input type="radio" name="plan" value="pro" /> Pro ($9/mo)</label>
    <label><input type="radio" name="plan" value="team" /> Team ($29/mo)</label>
  </fieldset>

  <button type="submit">Sign up</button>
  <button type="button" onclick="resetWithConfirmation()">Clear</button>
</form>
실수 한 자리에 다 모은 버전·html
<!-- 잘못된 버전 — 흔한 실수 모음 -->
<form>                              <!-- action 없음, method 없음 -->
  <div>Email</div>                  <!-- label 없음, 연결 없음 -->
  <input type="text" />             <!-- name 없음, id 없음, type=email 없음 -->

  <div>Password</div>
  <input type="text" />             <!-- password 가 일반 텍스트! -->

  <input type="radio" value="free" /> Free  <!-- name 없음 → radio 그룹 안 묶임 -->
  <input type="radio" value="pro" /> Pro

  <button>Sign up</button>          <!-- type 없음 → 기본 submit, 여기는 OK -->
  <button>Cancel</button>           <!-- type 없음 → 이것도 제출! -->
</form>
암시적 vs 명시적 label·html
<!-- 암시적 label (wrap) — id 필요 없음 -->
<label>
  Username
  <input name="username" type="text" required />
</label>

<!-- 명시적 label (for/id) — input 과 label 이 parent/child 가 아니라
     sibling 이어야 스타일링이 되는 경우 선호 -->
<label for="bio">Bio</label>
<textarea id="bio" name="bio" rows="4"></textarea>

External links

Exercise

다음을 가진 sign-up 폼 만들어: email (required, validated), password (required, 최소 8 자), name (required), country (최소 3 옵션의 select), '우리를 어떻게 알게 됐어?' 의 fieldset 안 radio 4 옵션, 'I agree to terms' required 체크박스, 그리고 버튼 둘: Submit 과 Cancel (cancel 은 제출 안 해야 해). Lighthouse → Accessibility 돌려. 100 점 나와야 해. 그러고 나서 VoiceOver 켜고 Tab 으로 순회 — 모든 필드가 label 과 상태를 announce 해야 해.
Hint
Lighthouse 가 label 빠짐 불평하면, for/id 페어 어디 빼먹은 거. Cancel 이 폼 제출하면 type="button" 빼먹은 거. Radio 그룹이 그룹으로 안 읽히면 <fieldset><legend> 빼먹은 거.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.