1. <form>: 입력 데이터의 컨테이너
1.1 정의와 역할
<form> 은 사용자 입력 요소들을 묶고, 그 데이터를 서버로 전송하는 컨테이너다. <form> 안의 입력 요소들은 제출(submit) 시 하나의 데이터 묶음으로 서버에 전달된다.
<form action="/signup" method="post">
<label for="email">이메일</label>
<input type="email" id="email" name="email" required>
<button type="submit">가입하기</button>
</form>
1.2 핵심 속성
action — 데이터를 보낼 주소
<form action="/signup">...</form> <!-- 상대 경로 -->
<form action="https://api.example.com/signup">...</form> <!-- 절대 경로 -->
<form action="">...</form> <!-- 현재 페이지로 전송 -->
action 을 생략하면 현재 페이지의 URL로 전송된다.
method — 전송 방식
<form method="get">...</form> <!-- 기본값 -->
<form method="post">...</form>
GET | POST | |
|---|---|---|
| 데이터 위치 | URL 쿼리 문자열 (?key=value) | 요청 바디(body) |
| URL 노출 | 노출됨 | 노출되지 않음 |
| 캐시·북마크 | 가능 | 불가 |
| 데이터 길이 | 제한 있음 (브라우저·서버마다 다름) | 제한 없음 |
| 적합한 용도 | 검색, 필터, 정렬 | 로그인, 회원가입, 결제, 파일 업로드 |
name속성이 없으면 데이터가 전송되지 않는다폼을 제출하면
name="email"처럼name속성이 있는 입력 요소의 값만 서버로 전달된다.name이 없는 요소는 어떤 타입이든 제출 데이터에 포함되지 않는다. 이 점이 가장 흔한 폼 관련 실수다.
enctype — 데이터 인코딩 방식
method="post" 일 때만 의미 있는 속성이다.
enctype 값 | 설명 | 용도 |
|---|---|---|
application/x-www-form-urlencoded (기본값) | 키=값 쌍을 URL 인코딩 | 일반 폼 |
multipart/form-data | 데이터를 여러 파트로 분리 | 파일 업로드 시 필수 |
text/plain | 인코딩 없이 평문 | 디버깅 전용, 실제 사용 비권장 |
파일 업로드 폼은 반드시 enctype="multipart/form-data" 를 명시해야 한다.
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="profile-image">
<button type="submit">업로드</button>
</form>
기타 유용한 속성
| 속성 | 설명 |
|---|---|
novalidate | 브라우저 기본 유효성 검사 비활성화 |
autocomplete="off" | 폼 전체의 자동완성 비활성화 |
target="_blank" | 응답을 새 탭에서 표시 |
2. <input>: 다목적 입력 요소
2.1 기본 구조
<input> 은 닫는 태그가 없는 빈 요소(void element) 다. type 속성으로 입력 방식이 완전히 달라진다.
<input type="text" id="username" name="username" placeholder="사용자명">
2.2 disabled vs readonly
둘 다 입력을 막지만 동작이 다르다.
disabled | readonly | |
|---|---|---|
| 시각 | 회색으로 표시(흐릿함) | 일반 텍스트처럼 표시 |
| 포커스 | 탭으로 접근 불가 | 탭으로 접근 가능 |
| 폼 제출 | 제출 데이터에 포함되지 않음 | 제출 데이터에 포함됨 |
| 적합한 용도 | 아직 활성화되지 않은 필드 | 수정 불가하지만 값은 필요한 필드 |
2.3 <input> 타입 총정리
텍스트 계열
<!-- 단순 텍스트 -->
<input type="text" name="username" placeholder="사용자명">
<!-- 비밀번호: 입력 내용을 •로 마스킹 -->
<input type="password" name="password" autocomplete="current-password">
<!-- 이메일: 형식 검증 + 모바일에서 이메일 키보드 -->
<input type="email" name="email" autocomplete="email">
<!-- URL: 형식 검증 -->
<input type="url" name="website">
<!-- 전화번호: 형식 검증 없음, 모바일에서 숫자 키보드 -->
<input type="tel" name="phone" autocomplete="tel">
<!-- 검색창: 지우기(x) 버튼 등 검색 UX 제공 -->
<input type="search" name="q">
숫자·범위
<!-- 숫자: 위아래 화살표 스피너 -->
<input type="number" name="age" min="1" max="120" step="1">
<!-- 슬라이더 -->
<input type="range" name="volume" min="0" max="100" step="5" value="50">
날짜·시간
<!-- 날짜 (YYYY-MM-DD) -->
<input type="date" name="birthdate" min="1900-01-01" max="2026-12-31">
<!-- 시간 (HH:MM) -->
<input type="time" name="meeting-time">
<!-- 날짜+시간 -->
<input type="datetime-local" name="appointment">
<!-- 월 (YYYY-MM) -->
<input type="month" name="start-month">
<!-- 주 (YYYY-W##) -->
<input type="week" name="delivery-week">
선택형
<!-- 체크박스: 독립적인 boolean 값 -->
<input type="checkbox" id="agree" name="agree" value="yes">
<!-- 라디오: 같은 name 그룹에서 하나만 선택 -->
<input type="radio" id="male" name="gender" value="male">
<input type="radio" id="female" name="gender" value="female">
라디오 버튼의 핵심 규칙
같은 그룹의 라디오 버튼은 반드시 동일한
name을 가져야 한다. 그래야 브라우저가 "이 중 하나만 선택 가능"이라는 그룹 관계를 인식한다.value는 각각 달라야 서버에서 어떤 항목을 선택했는지 구분할 수 있다.
파일·색상·숨김
<!-- 파일 업로드 (form에 enctype="multipart/form-data" 필수) -->
<input type="file" name="avatar" accept="image/png, image/jpeg">
<input type="file" name="documents" accept=".pdf,.docx" multiple>
<!-- 색상 선택기 -->
<input type="color" name="theme-color" value="#3b82f6">
<!-- 숨겨진 값: 화면에 표시되지 않지만 폼 데이터로 전송 -->
<input type="hidden" name="csrf-token" value="a1b2c3d4">
버튼형 input (비권장)
<input type="submit" value="제출"> <!-- <button type="submit"> 사용 권장 -->
<input type="reset" value="초기화"> <!-- UX상 거의 쓰지 않음 -->
<input type="button" value="클릭"> <!-- <button type="button"> 사용 권장 -->
2.4 공통 유효성 검사 속성
| 속성 | 설명 | 적용 타입 |
|---|---|---|
required | 필수 입력 | 거의 모든 타입 |
minlength | 최소 글자 수 | text, password, email 등 |
maxlength | 최대 글자 수 | text, password, email 등 |
min | 최솟값 | number, date, range 등 |
max | 최댓값 | number, date, range 등 |
step | 값 증감 단위 | number, date, range |
pattern | 정규표현식 패턴 | text, tel, password 등 |
<!-- 한국 휴대폰 번호 패턴 검증 -->
<input
type="tel"
name="phone"
pattern="01[0-9]-[0-9]{3,4}-[0-9]{4}"
placeholder="010-0000-0000"
>
HTML 유효성 검사는 보안이 아니다
required,pattern,min/max같은 HTML 유효성 검사는 브라우저 수준에서 이루어지므로, 사용자가 개발자 도구로 속성을 제거하거나 HTTP 요청을 직접 조작해 우회할 수 있다. 반드시 서버에서도 동일한 검증을 수행해야 한다.
3. <label>: 입력 요소의 이름표
3.1 왜 반드시 필요한가
<label> 은 입력 요소와 그 이름을 프로그래밍적으로 연결한다. 없으면 스크린 리더 사용자는 "텍스트 입력란이 있습니다"만 들을 뿐, 무엇을 입력해야 하는지 모른다. 또한 레이블을 클릭하면 연결된 입력 요소에 포커스가 이동해 터치 영역을 넓혀 주는 UX 효과도 있다.
3.2 두 가지 연결 방법
방법 A — for/id 연결 (권장):
<label for="email">이메일</label>
<input type="email" id="email" name="email">
<label> 의 for 속성값과 <input> 의 id 값이 일치하면 연결된다.
방법 B — 감싸기(wrapping):
<label>
이메일
<input type="email" name="email">
</label>
<input> 을 <label> 로 감싸면 for/id 없이 자동 연결된다. 단, CSS 스타일링이 복잡해질 수 있어 방법 A를 더 권장한다.
3.3 플레이스홀더는 레이블이 아니다
<!-- 잘못된 사용: placeholder만 있고 label 없음 -->
<input type="email" name="email" placeholder="이메일을 입력하세요"> ❌
<!-- 올바른 사용: label + placeholder 함께 -->
<label for="email">이메일</label>
<input type="email" id="email" name="email" placeholder="example@email.com"> ✅
placeholder 는 입력 전에 예시나 힌트를 보여주는 용도지, 레이블의 대체물이 아니다. 입력을 시작하면 사라져서 사용자가 무엇을 입력하는지 잊어버리게 만든다. 레이블은 항상 별도로 제공해야 한다.
4. <textarea>: 여러 줄 텍스트 입력
4.1 기본 구조
<textarea> 는 여러 줄의 텍스트를 입력받는 요소다. <input> 과 달리 여는 태그와 닫는 태그가 모두 필요하며, 태그 사이의 텍스트가 초기값이 된다.
<label for="bio">자기소개</label>
<textarea
id="bio"
name="bio"
rows="5"
cols="40"
maxlength="500"
placeholder="자신을 소개해 주세요 (최대 500자)"
></textarea>
4.2 주요 속성
| 속성 | 설명 |
|---|---|
rows | 기본 표시 행 수 |
cols | 기본 표시 열(문자) 수 |
maxlength | 최대 입력 글자 수 |
minlength | 최소 입력 글자 수 |
readonly | 읽기 전용 (값은 제출됨) |
disabled | 비활성화 (값 미제출) |
wrap | soft(기본): 줄바꿈 문자 미전송 / hard: 줄바꿈 문자도 전송 |
4.3 초기값 작성 주의
<!-- 잘못된 사용: 개행·공백이 초기값에 포함됨 -->
<textarea name="bio">
여기에 내용이 들어갑니다
</textarea>
<!-- 올바른 사용: 여는 태그 바로 뒤에 초기값 (또는 비워두기) -->
<textarea name="bio">여기에 내용이 들어갑니다</textarea>
<textarea name="bio"></textarea>
5. <select> 와 <option>: 드롭다운 목록
5.1 기본 구조
<label for="country">국가</label>
<select id="country" name="country">
<option value="">-- 선택하세요 --</option>
<option value="kr" selected>대한민국</option>
<option value="us">미국</option>
<option value="jp">일본</option>
</select>
<option value="">처럼 빈 값의 첫 항목을 두면 "미선택" 상태를 명확히 할 수 있다.selected속성이 있는<option>이 초기 선택값이 된다.
5.2 <optgroup>: 옵션 그룹화
관련 옵션들을 시각적으로 묶을 때 쓴다.
<select name="language">
<optgroup label="아시아">
<option value="ko">한국어</option>
<option value="ja">일본어</option>
<option value="zh">중국어</option>
</optgroup>
<optgroup label="유럽">
<option value="en">영어</option>
<option value="de">독일어</option>
</optgroup>
</select>
5.3 다중 선택: multiple
<select name="interests" multiple size="5">
<option value="html">HTML</option>
<option value="css">CSS</option>
<option value="js">JavaScript</option>
<option value="react">React</option>
<option value="node">Node.js</option>
</select>
multiple 속성을 추가하면 Ctrl(Mac은 Cmd)이나 Shift 클릭으로 여러 항목을 선택할 수 있다. size 는 한 번에 표시할 항목 수다. 단, multiple + <select> 는 UX가 직관적이지 않아 체크박스 그룹으로 대체하는 경우가 많다.
6. <datalist>: 자동완성 추천 목록
6.1 정의와 역할
<datalist> 는 <input> 에 입력 도중 나타나는 추천 후보 목록을 제공한다. <select> 와 다르게 사용자가 목록에 없는 값도 직접 입력할 수 있다.
<label for="lang">선호 프로그래밍 언어</label>
<input
type="text"
id="lang"
name="lang"
list="languages"
placeholder="언어를 입력하거나 선택하세요"
>
<datalist id="languages">
<option value="JavaScript">
<option value="TypeScript">
<option value="Python">
<option value="Rust">
<option value="Go">
</datalist>
<input> 의 list 속성값이 <datalist> 의 id 와 일치해야 연결된다.
6.2 <select> 와의 차이
<select> | <datalist> | |
|---|---|---|
| 입력 방식 | 목록에서만 선택 | 직접 입력 + 추천 목록 |
| 자유 입력 | 불가 | 가능 |
| 값 강제 | 목록 중 하나 | 제한 없음 |
| 적합한 용도 | 정해진 값 중 하나 | 자동완성, 검색 힌트 |
6.3 <datalist> 를 지원하는 input 타입
text, search, url, tel, email, number, date, month, week, time, datetime-local, range, color 타입과 함께 쓸 수 있다.
7. <button>: 버튼
7.1 세 가지 타입
<button> 은 반드시 type 속성을 명시해야 한다. 명시하지 않으면 기본값인 type="submit" 으로 동작해, 폼 안에 있는 버튼이 의도치 않게 폼을 제출할 수 있다.
<!-- 폼 제출 -->
<button type="submit">가입하기</button>
<!-- 폼 초기화 (UX상 거의 사용 안 함) -->
<button type="reset">초기화</button>
<!-- 폼과 무관한 일반 버튼 (JavaScript 동작 연결) -->
<button type="button" onclick="openModal()">미리보기</button>
7.2 <button> vs <input type="submit">
<!-- input: 텍스트만 담을 수 있음 -->
<input type="submit" value="제출">
<!-- button: HTML을 담을 수 있어 아이콘, span 등 추가 가능 -->
<button type="submit">
<img src="icon-send.svg" alt=""> 제출
</button>
<button> 이 더 유연하므로 현재는 <button> 사용을 권장한다.
7.3 폼 바깥에서 특정 폼과 연결하기
<button> 이 <form> 밖에 있어도 form 속성으로 연결할 수 있다.
<form id="signup-form" action="/signup" method="post">
<input type="email" name="email">
</form>
<!-- 폼 밖에 있지만 signup-form에 연결 -->
<button type="submit" form="signup-form">가입하기</button>
8. <fieldset> 과 <legend>: 관련 필드 묶기
8.1 정의와 역할
<fieldset> 은 관련된 입력 요소를 그룹으로 묶는다. <legend> 는 그 그룹의 제목이다. 특히 라디오 버튼과 체크박스 그룹에서는 거의 필수다.
<!-- 라디오 버튼 그룹 -->
<fieldset>
<legend>성별</legend>
<label>
<input type="radio" name="gender" value="male"> 남성
</label>
<label>
<input type="radio" name="gender" value="female"> 여성
</label>
<label>
<input type="radio" name="gender" value="other"> 기타
</label>
</fieldset>
<!-- 체크박스 그룹 -->
<fieldset>
<legend>관심 분야 (복수 선택 가능)</legend>
<label><input type="checkbox" name="interests" value="html"> HTML</label>
<label><input type="checkbox" name="interests" value="css"> CSS</label>
<label><input type="checkbox" name="interests" value="js"> JavaScript</label>
</fieldset>
스크린 리더는 각 라디오/체크박스를 읽을 때 <legend> 의 텍스트를 함께 읽어준다. 즉 "남성, 성별 라디오 버튼" 처럼 안내된다. <fieldset>·<legend> 없이는 스크린 리더 사용자가 각 선택지가 무엇의 선택지인지 알 수 없다.
9. 폼의 접근성과 구성 원칙
9.1 원칙 1: 모든 입력 요소에 레이블을 달아라
눈에 보이는 <label> 이 가장 좋다. 디자인상 레이블을 보이지 않게 처리해야 한다면 aria-label 을 사용한다.
<!-- 시각적 레이블 숨김 처리 (스크린 리더는 읽음) -->
<label for="search" class="visually-hidden">검색어 입력</label>
<input type="search" id="search" name="q" placeholder="검색...">
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
9.2 원칙 2: 오류 메시지는 구체적이고 연결되어야 한다
<label for="email">이메일</label>
<input
type="email"
id="email"
name="email"
aria-describedby="email-error"
aria-invalid="true"
>
<p id="email-error" role="alert">
올바른 이메일 형식을 입력해 주세요. (예: example@email.com)
</p>
aria-describedby: 입력 요소와 오류 메시지 요소를 프로그래밍적으로 연결.aria-invalid="true": 스크린 리더에 현재 입력값이 유효하지 않음을 알림.role="alert": 오류 메시지가 동적으로 나타날 때 스크린 리더가 즉시 읽도록 함.- 오류 내용은 "오류"가 아닌 "무엇이 잘못됐고 어떻게 고쳐야 하는지" 를 구체적으로 설명한다.
9.3 원칙 3: 필수 필드를 명확히 표시하라
<label for="email">
이메일 <abbr title="필수 입력 항목">*</abbr>
</label>
<input type="email" id="email" name="email" required>
- 시각적 표시(
*)와 함께required속성을 함께 쓴다. - 별표(
*)에<abbr title="필수 입력 항목">으로 의미를 설명하면 스크린 리더 사용자도 이해할 수 있다. - 폼 상단에 "* 표시된 항목은 필수 입력입니다" 같은 안내 문구도 권장된다.
9.4 원칙 4: autocomplete 속성을 활용하라
반복 입력을 줄이고 비밀번호 관리자와의 연동을 돕는다.
<input type="text" name="name" autocomplete="name">
<input type="email" name="email" autocomplete="email">
<input type="tel" name="phone" autocomplete="tel">
<input type="password" name="password" autocomplete="new-password">
<input type="password" name="current" autocomplete="current-password">
autocomplete="new-password" 는 비밀번호 관리자에게 "이것은 새 비밀번호 설정 필드"임을 알려 기존 저장된 비밀번호를 자동으로 채우지 않게 한다.
9.5 원칙 5: 색깔만으로 오류를 표시하지 마라
빨간 테두리만으로 오류를 나타내면 색맹 사용자가 오류를 인식하지 못한다. 색 + 아이콘 + 텍스트 메시지를 함께 제공한다.
10. [실습] 종합 회원가입 폼 구성하기
지금까지 배운 모든 입력 요소를 활용해 실제 사용 가능한 회원가입 폼을 만든다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>회원가입 | uzu-dev</title>
</head>
<body>
<main>
<h1>회원가입</h1>
<p><abbr title="필수 입력 항목">*</abbr> 표시된 항목은 필수입니다.</p>
<form action="/signup" method="post" enctype="multipart/form-data" novalidate>
<!-- 1. 기본 정보 -->
<fieldset>
<legend>기본 정보</legend>
<div>
<label for="name">
이름 <abbr title="필수 입력 항목">*</abbr>
</label>
<input
type="text"
id="name"
name="name"
required
minlength="2"
maxlength="20"
autocomplete="name"
placeholder="홍길동"
>
</div>
<div>
<label for="email">
이메일 <abbr title="필수 입력 항목">*</abbr>
</label>
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
placeholder="example@email.com"
>
</div>
<div>
<label for="password">
비밀번호 <abbr title="필수 입력 항목">*</abbr>
</label>
<input
type="password"
id="password"
name="password"
required
minlength="8"
autocomplete="new-password"
>
<p id="password-hint">영문, 숫자, 특수문자 포함 8자 이상</p>
</div>
<div>
<label for="password-confirm">
비밀번호 확인 <abbr title="필수 입력 항목">*</abbr>
</label>
<input
type="password"
id="password-confirm"
name="password-confirm"
required
autocomplete="new-password"
aria-describedby="password-hint"
>
</div>
</fieldset>
<!-- 2. 추가 정보 -->
<fieldset>
<legend>추가 정보</legend>
<div>
<label for="birthdate">생년월일</label>
<input
type="date"
id="birthdate"
name="birthdate"
min="1900-01-01"
max="2026-12-31"
autocomplete="bday"
>
</div>
<div>
<label for="phone">전화번호</label>
<input
type="tel"
id="phone"
name="phone"
pattern="01[0-9]-[0-9]{3,4}-[0-9]{4}"
placeholder="010-0000-0000"
autocomplete="tel"
>
</div>
<div>
<label for="website">개인 사이트</label>
<input
type="url"
id="website"
name="website"
placeholder="https://example.com"
autocomplete="url"
>
</div>
<div>
<label for="avatar">프로필 사진</label>
<input
type="file"
id="avatar"
name="avatar"
accept="image/png, image/jpeg, image/webp"
>
</div>
</fieldset>
<!-- 3. 성별 (라디오 그룹) -->
<fieldset>
<legend>성별</legend>
<label><input type="radio" name="gender" value="male"> 남성</label>
<label><input type="radio" name="gender" value="female"> 여성</label>
<label><input type="radio" name="gender" value="other"> 기타</label>
<label><input type="radio" name="gender" value="prefer-not"> 밝히지 않음</label>
</fieldset>
<!-- 4. 관심 분야 (체크박스 그룹) -->
<fieldset>
<legend>관심 분야 (복수 선택 가능)</legend>
<label><input type="checkbox" name="interests" value="html"> HTML</label>
<label><input type="checkbox" name="interests" value="css"> CSS</label>
<label><input type="checkbox" name="interests" value="js"> JavaScript</label>
<label><input type="checkbox" name="interests" value="react"> React</label>
<label><input type="checkbox" name="interests" value="node"> Node.js</label>
</fieldset>
<!-- 5. 자기소개 -->
<div>
<label for="bio">자기소개</label>
<textarea
id="bio"
name="bio"
rows="4"
maxlength="300"
placeholder="간단히 자신을 소개해 주세요 (최대 300자)"
></textarea>
</div>
<!-- 6. 국가 (드롭다운) -->
<div>
<label for="country">국가</label>
<select id="country" name="country" autocomplete="country">
<option value="">-- 선택하세요 --</option>
<optgroup label="아시아">
<option value="KR" selected>대한민국</option>
<option value="JP">일본</option>
<option value="CN">중국</option>
</optgroup>
<optgroup label="기타">
<option value="US">미국</option>
<option value="GB">영국</option>
</optgroup>
</select>
</div>
<!-- 7. 선호 기술 (datalist 자동완성) -->
<div>
<label for="tech">주력 기술</label>
<input
type="text"
id="tech"
name="tech"
list="tech-list"
placeholder="기술명을 입력하거나 선택하세요"
>
<datalist id="tech-list">
<option value="HTML">
<option value="CSS">
<option value="JavaScript">
<option value="TypeScript">
<option value="React">
<option value="Next.js">
<option value="Vue">
<option value="Python">
</datalist>
</div>
<!-- 8. 뉴스레터 구독 -->
<div>
<label>
<input type="checkbox" name="newsletter" value="yes">
uzu-dev 뉴스레터를 구독합니다.
</label>
</div>
<!-- 9. 이용약관 동의 (필수) -->
<div>
<label>
<input type="checkbox" name="terms" value="agreed" required>
<a href="/terms" target="_blank" rel="noopener noreferrer">이용약관</a> 및
<a href="/privacy" target="_blank" rel="noopener noreferrer">개인정보처리방침</a>에
동의합니다. <abbr title="필수 입력 항목">*</abbr>
</label>
</div>
<!-- 제출 -->
<div>
<button type="submit">가입하기</button>
<button type="button" onclick="history.back()">취소</button>
</div>
</form>
</main>
</body>
</html>