HTML에서 폼 만드는 법
폼은 입력 필드와 제출 버튼을 담은 <form> 요소예요. 각 필드에는 name이 필요해요. 값이 그 이름으로 전송되거든요. 폼을 채우고 Send를 눌러 보세요. 미리보기는 어디로도 아무것도 보내지 않고, 브라우저가 보냈을 내용을 그대로 보여 줘요.
구성 요소는 이래요.
action은 데이터가 갈 URL이고,method는 보내는 방법(여기서는post)이에요.- 모든 필드에
name이 있어요. 값은 독자가 입력한 내용이에요. <label>은 화면과 스크린 리더에게 각 필드의 이름을 알려 줘요(HTML label 참고).required와type="email"은 브라우저가 보내기 전에 필드를 검사하게 해요. 이름을 비워 두거나 이메일에abc를 넣고 보내 보세요.- 제출 버튼이 폼을 보내요. 텍스트 필드에서 Enter를 눌러도 보내져요.
action과 method: 데이터가 어디로, 어떻게 가는지
| 속성 | 값 | 기본값 |
|---|---|---|
action | 아무 URL | 현재 페이지 URL |
method | get, post, dialog | get |
enctype | application/x-www-form-urlencoded, multipart/form-data, text/plain | application/x-www-form-urlencoded |
method="get"이면 필드가 URL의 쿼리 문자열이 돼요. name="q"인 검색 폼은 /search?q=blue+shoes로 가요. 결과를 북마크하고 공유할 수 있으니 검색과 필터에 알맞아요. method="post"이면 필드가 요청 본문에 담겨서 주소창이나 방문 기록에 나타나지 않아요. 로그인, 생성, 수정, 삭제하는 모든 작업과 비밀번호에는 POST를 쓰세요.
파일 업로드(<input type="file">)가 있는 폼은 method="post"와 enctype="multipart/form-data"가 필요해요. 그러지 않으면 파일 이름만 전송돼요. method="dialog"는 <dialog> 안의 폼용이에요. 제출하면 요청을 보내는 대신 대화 상자를 닫아요.
무엇이 전송되나요
name이 있고 disabled가 아닌 필드만 전송돼요. Submit을 누르고 필드와 폼이 보내는 내용을 비교해 보세요.
폼은 city, zip, source, action을 보내요. 이름이 없는 필드, 비활성화된 필드, 체크하지 않은 체크박스는 빠져요. 숨겨진 입력 필드는 보이지 않지만 전송되고, 제출 버튼은 사용된 버튼이라서 자기 name과 value를 보내요.
fieldset과 legend로 필드 묶기
<fieldset>은 관련된 필드 주위에 상자를 그리고, <legend>는 그 제목이에요. 포커스가 묶음 안으로 들어오면 스크린 리더가 legend를 알려 줘서, 아래 같은 라디오 버튼 묶음을 이해할 수 있게 돼요. "Delivery, Standard, 라디오 버튼"처럼요.
fieldset에 disabled를 주면 안의 모든 필드가 한꺼번에 비활성화되고 하나도 전송되지 않아요. 속성을 지우면 선물 관련 필드를 쓸 수 있게 돼요.
기본 유효성 검사: required, pattern 등
브라우저는 제출하기 전에 이 속성들을 검사하고, 처음으로 실패한 필드에 메시지를 보여 줘요.
| 속성 | 검사하는 것 |
|---|---|
required | 필드가 비어 있지 않음(체크박스는 체크됨) |
type="email", type="url" | 값이 이메일 주소나 URL처럼 생김 |
minlength, maxlength | 텍스트 길이 |
min, max, step | 숫자와 날짜 범위 |
pattern | 값 전체가 정규 표현식과 일치함 |
:invalid와 :valid는 상태에 따라 필드를 꾸며요. 보통은 :user-invalid가 더 나아요. 독자가 필드를 바꾸고 다른 곳으로 이동했거나 제출을 시도한 뒤에만 적용돼서, 빈 폼이 처음부터 빨간색으로 뒤덮이지 않아요.
pattern에서 사람들이 자주 걸려 넘어지는 점이 세 가지 있어요. 값 전체와 일치해야 해요(^나 $가 필요 없어요). 필드가 비어 있지 않을 때만 실행되니 required와 함께 쓰세요. 그리고 브라우저의 오류 메시지는 일반적이라서, 필드 옆에 보이는 텍스트로 형식을 설명하세요. Chrome과 Firefox는 title 텍스트를 메시지에 덧붙이지만 놓치기 쉬워요.
서버의 유효성 검사를 테스트할 때처럼 검사를 끄려면 <form>에 novalidate를 추가하세요. 제출 버튼 하나만 formnovalidate로 검사를 건너뛸 수도 있어요.
브라우저 유효성 검사는 독자의 편의를 위한 거예요. 누구든 페이지를 고치거나 검사 없이 요청을 보낼 수 있으니, 서버는 모든 값을 다시 검사해야 해요.
setCustomValidity로 직접 메시지 정하기
"두 비밀번호가 일치해야 함"처럼 속성으로 쓸 수 없는 규칙은 JavaScript로 메시지를 정하세요. 사용자 정의 메시지가 비어 있지 않은 필드는 유효하지 않고, 빈 문자열을 주면 다시 유효해져요.
반복 입력 필드에는 name이 없어서 비밀번호는 한 번만 전송돼요.
JavaScript로 제출 처리하기
페이지를 떠나지 않고 폼을 보내려면 submit을 듣고 preventDefault()를 호출한 뒤 FormData로 필드를 읽으세요. 버튼의 click이 아니라 submit을 들으면 Enter를 포함한 모든 제출 방법을 잡을 수 있고, 기본 유효성 검사를 통과한 뒤에만 실행돼요.
Object.fromEntries(new FormData(form))는 필드를 일반 객체로 바꿔요. 값이 여러 개일 수 있는 필드(이름을 공유하는 체크박스, multiple select)에는 대신 formData.getAll('name')을 쓰세요. 객체는 마지막 값만 남기거든요.
자주 하는 실수
name이 없는 필드. 제출된 데이터에서 조용히 빠져요.- 라벨 대신 placeholder를 씀. 독자가 입력하는 순간 힌트가 사라져요.
<label>을 쓰세요. type없는<button>을 제출 외의 용도로 씀. 폼 안에서는 제출 버튼이에요.- 폼 안에 폼을 넣음. 폼 중첩은 올바르지 않고, 브라우저는 안쪽
<form>태그를 무시해요. - 폼의
submit대신 제출 버튼의click을 들음. Enter 키로 제출하는 경우를 놓쳐요. - 서버에서 브라우저 유효성 검사를 믿음. 서버에서 다시 검사하세요.
- 비밀번호에 GET을 씀. URL, 방문 기록, 서버 로그에 남아요.
자주 묻는 질문
HTML에서 폼은 어떻게 만드나요?
입력 필드를 <form action="/signup" method="post">로 감싸고, 모든 필드에 name과 <label>을 주고, 마지막에 <button type="submit">을 넣으세요. 폼이 제출되면 브라우저가 각 name=value 쌍을 action URL로 보내요.
폼에서 GET과 POST는 무엇이 다른가요?
method="get"(기본값)은 /search?q=shoes처럼 필드를 URL에 넣어서, 북마크할 수 있어야 하는 검색과 필터에 알맞아요. method="post"는 필드를 요청 본문에 담아 보내서, 로그인, 회원가입, 데이터를 바꾸는 모든 작업에 알맞아요.
폼 필드가 왜 제출되지 않나요?
name 속성이 없거나 disabled 상태예요. 이름이 있고 활성화된 필드만 전송돼요. 체크하지 않은 체크박스나 라디오 버튼도 아예 빠져요.
fieldset과 legend는 어디에 쓰나요?
<fieldset>은 관련된 필드를 묶고 <legend>는 그 묶음의 제목이에요. 포커스가 묶음 안으로 들어오면 스크린 리더가 legend를 알려 줘서 라디오 버튼 묶음을 이해할 수 있게 되고, fieldset에 disabled를 주면 안의 모든 필드가 비활성화돼요.
JavaScript 없이 HTML로 폼 유효성 검사를 하려면 어떻게 하나요?
속성을 쓰세요. required, type="email", minlength, maxlength, min, max, pattern이에요. 브라우저가 제출을 막고 처음으로 잘못된 필드에 메시지를 보여 줘요. 이 검사는 독자의 브라우저에서 실행되어 우회할 수 있으니 서버에서도 항상 검사하세요.
폼 제출 시 페이지가 새로고침되지 않게 하려면 어떻게 하나요?
폼의 submit 이벤트를 듣고 event.preventDefault()를 호출한 뒤 fetch로 직접 데이터를 보내세요. form.addEventListener('submit', (e) => { e.preventDefault(); fetch(form.action, { method: 'POST', body: new FormData(form) }); })처럼요.