본문 바로가기

리스트, 테이블, 폼 요소

목록과 표로 정보를 구조화하고 input·textarea·select를 조합해 기본 회원가입 폼을 작성합니다.

앞 글에서는 텍스트, 링크, 이미지를 다뤘습니다. 이 글에서는 정보를 더 체계적으로 보여 주는 목록과 표, 그리고 사용자에게서 데이터를 입력받는 폼(form)을 다룹니다. 목록으로 항목을 나열하고, 표로 데이터를 행과 열에 정리하며, 폼으로 입력을 받아 서버에 보내는 기본 방법을 익힌 뒤 간단한 회원가입 폼을 만듭니다.

HTML 요소는 데이터가 가진 관계에 맞춰 고릅니다

STRUCTURED HTML

HTML 요소는 데이터가 가진 관계에 맞춰 고릅니다

항목의 순서, 이름과 설명, 행·열 비교, 사용자 제출은 서로 다른 관계이며 함께 조합할 수 있습니다.

정보 관계에서 HTML 요소로 연결순서가 핵심이 아닌 목록은 ul, 순서가 의미인 목록은 ol, 이름과 설명은 dl, 행·열 비교는 table, 입력 제출은 form으로 각각 연결된다. 화살표는 선택 근거이며 다섯 선택은 서로 배타적이지 않다.
순서가 핵심 아님관심사 목록
맞는 요소맞는 요소순서가 핵심 아님 · 관심사 목록 → ul · 해당 부분에 사용
ul해당 부분에 사용
순서가 의미 있음조리 절차
맞는 요소맞는 요소순서가 의미 있음 · 조리 절차 → ol · 해당 부분에 사용
ol해당 부분에 사용
이름과 설명용어 풀이
맞는 요소맞는 요소이름과 설명 · 용어 풀이 → dl · 해당 부분에 사용
dl해당 부분에 사용
행·열로 비교품목·수량·가격
맞는 요소맞는 요소행·열로 비교 · 품목·수량·가격 → table · 해당 부분에 사용
table해당 부분에 사용
사용자 입력 제출가입 입력
맞는 요소맞는 요소사용자 입력 제출 · 가입 입력 → form · 해당 부분에 사용
form해당 부분에 사용
좁은 화면에서 같은 다섯 선택 근거각 정보 관계 아래로 맞는 요소가 연결된다. 관심사와 ul, 절차와 ol, 용어와 dl, 행·열과 table, 제출과 form이다. 다섯 관계를 한 페이지에서 조합할 수 있으며 위에서 아래로 모두 실행하라는 뜻이 아니다.
순서가 핵심 아님관심사 목록
맞는 요소맞는 요소순서가 핵심 아님 · 관심사 목록 → ul · 해당 부분에 사용
ul해당 부분에 사용
순서가 의미 있음조리 절차
맞는 요소맞는 요소순서가 의미 있음 · 조리 절차 → ol · 해당 부분에 사용
ol해당 부분에 사용
이름과 설명용어 풀이
맞는 요소맞는 요소이름과 설명 · 용어 풀이 → dl · 해당 부분에 사용
dl해당 부분에 사용
행·열로 비교품목·수량·가격
맞는 요소맞는 요소행·열로 비교 · 품목·수량·가격 → table · 해당 부분에 사용
table해당 부분에 사용
사용자 입력 제출가입 입력
맞는 요소맞는 요소사용자 입력 제출 · 가입 입력 → form · 해당 부분에 사용
form해당 부분에 사용

ul · 순서가 핵심이 아닌 항목

<ul>
  <li>HTML</li>
  <li>CSS</li>
</ul>

관심사처럼 항목 순서를 바꾸어도 의미가 같은 목록입니다.

ol · 순서가 의미인 항목

<ol>
  <li>재료 준비</li>
  <li>조리</li>
</ol>

절차·순위처럼 순서가 내용을 설명하는 목록입니다.

dl · 이름과 설명의 관계

<dl>
  <dt>HTML</dt>
  <dd>문서 구조</dd>
</dl>

용어 풀이·메타데이터 등 이름과 값 또는 설명의 그룹을 표현합니다.

table · 행과 열의 비교

학습 계획 표 예기술과 학습 목표 두 열, HTML과 구조 및 CSS와 표현의 두 데이터 행. 행과 열의 제목이 값을 설명한다.학습 계획 · 표 이름기술학습 목표HTML구조CSS표현

caption과 th의 scope로 표 이름과 머리글 관계를 설명합니다. 배치만 만들려고 쓰는 요소는 아닙니다.

form · 사용자 입력을 제출

<form action="/register"
  method="post">
  <!-- label과 named 입력 -->
  <button type="submit">가입</button>
</form>

form은 입력을 모아 제출하는 관계입니다. 내부에 목록이나 데이터 표가 함께 들어갈 수 있습니다.

다섯 요소는 서로 배타적인 선택지가 아닙니다. 한 문서나 폼 안에 여러 관계가 있으면 각 부분에 맞는 요소를 조합합니다.

위 그림은 데이터가 가진 관계에 따라 HTML 요소를 고르는 기준을 보여 줍니다. 관심사처럼 순서가 핵심이 아닌 항목은 ul, 조리 절차처럼 순서가 의미를 가지는 항목은 ol, 용어 풀이처럼 이름과 설명이 짝을 이루는 정보는 dl, 품목과 수량, 가격처럼 행과 열로 비교하는 데이터는 table, 가입 입력처럼 사용자가 값을 제출하는 경우는 form입니다.

화살표는 고르는 기준일 뿐이며 다섯 요소가 서로 배타적인 것은 아닙니다. 그림 아래의 설명처럼 한 문서나 폼 안에 여러 관계가 있으면 각 부분에 맞는 요소를 함께 씁니다. 예를 들어 폼 안에 목록이나 표가 들어갈 수 있습니다.


목록 요소

목록은 관련 있는 항목을 묶어 보여 줄 때 씁니다. 순서 없는 목록, 순서 있는 목록, 설명 목록의 세 가지가 있습니다.

순서 없는 목록: <ul>

<ul>(unordered list)은 항목의 순서가 의미를 바꾸지 않을 때 쓰며, 각 항목은 <li>(list item)로 씁니다. 브라우저는 보통 각 항목 앞에 글머리 기호(•)를 붙입니다. 내비게이션 메뉴나 분류처럼 순서를 바꿔도 뜻이 같은 항목에 씁니다. 순서가 없다는 것이 항목이 덜 중요하다는 뜻은 아닙니다.

<h3>제가 좋아하는 과일들</h3>
<ul>
    <li>사과</li>
    <li>바나나</li>
    <li>딸기</li>
</ul>

순서 있는 목록: <ol>

<ol>(ordered list)은 레시피의 단계, 순위, 지시 사항처럼 순서가 내용의 일부일 때 씁니다. 항목은 마찬가지로 <li>로 쓰며, 브라우저는 기본으로 1부터 번호를 붙입니다.

<h3>커피 만드는 법</h3>
<ol>
    <li>원두를 갈아 준비합니다.</li>
    <li>뜨거운 물을 준비합니다.</li>
    <li>천천히 물을 부어 커피를 추출합니다.</li>
    <li>맛있게 드세요!</li>
</ol>

<ol>의 type 속성으로 번호의 종류를 바꿀 수 있습니다. 기본값인 type="1"은 숫자(1, 2, 3), type="a"와 type="A"는 소문자와 대문자 알파벳, type="i"와 type="I"는 소문자와 대문자 로마 숫자입니다.

<h3>시험 점수 등급</h3>
<ol type="A">
    <li>A 학점</li>
    <li>B 학점</li>
    <li>C 학점</li>
</ol>

start 속성은 시작 번호를 바꿉니다. 다음 목록은 5, 6, 7로 번호가 붙습니다.

<h3>재개되는 작업 목록</h3>
<ol start="5">
    <li>이전 작업 마무리</li>
    <li>새로운 기능 개발 시작</li>
    <li>테스트 및 배포</li>
</ol>

설명 목록: <dl>

설명 목록(description list)은 용어와 그 설명을 짝지어 나열할 때 씁니다. 용어 사전이나 질문과 답변(FAQ)에 알맞습니다. <dl>이 목록 전체를 감싸고, <dt>(description term)가 용어를, <dd>(description details)가 그 설명을 나타냅니다.

<h3>용어 사전</h3>
<dl>
    <dt>HTML</dt>
    <dd>웹 페이지의 구조를 정의하는 마크업 언어입니다.</dd>

    <dt>CSS</dt>
    <dd>웹 페이지의 스타일과 디자인을 담당하는 스타일 시트 언어입니다.</dd>

    <dt>JavaScript</dt>
    <dd>웹 페이지에 동적인 기능과 상호작용을 부여하는 프로그래밍 언어입니다.</dd>
</dl>

표 요소

표(table)는 데이터를 행(row)과 열(column)로 정리해 보여 줄 때 씁니다. 통계, 제품 비교, 시간표처럼 행과 열의 관계가 있는 데이터에 알맞으며, 여러 요소를 조합해 만듭니다.

  • <table>: 표 전체를 감쌉니다.
  • <caption>: 표의 제목이나 설명입니다.
  • <thead>, <tbody>, <tfoot>: 머리글 행, 본문 행, 요약 행을 각각 묶습니다.
  • <tr>(table row): 한 행입니다.
  • <th>(table header): 다른 셀의 제목이 되는 머리글 셀입니다. 열의 제목뿐 아니라 행의 제목에도 씁니다.
  • <td>(table data): 실제 데이터가 들어가는 셀입니다.

<th>에는 scope 속성으로 이 머리글이 설명하는 방향을 적을 수 있습니다. scope="col"은 아래쪽 열을, scope="row"는 오른쪽 행을 설명한다는 뜻이며, 스크린 리더는 이 정보로 각 데이터 셀을 읽을 때 해당 머리글을 함께 읽어 줍니다.

<h3>월별 웹사이트 방문자 수</h3>
<table>
    <caption>2024년 월별 방문자 통계</caption>
    <thead>
        <tr>
            <th scope="col">월</th>
            <th scope="col">방문자 수</th>
            <th scope="col">신규 방문자</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th scope="row">1월</th>
            <td>10,500</td>
            <td>3,200</td>
        </tr>
        <tr>
            <th scope="row">2월</th>
            <td>12,300</td>
            <td>4,100</td>
        </tr>
        <tr>
            <th scope="row">3월</th>
            <td>15,800</td>
            <td>5,500</td>
        </tr>
    </tbody>
    <tfoot>
        <tr>
            <th scope="row">총계</th>
            <td>38,600</td>
            <td>12,800</td>
        </tr>
    </tfoot>
</table>

이 표의 총계는 방문자 수 10,500 + 12,300 + 15,800 = 38,600, 신규 방문자 3,200 + 4,100 + 5,500 = 12,800입니다. 월 이름과 “총계”를 th scope="row"로 쓴 덕분에, 스크린 리더는 12,300 셀을 읽을 때 “2월, 방문자 수”라는 머리글을 함께 알려 줄 수 있습니다.

셀 합치기: colspan과 rowspan

셀의 colspan 속성은 여러 열을, rowspan 속성은 여러 행을 한 셀로 합칩니다. 두 속성 모두 값으로 합칠 칸의 개수를 받으며, colspan을 쓴 셀은 옆으로, rowspan을 쓴 셀은 아래로 넓어집니다. 다음 시간표에서는 화요일의 CSS 심화 셀과 마지막 행의 점심 시간 셀을 보면 됩니다.

<h3>시간표 (예시)</h3>
<table>
    <thead>
        <tr>
            <th scope="col">시간</th>
            <th scope="col">월요일</th>
            <th scope="col">화요일</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>9:00 - 10:00</td>
            <td>HTML 기초</td>
            <td rowspan="2">CSS 심화</td>
        </tr>
        <tr>
            <td>10:00 - 11:00</td>
            <td>JavaScript 기초</td>
        </tr>
        <tr>
            <td colspan="3">점심 시간</td>
        </tr>
    </tbody>
</table>

화요일의 rowspan="2" 셀은 다음 행까지 차지하므로, 두 번째 행에는 화요일 셀을 다시 쓰지 않고 셀이 두 개뿐입니다. 점심 시간의 colspan="3" 셀은 시간, 월요일, 화요일 세 열을 한 셀로 합칩니다. 각 행의 셀 수를 셀 때는 위 행에서 내려온 셀과 합친 열 수까지 계산해야 열이 어긋나지 않습니다.

표는 데이터를 보여 줄 때만 씁니다. 예전에는 페이지의 배치를 잡으려고 표를 쓰는 테이블 레이아웃도 있었지만, 스크린 리더가 배치용 칸을 데이터로 읽어 혼란을 주고 화면 크기에 맞추기도 어렵습니다. 배치는 CSS의 Flexbox나 Grid로 만듭니다.


폼 요소

로그인, 회원 가입, 검색, 게시글 작성처럼 대부분의 웹 서비스는 사용자의 입력을 받습니다. 이런 입력 양식을 폼이라고 하며, <form> 요소 안에 여러 입력 요소를 넣어 만듭니다.

레이블에서 서버 검증까지 이어지는 폼 제출 계약 — 상태 추적

required/type 제약 검사가 실패한 제출 A는 요청 전에 중단됩니다. 독립적인 유효한 제출 B는 name 기준 수집과 서버 검증으로 진행하며 서버 판정은 별도 경계입니다.

브라우저 검증 뒤에 제출하고 서버에서 다시 검증합니다

FORM SUBMISSION

브라우저 검증 뒤에 제출하고 서버에서 다시 검증합니다

입력 id는 레이블 연결, name은 제출 키입니다. 클라이언트 검증과 서버 검증은 서로 다른 단계입니다.

브라우저 검증 분기와 서버 경계일반 제출의 제약 검증 실패는 요청 전 입력 오류로 끝난다. 통과하면 named 입력을 구성하고 HTTP 요청을 보내며 별도 서버 검증이 오류 또는 성공 응답으로 갈라진다. 교재는 브라우저 폼만 구현하고 서버는 제공하지 않는다.브라우저 · 교재 예제 구현서버 · 별도 구현 필요통과실패HTTP 요청거절승인제약 검증required · type 등입력 오류요청 전 중단제출 데이터name별 수집·인코딩서버 검증권한·중복·업무 규칙오류 응답입력과 연결성공 응답결과 표시
좁은 화면의 같은 검증 분기브라우저 제약 검증의 실패 화살표는 입력 오류에서 끝난다. 오른쪽 통과 선은 오류 노드를 우회해 제출 데이터로 이어진다. HTTP 요청은 서버 경계를 넘는다. 서버 검증의 거절은 오류 응답, 오른쪽 승인 선은 성공 응답으로 연결된다. 서버 부분은 별도 구현이다.브라우저 · 예제 구현서버 · 별도 구현실패통과HTTP 요청거절승인제약 검증required · type 등입력 오류이 가지는 요청 중단제출 데이터name 수집·인코딩서버 검증권한·중복·업무 규칙오류 응답입력과 연결성공 응답결과 표시
  1. 레이블과 입력 연결

    label의 for와 입력 id를 연결합니다. 제출 데이터의 키는 id가 아니라 name입니다.

  2. 브라우저 제약 검증

    일반 제출에서 required·type·minlength 등을 검사합니다. 실패하면 요청 전에 멈춥니다. novalidate·formnovalidate 또는 form.submit()은 검증을 우회할 수 있습니다.

  3. 제출 항목 구성

    이름이 있고 활성화된 입력을 수집합니다. 체크되지 않은 checkbox·radio, disabled 입력, 제출에 쓰지 않은 버튼 등은 제외됩니다.

  4. 인코딩해 요청

    GET은 URL query에, POST는 enctype에 맞춘 본문에 보냅니다. POST 자체가 내용을 암호화하지는 않습니다.

  5. 서버가 다시 검증

    서버는 형식·권한·중복·업무 규칙 등을 별도로 확인합니다. 브라우저 검증만 신뢰하지 않습니다.

  6. 결과 또는 입력별 오류

    성공 결과를 보여주거나 오류를 해당 입력과 연결합니다. 비밀번호를 응답에 다시 채워 넣지 않습니다.

교재의 가입 폼 · 일부 제출 항목

POST /register

user_id=codex
gender=male
terms_agree=on

가입 예의 일부를 펼친 표시입니다. 실제 기본 인코딩은 URL-encoded 본문이며 이메일·비밀번호 등 나머지 named 입력도 포함됩니다.

왜 terms_agree=on인가?

교재 checkbox에는 name="terms_agree"와 required가 있고 value는 없습니다. 체크되면 기본값 on이 들어갑니다. 체크하지 않으면 일반 제출은 검증에서 멈춥니다.

교재 예제는 HTML 폼까지 구현합니다. /register 서버와 오류 응답은 별도 구현이 필요합니다. 비밀번호 확인의 required는 두 비밀번호가 같은지 검사하지 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 첫 장면은 invalid 제출 A, 나머지 두 장면은 독립적인 valid 제출 B의 수집·서버 검증입니다. 실패한 제출 A가 입력 수정이나 새 제출 없이 유효한 요청으로 바뀌는 흐름이 아닙니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석레이블과 입력 연결: label의 for와 입력 id를 연결합니다. 제출 데이터의 키는 id가 아니라 name입니다.
원도표 조건·주석브라우저 제약 검증: 일반 제출에서 required·type·minlength 등을 검사합니다. 실패하면 요청 전에 멈춥니다. novalidate·formnovalidate 또는 form.submit()은 검증을 우회할 수 있습니다.
원도표 조건·주석제출 항목 구성: 이름이 있고 활성화된 입력을 수집합니다. 체크되지 않은 checkbox·radio, disabled 입력, 제출에 쓰지 않은 버튼 등은 제외됩니다.
원도표 조건·주석인코딩해 요청: GET은 URL query에, POST는 enctype에 맞춘 본문에 보냅니다. POST 자체가 내용을 암호화하지는 않습니다.
원도표 조건·주석서버가 다시 검증: 서버는 형식·권한·중복·업무 규칙 등을 별도로 확인합니다. 브라우저 검증만 신뢰하지 않습니다.
원도표 설명성공 결과를 보여 주거나 오류를 해당 입력과 연결합니다. 비밀번호를 응답에 다시 채워 넣지 않습니다.
원도표 조건·주석결과 또는 입력별 오류: 성공 결과를 보여 주거나 오류를 해당 입력과 연결합니다. 비밀번호를 응답에 다시 채워 넣지 않습니다.
원도표 설명교재 checkbox에는 name="terms_agree"와 required가 있고 value는 없습니다. 체크되면 기본값 on이 들어갑니다. 체크하지 않으면 일반 제출은 검증에서 멈춥니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
제출 A · 잘못된 필드폼: invalid / HTTP: 없음required/type 조건을 통과하지 못한다브라우저의 유효성 검사는 조건에 맞지 않는 제출 요청을 막는다
제출 B · 유효한 필드폼: valid / 수집: name 기준조건을 통과하고 name이 있는 값을 수집한다화면 요소 모두가 제출 데이터가 되는 것은 아니다
제출 B · 서버 판정HTTP: 요청 발생 / 서버: 최종 업무 검증보낸 값은 서버에서 독립적으로 검사한다브라우저 통과는 서버 허용 보장이 아니다

레이블에서 서버 검증까지 이어지는 폼 제출 계약 — 상태 추적

required/type 제약 검사가 실패한 제출 A는 요청 전에 중단됩니다. 독립적인 유효한 제출 B는 name 기준 수집과 서버 검증으로 진행하며 서버 판정은 별도 경계입니다.

브라우저 검증 뒤에 제출하고 서버에서 다시 검증합니다

FORM SUBMISSION

브라우저 검증 뒤에 제출하고 서버에서 다시 검증합니다

입력 id는 레이블 연결, name은 제출 키입니다. 클라이언트 검증과 서버 검증은 서로 다른 단계입니다.

브라우저 검증 분기와 서버 경계일반 제출의 제약 검증 실패는 요청 전 입력 오류로 끝난다. 통과하면 named 입력을 구성하고 HTTP 요청을 보내며 별도 서버 검증이 오류 또는 성공 응답으로 갈라진다. 교재는 브라우저 폼만 구현하고 서버는 제공하지 않는다.브라우저 · 교재 예제 구현서버 · 별도 구현 필요통과실패HTTP 요청거절승인제약 검증required · type 등입력 오류요청 전 중단제출 데이터name별 수집·인코딩서버 검증권한·중복·업무 규칙오류 응답입력과 연결성공 응답결과 표시
좁은 화면의 같은 검증 분기브라우저 제약 검증의 실패 화살표는 입력 오류에서 끝난다. 오른쪽 통과 선은 오류 노드를 우회해 제출 데이터로 이어진다. HTTP 요청은 서버 경계를 넘는다. 서버 검증의 거절은 오류 응답, 오른쪽 승인 선은 성공 응답으로 연결된다. 서버 부분은 별도 구현이다.브라우저 · 예제 구현서버 · 별도 구현실패통과HTTP 요청거절승인제약 검증required · type 등입력 오류이 가지는 요청 중단제출 데이터name 수집·인코딩서버 검증권한·중복·업무 규칙오류 응답입력과 연결성공 응답결과 표시
  1. 레이블과 입력 연결

    label의 for와 입력 id를 연결합니다. 제출 데이터의 키는 id가 아니라 name입니다.

  2. 브라우저 제약 검증

    일반 제출에서 required·type·minlength 등을 검사합니다. 실패하면 요청 전에 멈춥니다. novalidate·formnovalidate 또는 form.submit()은 검증을 우회할 수 있습니다.

  3. 제출 항목 구성

    이름이 있고 활성화된 입력을 수집합니다. 체크되지 않은 checkbox·radio, disabled 입력, 제출에 쓰지 않은 버튼 등은 제외됩니다.

  4. 인코딩해 요청

    GET은 URL query에, POST는 enctype에 맞춘 본문에 보냅니다. POST 자체가 내용을 암호화하지는 않습니다.

  5. 서버가 다시 검증

    서버는 형식·권한·중복·업무 규칙 등을 별도로 확인합니다. 브라우저 검증만 신뢰하지 않습니다.

  6. 결과 또는 입력별 오류

    성공 결과를 보여주거나 오류를 해당 입력과 연결합니다. 비밀번호를 응답에 다시 채워 넣지 않습니다.

교재의 가입 폼 · 일부 제출 항목

POST /register

user_id=codex
gender=male
terms_agree=on

가입 예의 일부를 펼친 표시입니다. 실제 기본 인코딩은 URL-encoded 본문이며 이메일·비밀번호 등 나머지 named 입력도 포함됩니다.

왜 terms_agree=on인가?

교재 checkbox에는 name="terms_agree"와 required가 있고 value는 없습니다. 체크되면 기본값 on이 들어갑니다. 체크하지 않으면 일반 제출은 검증에서 멈춥니다.

교재 예제는 HTML 폼까지 구현합니다. /register 서버와 오류 응답은 별도 구현이 필요합니다. 비밀번호 확인의 required는 두 비밀번호가 같은지 검사하지 않습니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
  • 첫 장면은 invalid 제출 A, 나머지 두 장면은 독립적인 valid 제출 B의 수집·서버 검증입니다. 실패한 제출 A가 입력 수정이나 새 제출 없이 유효한 요청으로 바뀌는 흐름이 아닙니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 조건·주석레이블과 입력 연결: label의 for와 입력 id를 연결합니다. 제출 데이터의 키는 id가 아니라 name입니다.
원도표 조건·주석브라우저 제약 검증: 일반 제출에서 required·type·minlength 등을 검사합니다. 실패하면 요청 전에 멈춥니다. novalidate·formnovalidate 또는 form.submit()은 검증을 우회할 수 있습니다.
원도표 조건·주석제출 항목 구성: 이름이 있고 활성화된 입력을 수집합니다. 체크되지 않은 checkbox·radio, disabled 입력, 제출에 쓰지 않은 버튼 등은 제외됩니다.
원도표 조건·주석인코딩해 요청: GET은 URL query에, POST는 enctype에 맞춘 본문에 보냅니다. POST 자체가 내용을 암호화하지는 않습니다.
원도표 조건·주석서버가 다시 검증: 서버는 형식·권한·중복·업무 규칙 등을 별도로 확인합니다. 브라우저 검증만 신뢰하지 않습니다.
원도표 설명성공 결과를 보여 주거나 오류를 해당 입력과 연결합니다. 비밀번호를 응답에 다시 채워 넣지 않습니다.
원도표 조건·주석결과 또는 입력별 오류: 성공 결과를 보여 주거나 오류를 해당 입력과 연결합니다. 비밀번호를 응답에 다시 채워 넣지 않습니다.
원도표 설명교재 checkbox에는 name="terms_agree"와 required가 있고 value는 없습니다. 체크되면 기본값 on이 들어갑니다. 체크하지 않으면 일반 제출은 검증에서 멈춥니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
제출 A · 잘못된 필드폼: invalid / HTTP: 없음required/type 조건을 통과하지 못한다브라우저의 유효성 검사는 조건에 맞지 않는 제출 요청을 막는다
제출 B · 유효한 필드폼: valid / 수집: name 기준조건을 통과하고 name이 있는 값을 수집한다화면 요소 모두가 제출 데이터가 되는 것은 아니다
제출 B · 서버 판정HTTP: 요청 발생 / 서버: 최종 업무 검증보낸 값은 서버에서 독립적으로 검사한다브라우저 통과는 서버 허용 보장이 아니다

위 그림은 폼을 제출할 때 브라우저와 서버에서 일어나는 일을 단계별로 보여 줍니다. 레이블의 for와 입력의 id가 둘을 연결하고, 제출하면 브라우저가 먼저 required, type, minlength 같은 제약을 검사합니다. 검사에 실패하면 요청을 보내지 않고 입력 오류를 보여 주며, 통과하면 name이 있는 입력을 모아 인코딩한 뒤 HTTP 요청을 보냅니다.

서버는 받은 데이터를 형식, 권한, 중복, 업무 규칙에 따라 다시 검사해 오류나 성공을 응답합니다. 그림 아래의 설명처럼 이 글의 예제는 브라우저 쪽 HTML 폼까지만 구현하며, /register를 처리하는 서버는 따로 만들어야 합니다. 또 체크박스에 value가 없으면 체크했을 때 기본값 on이 전송되므로, 그림의 예시 요청에 terms_agree=on이 들어 있습니다.

<form>의 주요 속성은 두 가지입니다. action은 폼 데이터를 보낼 서버의 URL이고, method는 보낼 때 쓸 HTTP 메서드입니다. GET은 데이터를 URL 끝의 쿼리 문자열로 붙여 보내므로 검색처럼 결과 주소를 공유해도 되는 요청에 씁니다. POST는 데이터를 요청 본문에 담아 보내므로 URL에 값이 남지 않지만, POST 자체가 데이터를 암호화하지는 않습니다. 비밀번호 같은 정보는 HTTPS로 보내고 서버에서도 검증해야 합니다. 파일을 보낼 때는 enctype="multipart/form-data"를 함께 지정합니다.

<form action="/submit-form" method="POST">
    <!-- 여기에 입력 요소를 넣습니다. -->
</form>

입력 필드: <input>

<input>은 폼에서 가장 많이 쓰는 요소이며, type 속성에 따라 여러 종류의 입력 필드가 됩니다. 내용과 종료 태그가 없는 빈 요소입니다.

type="text"는 한 줄 텍스트 입력 필드입니다.

<label for="username">사용자 이름:</label>
<input type="text" id="username" name="username" placeholder="이름을 입력하세요">

입력 요소에서 자주 쓰는 속성은 다음과 같습니다.

  • id: 문서에서 이 입력을 가리키는 고유한 이름으로, <label>의 for나 자바스크립트에서 이 입력을 찾을 때 씁니다.
  • name: 제출할 때 값과 함께 보내는 키입니다. id가 있어도 name이 없으면 그 입력의 값은 전송되지 않습니다. id는 연결용, name은 전송용으로 역할이 다릅니다.
  • placeholder: 입력이 비어 있을 때 보이는 안내 문구입니다.
  • value: 입력의 초기값입니다.
  • required: 반드시 입력해야 하는 필드임을 나타냅니다.

<label>은 입력 필드의 이름표입니다. for 속성에 입력의 id를 적어 연결하면, 스크린 리더가 입력에 초점이 갔을 때 레이블을 읽어 주고, 레이블을 클릭해도 그 입력에 초점이 가거나 체크박스가 선택됩니다.

그 밖의 주요 type은 다음과 같습니다. type="password"는 입력한 글자를 점이나 별표로 가립니다.

<label for="password">비밀번호:</label>
<input type="password" id="password" name="password" required>

가려지는 것은 화면에 보이는 글자뿐이고 전송되는 값은 입력한 그대로이므로, 앞에서 말한 대로 HTTPS로 보내야 합니다. 이 예에는 required도 붙어 있어서, 비워 둔 채 제출하면 브라우저가 전송을 막고 입력을 요청합니다.

type="email"은 이메일 주소를 받으며, 제출할 때 브라우저가 @가 들어간 이메일 형식인지 검사합니다. 다만 형식만 볼 뿐 실제로 메일을 받을 수 있는 주소인지는 알 수 없습니다. 또 다음 예처럼 required가 없으면 빈 값은 검사 없이 그대로 제출됩니다.

<label for="email">이메일:</label>
<input type="email" id="email" name="email">

type="number"는 숫자를 받습니다. min과 max로 범위를 정하고, 화살표 버튼으로 값을 늘리거나 줄일 수 있습니다.

<label for="quantity">수량:</label>
<input type="number" id="quantity" name="quantity" min="1" max="10" value="1">

type="radio"는 여러 선택지 중 하나만 고르는 라디오 버튼입니다. 같은 name을 가진 라디오 버튼들이 한 그룹이 되어, 하나를 고르면 나머지가 해제됩니다. 제출할 때는 고른 버튼의 value가 name의 값으로 전송됩니다.

<p>성별:</p>
<input type="radio" id="male" name="gender" value="male">
<label for="male">남성</label><br>
<input type="radio" id="female" name="gender" value="female">
<label for="female">여성</label>

type="checkbox"는 여러 개를 동시에 고를 수 있는 체크박스입니다. 같은 name의 체크박스를 여러 개 고르면 hobby=reading&hobby=travel처럼 같은 키가 여러 번 전송되고, 체크하지 않은 체크박스는 전송되지 않습니다.

<p>좋아하는 취미 (다중 선택 가능):</p>
<input type="checkbox" id="reading" name="hobby" value="reading">
<label for="reading">독서</label><br>
<input type="checkbox" id="travel" name="hobby" value="travel">
<label for="travel">여행</label>

type="file"은 파일을 고르는 필드입니다. accept 속성은 파일 선택 창에 보여 줄 파일 종류를 제안할 뿐, 사용자가 다른 파일을 고르거나 요청을 직접 만들 수도 있으므로 서버의 파일 검사를 대신하지 않습니다.

<label for="upload_file">파일 업로드:</label>
<input type="file" id="upload_file" name="uploaded_file" accept=".pdf,.doc">

선택한 파일의 내용은 앞에서 본 것처럼 method="POST"와 enctype="multipart/form-data"를 지정한 폼에서만 제대로 전송됩니다. 이 설정이 없으면 파일 내용 대신 파일 이름만 전송됩니다.

type="submit"은 폼을 제출하는 버튼이며, value 속성이 버튼에 보이는 글자가 됩니다.

<input type="submit" value="제출하기">

이 버튼은 자신을 감싼 <form>을 제출하며, 텍스트 입력 필드에서 Enter 키를 눌러도 같은 제출이 일어납니다. 브라우저는 전송하기 전에 required나 type="email" 같은 조건을 먼저 검사하고, 조건에 맞지 않는 필드가 있으면 전송하지 않고 그 필드에 안내를 띄웁니다.

여러 줄 입력: <textarea>

<textarea>는 여러 줄의 텍스트를 받습니다. rows와 cols로 처음 보이는 줄 수와 글자 너비를 정합니다. 초기값은 value 속성이 아니라 시작 태그와 종료 태그 사이에 적습니다.

<label for="message">메시지:</label><br>
<textarea id="message" name="message" rows="5" cols="30" placeholder="메시지를 입력하세요."></textarea>

드롭다운 목록: <select>와 <option>

<select>는 미리 정한 선택지 가운데 하나를 고르는 드롭다운 목록이며, 각 선택지는 <option>으로 씁니다. <option>의 value는 서버로 보낼 값이고, selected는 처음부터 선택되어 있을 선택지를 나타냅니다.

<label for="country">국가 선택:</label>
<select id="country" name="country">
    <option value="">-- 국가를 선택하세요 --</option>
    <option value="kr">대한민국</option>
    <option value="us" selected>미국</option>
    <option value="jp">일본</option>
</select>

이 목록은 처음에 미국이 선택된 상태로 보이며, 그대로 제출하면 country=us가 전송됩니다. 첫 번째 선택지의 value=""는 아무것도 고르지 않은 상태를 나타내므로, <select>에 required를 붙이면 이 선택지가 선택된 채로는 제출되지 않습니다.

버튼: <button>

<button>은 클릭할 수 있는 버튼입니다. <input type="submit">과 비슷하지만 안에 글자뿐 아니라 아이콘 같은 HTML 내용도 넣을 수 있어 더 유연합니다.

<button type="submit">회원가입</button>
<button type="reset">초기화</button>
<button type="button">클릭 미!</button>

type 속성은 버튼의 동작을 정합니다. 폼 안에서 type을 생략하면 기본값인 submit이 되어 폼을 제출합니다. reset은 폼의 입력을 HTML에 적힌 초기값과 초기 선택 상태로 되돌리며, 모든 값을 빈칸으로 만드는 것은 아닙니다. button은 아무 기본 동작이 없는 일반 버튼으로, 자바스크립트로 동작을 붙일 때 씁니다. 폼 안에서 자바스크립트 동작용 버튼에 type="button"을 빠뜨리면, 누를 때마다 폼이 제출되는 실수가 생깁니다.

폼 요소 묶기: <fieldset>과 <legend>

관련된 입력을 의미상으로 묶을 때는 <fieldset>으로 감싸고, <legend>로 그 묶음의 제목을 붙입니다. 브라우저는 기본으로 묶음 둘레에 테두리를 그립니다.

<fieldset>
    <legend>개인 정보</legend>
    <label for="first_name">이름:</label>
    <input type="text" id="first_name" name="first_name"><br><br>
    <label for="last_name">성:</label>
    <input type="text" id="last_name" name="last_name">
</fieldset>

스크린 리더는 묶음 안의 입력에 초점이 가면 <legend>의 제목을 함께 읽어 줍니다. 성별 같은 라디오 버튼 그룹은 각 버튼의 레이블만으로는 무엇을 고르는지 알 수 없으므로, <fieldset>과 <legend>로 묶어 질문을 알려 주는 것이 좋습니다.

더 깊이: 브라우저의 검사를 코드에서 다루기

required나 type="email" 같은 속성이 만드는 검사는 제약 검증(constraint validation)이라는 HTML 표준 기능이고, 자바스크립트에서도 같은 결과를 읽을 수 있습니다. 입력 요소의 validity 속성은 valueMissing, typeMismatch, patternMismatch, tooShort, rangeOverflow처럼 실패한 이유마다 불리언 값을 가진 ValidityState 객체입니다. checkValidity()는 검사 결과를 불리언으로 돌려주고 실패한 요소에 invalid 이벤트를 보내며, reportValidity()는 여기에 더해 브라우저의 안내 말풍선까지 보여 줍니다.

비밀번호 확인처럼 속성으로 표현할 수 없는 규칙은 setCustomValidity()로 더합니다. 빈 문자열이 아닌 메시지를 넘기면 그 요소는 그 메시지를 가진 실패 상태가 되고, 빈 문자열을 넘겨야 다시 통과 상태가 됩니다. 그래서 입력이 바뀔 때마다 조건을 다시 계산해 둘 중 하나를 넘기는 것이 보통이며, 한 번 실패 메시지를 넣고 지우지 않으면 사용자가 값을 고쳐도 계속 제출이 막힙니다.

몇 가지 동작은 예상과 다릅니다. minlength와 maxlength는 사용자가 직접 입력한 값에만 검사되므로, 코드로 value를 넣은 뒤 checkValidity()를 호출해도 길이 위반으로 보지 않습니다. CSS의 :invalid는 페이지를 열자마자 빈 필수 입력에도 맞아 처음부터 빨간 테두리를 보여 주기 쉬운데, 사용자가 입력을 건드리거나 제출을 시도한 뒤에만 맞는 :user-invalid를 쓰면 이 문제를 피할 수 있습니다. 폼에 novalidate 속성을 붙이면 제출할 때의 자동 검사와 안내를 끄고 이 API로 직접 처리할 수 있습니다. 자세한 규칙은 MDN의 제약 검증 설명에 있습니다.


실습: 회원가입 폼

배운 요소를 조합해 회원가입 폼을 만들어 봅니다. web-dev-practice 폴더에 signup.html을 만들고 다음 코드를 씁니다.

signup.html
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>회원가입 페이지</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f2f2f2;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
        }
        .signup-container {
            background-color: white;
            padding: 30px 40px;
            border-radius: 8px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.1);
            width: 100%;
            max-width: 450px;
        }
        h1 {
            text-align: center;
            color: #333;
            margin-bottom: 30px;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label,
        legend {
            display: block;
            margin-bottom: 8px;
            color: #555;
            font-weight: bold;
        }
        fieldset {
            border: none; /* 기본 테두리와 여백 제거 */
            padding: 0;
            margin: 0 0 20px;
        }
        input[type="text"],
        input[type="email"],
        input[type="password"],
        textarea,
        select {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 1em;
            box-sizing: border-box; /* padding과 border를 너비에 포함 */
        }
        input[type="radio"],
        input[type="checkbox"] {
            margin-right: 8px;
        }
        button[type="submit"] {
            width: 100%;
            padding: 12px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 1.1em;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }
        button[type="submit"]:hover {
            background-color: #0056b3;
        }
        .radio-group label,
        .checkbox-group label {
            display: inline-block;
            margin-right: 15px;
            font-weight: normal;
        }
    </style>
</head>
<body>
    <div class="signup-container">
        <h1>회원가입</h1>
        <form action="/register" method="POST">
            <div class="form-group">
                <label for="user_id">아이디:</label>
                <input type="text" id="user_id" name="user_id" placeholder="5~12자 영문/숫자" required minlength="5" maxlength="12">
            </div>

            <div class="form-group">
                <label for="user_email">이메일:</label>
                <input type="email" id="user_email" name="user_email" placeholder="[email protected]" required>
            </div>

            <div class="form-group">
                <label for="user_password">비밀번호:</label>
                <input type="password" id="user_password" name="user_password" placeholder="비밀번호 입력" required minlength="8">
            </div>

            <div class="form-group">
                <label for="password_confirm">비밀번호 확인:</label>
                <input type="password" id="password_confirm" name="password_confirm" placeholder="비밀번호 다시 입력" required>
            </div>

            <fieldset class="radio-group">
                <legend>성별</legend>
                <input type="radio" id="gender_male" name="gender" value="male" checked>
                <label for="gender_male">남성</label>
                <input type="radio" id="gender_female" name="gender" value="female">
                <label for="gender_female">여성</label>
                <input type="radio" id="gender_other" name="gender" value="other">
                <label for="gender_other">기타</label>
            </fieldset>

            <div class="form-group">
                <label for="birth_year">출생 연도:</label>
                <select id="birth_year" name="birth_year">
                    <option value="">-- 선택 --</option>
                </select>
            </div>

            <div class="form-group">
                <label for="self_intro">자기소개 (선택 사항):</label>
                <textarea id="self_intro" name="self_intro" rows="5" placeholder="간단히 자신을 소개해 주세요."></textarea>
            </div>

            <div class="form-group checkbox-group">
                <input type="checkbox" id="terms_agree" name="terms_agree" required>
                <label for="terms_agree">서비스 이용 약관에 동의합니다.</label>
            </div>

            <button type="submit">회원가입</button>
        </form>
    </div>

    <script>
        // 올해부터 1900년까지 출생 연도 선택지를 만듭니다.
        const birthYear = document.getElementById('birth_year');
        for (let year = new Date().getFullYear(); year >= 1900; year--) {
            birthYear.add(new Option(year, year));
        }
    </script>
</body>
</html>

예제를 짧게 하려고 CSS를 <style> 안에 직접 썼지만, 실제 프로젝트에서는 별도의 .css 파일로 나누는 편이 관리하기 좋습니다. 입력 상자에는 box-sizing: border-box를 주었으므로 width: 100%에 안쪽 여백과 테두리가 모두 포함되어, 상자가 카드 밖으로 넘치지 않습니다.

출생 연도 선택지는 맨 아래의 스크립트가 페이지를 열 때 만듭니다. new Option(year, year)는 보이는 글자와 value가 모두 그 연도인 선택지를 만들고, add()가 그것을 <select>에 차례로 붙입니다. 2026년에 열면 “-- 선택 --” 다음에 2026부터 1900까지 127개의 연도가 생깁니다. 반복문이 아직 낯설다면 5장의 조건문과 반복문을 참고하세요.

이 파일은 입력 화면과 브라우저의 기본 검사만 제공합니다. /register를 처리하는 서버 코드가 없으므로, 검사를 통과해 제출하면 브라우저가 /register 주소를 요청하고 오류 페이지가 보입니다. 예를 들어 Live Server로 열었다면 Live Server가 POST 요청을 처리하지 않으므로 HTTP 405 오류가 나옵니다. 이 오류는 HTML이 잘못되었다는 뜻이 아닙니다. 연습할 때는 실제 비밀번호 대신 연습용 값을 쓰세요.

입력 조건브라우저에서 보이는 결과이 예제만으로 확인할 수 없는 것
필수 필드나 약관을 비워 두고 제출필수 입력 안내가 나오고 제출이 막힘서버도 같은 규칙을 검사하는지
아이디를 abc처럼 3자로 입력minlength="5"에 맞지 않아 제출이 막힘이미 있는 아이디인지
아이디를 가나다라마로 입력placeholder는 안내일 뿐이라 제출이 막히지 않음영문과 숫자만 허용하는 규칙
두 비밀번호 칸에 서로 다른 값을 입력두 값이 같은지 검사하는 코드가 없어 제출이 막히지 않음비밀번호 확인 검사

placeholder는 안내 문구일 뿐이며, 규칙은 속성이나 코드로 따로 정해야 합니다. 예를 들어 영문과 숫자만 받으려면 pattern="[A-Za-z0-9]+" 같은 속성을 더합니다. 그래도 브라우저의 검사는 개발자 도구나 직접 만든 요청으로 우회할 수 있으므로 서버의 검증을 대신하지 않습니다. 기본 제출의 동작은 HTML 표준의 폼 소개에서 확인할 수 있습니다.


정리

  • 순서가 의미 없는 항목은 ul, 순서가 의미 있는 항목은 ol, 이름과 설명의 짝은 dl로 나열합니다.
  • 표는 caption, thead, tbody, tfoot, tr, th, td로 만들고, th의 scope로 머리글이 설명하는 방향을 알려 줍니다.
  • colspan과 rowspan으로 셀을 합칠 수 있으며, 표는 데이터에만 쓰고 배치는 CSS로 합니다.
  • label의 for는 입력의 id와 연결되고, 제출 데이터의 키는 name이며, name이 없는 입력은 전송되지 않습니다.
  • 브라우저는 제출 전에 required, type, minlength 같은 제약을 검사하지만, 서버도 같은 규칙을 다시 검사해야 합니다.
  • 폼 안의 <button>은 기본으로 제출 버튼이므로, 자바스크립트용 버튼에는 type="button"을 붙입니다.

연습 문제

1. 전송될 키와 값

회원가입 폼에서 아이디를 learner1로 입력하고, 성별은 기타, 약관에는 체크했습니다. 제출 데이터에 들어갈 아이디, 성별, 약관의 키와 값은 무엇일까요? user_id 입력에서 name 속성만 지우면 무엇이 달라질까요?

해설 보기

아이디는 user_id=learner1, 성별은 고른 라디오 버튼의 값인 gender=other, 약관은 value가 없는 체크박스이므로 기본값을 쓴 terms_agree=on입니다. 약관에 체크하지 않으면 이 값은 빠지는데, 이 폼에서는 required 때문에 아예 제출이 막힙니다.

name만 지우면 입력 상자와 for="user_id" 레이블의 연결은 그대로라 화면과 레이블 클릭은 똑같이 동작하지만, 그 입력값은 제출 데이터에서 빠집니다. 연결은 id가, 전송 키는 name이 맡기 때문입니다.

2. 시간표의 셀 수

시간표 예제의 두 번째 행에 실수로 <td>CSS 심화</td>를 하나 더 넣어 셀이 세 개가 되었습니다. 표는 어떻게 보일까요?

해설 보기

첫 행의 화요일 셀이 rowspan="2"로 두 번째 행의 화요일 칸까지 이미 차지하고 있습니다. 그래서 두 번째 행의 세 번째 셀은 화요일 칸에 들어가지 못하고 오른쪽에 새로운 네 번째 열을 만들어, 머리글이 없는 칸이 하나 튀어나와 보입니다.

rowspan이나 colspan을 쓸 때는 위 행에서 내려온 셀과 합친 열 수를 포함해 각 행의 칸 수가 같은지 확인합니다.

3. 버튼을 눌렀더니 제출되었다

폼 안에 자바스크립트로 미리보기를 보여 주려고 <button>미리보기</button>을 넣었습니다. 그런데 누를 때마다 페이지가 /register로 이동합니다. 왜 그럴까요? 어떻게 고칠까요?

해설 보기

폼 안의 <button>에 type을 쓰지 않으면 기본값이 submit이라, 누르면 폼이 제출되기 때문입니다. 필수 입력을 모두 채웠다면 검사를 통과해 /register로 요청을 보냅니다.

<button type="button">미리보기</button>처럼 type="button"을 적으면 기본 동작이 없어지므로, 자바스크립트로 붙인 동작만 실행됩니다.

다음 글 시맨틱 HTML과 접근성에서는 header, nav, main 같은 의미 요소로 문서의 영역을 나누고, 누구나 쓸 수 있는 페이지를 만드는 방법을 다룹니다.