Forms and buttons in HTML

김동현·2026년 3월 2일

mdn 학습 번역 - HTML

목록 보기
14/31

HTML의 폼(Form)과 버튼(Button)

사용자와 상호작용하기

지금까지 코스를 진행하면서 사용자가 웹과 상호작용할 수 있는 몇 가지 방법을 보셨을 거예요.

  • 링크(Links)는 같은 페이지 내에서 혹은 다른 페이지의 다른 콘텐츠 섹션으로 이동할 때 사용해요.
  • <video><audio> 요소는 보통 재생/일시정지, 빨리 감기, 되감기 등의 컨트롤을 제공해서 사용자가 원하는 대로 미디어 콘텐츠를 소비할 수 있게 해줍니다.

하지만 이런 기능들은 사용자가 콘텐츠를 수동적으로 소비하는 '단방향' 상호작용을 촉진하는 경향이 있어요. 물론 이것도 좋지만, 웹은 기본적으로 '양방향' 경험이랍니다! 웹사이트 사용자들은 콘텐츠와 서비스를 어떻게 경험할지 선호도를 설정하기도 하고, 택시를 예약하거나 콜백을 요청하기도 하죠. 피드백을 남기고 불만을 접수하기도 하고, 상품을 구매해서 집으로 배송받기도 합니다.

이런 양방향 경험을 제공하려면 버튼과 폼을 반드시 사용해야 해요.

보통 버튼은 HTML의 <button> 요소를 사용해서 만들어요. (가끔은 <input> 요소에 type 속성을 button이나 submit으로 설정해서 만들기도 한답니다). 이렇게 누를 수 있는 형태의 버튼은 범용적이라서 개발자의 상상력과 코딩 실력만 있다면 어떤 기능이든 연결해서 실행되게 만들 수 있죠.

폼은 <form>, <label>, <input>, <select> 같은 요소들을 조합해서 만듭니다. 폼 요소들을 활용하면 단순한 버튼보다 훨씬 복잡한 컨트롤을 만들 수 있어요. 예를 들어, UI 디자인의 여러 테마 중 하나를 선택할 수 있게 해주는 드롭다운 메뉴 같은 것 말이죠.

가장 결정적으로, 폼은 사용자가 웹사이트 서버로 정보를 제출해야 할 때 기입하는 양식을 만드는 데 사용됩니다. 이커머스 사이트를 생각해보세요. 사고 싶은 물건을 검색할 때는 폼을 사용해 검색어를 입력하죠. 물건값을 결제하고 배송을 확정 지을 때는 배송지 주소를 입력하는 폼과 신용카드 정보를 입력하는 또 다른 폼을 사용하게 됩니다.

이번 문서에서는 폼 요소를 활용하는 이런 전통적인 용도에 주로 집중해 볼 거예요. 폼 내부에서 입력된 데이터를 서버로 전송하기 위해 버튼이 흔히 사용된다는 점도 꼭 기억해 두세요.

💡 강사의 팁! > 나중에 React나 Next.js를 다루게 되시면, 전통적인 폼 제출 방식처럼 페이지가 새로고침되는 것을 막고, 자바스크립트로 데이터를 가로채서 서버와 통신(Fetch API 등)하는 방식을 많이 쓰게 될 거예요. (e.preventDefault() 기억나시죠?) 하지만 그 바탕이 되는 HTML 폼의 시맨틱한 구조와 기본 동작 원리를 제대로 아는 것이 접근성과 웹 표준을 지키는 튼튼한 프론트엔드 개발의 첫걸음입니다!

이 중요한 이론을 바탕으로, 코드를 직접 살펴보면서 버튼과 폼이 어떻게 구현되는지 알아볼까요?


버튼

위에서도 살짝 언급했듯이, 웹에서 버튼은 몇 가지 주요한 용도를 가집니다. 가장 먼저, UI 컨트롤을 만들 때 유용한 기능을 트리거(실행)하는 데 사용되죠. 가장 단순한 버튼은 아래 코드처럼 구현할 수 있어요.

<button>Press me</button>

화면에는 이렇게 렌더링됩니다:

<button></button> 태그 사이에 있는 텍스트는 버튼 안에 렌더링되고, 브라우저가 기본적으로 제공하는 스타일링이 적용되어서 딱 봤을 때 버튼처럼 보이고 버튼처럼 동작하게 됩니다. 여기까지는 아주 좋죠. 하지만 문제가 하나 있어요. 이렇게 덩그러니 놓인 버튼 혼자서는 아무런 유용한 일도 하지 않는다는 거예요. 이 버튼이 쓸모 있는 일을 하게 만들려면 나중에 배울 폼 안에 넣거나, 자바스크립트를 추가해야 합니다.

예를 들어, 위 버튼에 다음과 같은 자바스크립트를 적용한다면 어떨까요?

<button>Press me</button>
const btn = document.querySelector("button");
btn.addEventListener("click", () => {
  btn.textContent = "YOU CLICKED ME!! ❤️";
  setTimeout(() => {
    btn.textContent = "Press me";
  }, 1000);
});

클릭해 보면 이런 결과가 나옵니다.

지금 당장 이 자바스크립트가 어떻게 동작하는지 완벽하게 이해하실 필요는 없어요. 코스 후반부에서 더 자세히 배우게 될 거니까요.

다음 섹션에서는 버튼의 두 번째 주요 용도인 폼 제출(Submitting forms)에 대해 알아보겠습니다.


폼의 구조

기본적인 폼은 크게 세 가지 요소로 구성됩니다.

  • <form> 요소: 다른 모든 폼 콘텐츠를 감싸는 껍데기 역할을 합니다. <form></form> 태그 안에 있는 모든 폼 컨트롤들은 하나의 같은 폼으로 취급되고, 폼이 제출될 때 그 안의 데이터들이 함께 묶여서 전송돼요.
  • 하나 이상의 <label> 요소와 폼 컨트롤 요소(주로 <input> 요소가 쓰이지만 <select> 같은 다른 타입도 있어요)의 쌍:
    • 폼 컨트롤 요소는 사용자가 데이터를 입력하거나 선택할 수 있게 해주고, 폼 제출 시 이 데이터가 서버로 전송됩니다.
    • <label> 요소는 연결된 폼 컨트롤이 어떤 데이터를 입력받아야 하는지 설명해주는 식별용 꼬리표 역할을 해요.
  • 폼을 제출하는 데 사용되는 <button> 요소.

위의 세 가지가 모두 포함된 아주 기본적인 예제를 살펴볼게요. 이 폼은 뉴스레터 구독을 위해 사용자의 이름과 이메일을 물어보는 용도로 쓸 수 있겠네요. (아직 어떤 서버와도 연결되어 있지 않으니 실제로 작동하지는 않으니 걱정 마세요!)

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>First form</title>
  </head>
  <body>
    <form action="./submit_page" method="get">
      <h2>Subscribe to our newsletter</h2>
      <p>
        <label for="name">Name (required):</label>
        <input type="text" name="name" id="name" required />
      </p>
      <p>
        <label for="email">Email (required):</label>
        <input type="email" name="email" id="email" required />
      </p>
      <p>
        <button>Sign me up!</button>
      </p>
    </form>
  </body>
</html>

화면에는 이렇게 렌더링됩니다:

아무것도 입력하지 않고 바로 "Sign me up!"을 클릭하면, 데이터가 없기 때문에 브라우저 자체 유효성 검사 에러가 나타날 거예요. 이름과 이메일을 제대로 채우고 버튼을 클릭하면 404 에러 메시지를 보게 됩니다.

왜 그런지는 뒤에서 설명해 드릴게요. 넘어가기 전에, 코드 에디터를 열고 새 HTML 파일을 하나 만들어서 위의 HTML 코드를 복사해 넣은 다음 브라우저 탭에서 한 번 열어보세요!

💡 강사의 팁! > 디자인 툴인 Figma로 화면 구조를 잡을 때도, 시각적으로 '이름'이라는 텍스트와 입력 칸이 세트라는 걸 염두에 두시죠? HTML로 코딩할 때도 마찬가지입니다. <label><input>을 항상 짝꿍으로 묶어주는 습관을 들이세요!

<form> 요소

앞서 말씀드렸듯, <form> 요소는 폼을 구성하는 바깥쪽 래퍼(Wrapper) 역할을 해서 그 안의 모든 폼 컨트롤들을 하나로 묶어줍니다. <button>이 눌리면 폼 컨트롤에 담긴 모든 데이터가 서버로 전송되는 거죠. <form> 요소에는 여러 가지 속성을 지정할 수 있는데, 우리 예제에도 포함된 가장 중요한 속성 두 가지는 다음과 같습니다.

  • action: 제출된 폼 데이터를 받아서 처리해 줄 서버의 페이지 경로를 담고 있어요. 나중에 폼을 제출해 보시면 URL 끝에 /submit_page가 붙는 걸 보실 수 있을 거예요. 그리고 페이지가 실제로 존재하지 않아서 404 에러가 뜨겠지만, 지금은 그게 정상입니다.
  • method: 폼 데이터를 서버로 보낼 때 사용할 데이터 전송 메서드(method)를 지정해요. 지금은 너무 깊게 생각하지 마세요. get 값은 데이터를 URL 끝에 파라미터 형태로 붙여서 보내는 방식이랍니다.

제출된 데이터 확인해 보기

  1. 방금 따로 띄워둔 브라우저 탭에서 Name에 "Bob", Email에 "bob@bob.com"이라고 입력해 보세요.
  2. <button>을 누르세요.

actionmethod 속성 덕분에 폼 데이터가 제출되면 브라우저 URL이 대략 다음과 같은 모양으로 바뀌게 됩니다.

/some/url/submit_page?name=Bob&email=bob%40bob.com

폼 구조화하기

<form> 요소 안에는 어떤 HTML 요소든 넣어서 폼의 구조를 잡고, CSS로 꾸미기 위한 컨테이너를 만들 수 있어요.

우리 예제에서는 폼의 목적을 설명하기 위해 제목 요소<h2>를 넣었죠.

그리고 input/label 쌍과 제출 버튼을 각각 개별적인 <p> 단락 요소 안에 넣어서, 각각이 화면의 한 줄씩 차지하도록 만들었습니다. 이 요소들은 기본적으로 인라인(inline) 요소이기 때문에 이렇게 묶어주지 않으면 다닥다닥 한 줄로 붙어버릴 거예요.

이건 폼을 구조화할 때 정말 흔하게 쓰는 패턴입니다. 어떤 사람들은 <p> 요소로 폼 요소들을 나누고, 어떤 사람들은 <div>, <section>, 심지어 <li> 요소를 쓰기도 해요. 어떤 요소를 쓰든 크게 상관은 없지만, 시맨틱(의미론적)으로 타당한 요소를 써야 합니다. 폼 컨트롤 그룹을 별도의 단락이나 섹션, 리스트 항목으로 나누는 건 아주 자연스럽죠. 하지만 이걸 인용구를 뜻하는 blockquote나 부가 정보를 뜻하는 aside, 혹은 주소를 뜻하는 address 요소로 표현한다면 굉장히 어색하고 말이 안 되겠죠?

그리고 폼 요소들을 그룹으로 묶어주는 아주 특화된 요소인 <fieldset>도 있습니다. 복잡한 폼을 만들 때나, 여러 개의 체크박스 및 라디오 버튼을 하나로 묶을 때 아주 유용해요. 뒤에서 <fieldset> 예제도 몇 개 살펴볼게요.

<input> 요소

<input> 요소들은 폼에 입력되는 다양한 종류의 데이터를 나타냅니다. 아까 작성했던 기본 폼 예제의 일부분을 좀 더 자세히 뜯어볼까요?

<input type="text" name="name" id="name" required />

사용된 속성들은 다음과 같습니다:

  • type: 생성할 폼 컨트롤의 종류를 지정합니다. 단순한 텍스트 필드부터 라디오 버튼, 체크박스 등 정말 다양한 종류가 있어요. type="text"는 어떤 값이든 입력할 수 있는 기본적인 텍스트 필드를 만들어줍니다.
  • name: 해당 데이터 항목의 이름을 지정해요. 폼이 제출될 때 데이터는 "이름=값(name=value)"의 쌍으로 전송되거든요. 여기서 이름(name)은 바로 이 name 속성에 적힌 값이 되고, 값(value)은 사용자가 텍스트 필드에 직접 입력한 텍스트가 됩니다.
  • id: 요소를 고유하게 식별할 수 있는 ID를 지정합니다. 여기서는 폼 컨트롤을 <label>과 연결해 주는 끈 역할을 하고 있어요.
  • required: 폼을 제출하기 전에 반드시 이 필드에 값을 입력해야 함을 명시합니다. 선택사항(optional) 필드에는 쓰면 안 되고, 꼭 입력받아야 하는 필수 필드에만 설정해 주시면 됩니다.

참고로 알아두실 점은, 텍스트를 직접 쳐서 값을 얻지 않는 input 타입들도 있다는 거예요. 예를 들어, <input type="color">를 쓰면 색상을 고를 수 있는 위젯이 화면에 나타나고, <input type="radio">는 선택하거나 해제할 수 있는 라디오 버튼 컨트롤을 만들어줍니다.

라디오 버튼 같은 경우에는, 선택되었을 때 서버로 전송될 값을 특정 value 속성 안에 직접 적어주어야 해요. 물론 textcolor 타입에도 value 속성을 미리 지정할 수 있는데, 이렇게 하면 폼이 화면에 처음 나타날 때 필드 안에 그 값이 미리 채워진(pre-filled) 상태로 렌더링 된답니다.

requiredvalue 속성 직접 써보기

  1. 아까 따로 띄워둔 브라우저 탭으로 가서, 두 필드 모두 아무것도 입력하지 않은 채로 폼을 제출해 보세요. "Name" 필드 옆에 "이 필드를 작성해 주세요" 비슷한 에러 메시지가 뜰 거예요(브라우저마다 메시지는 조금씩 다릅니다). 이게 바로 required 속성, 즉 브라우저가 기본적으로 제공하는 클라이언트 사이드 폼 유효성 검사가 작동하는 모습이에요.
  2. 이번에는 첫 번째 필드에 올바른 이름을 적고, 두 번째 필드에는 이메일 주소 형식이 아닌 엉뚱한 값(예: "aaaa")을 넣고 제출해 보세요. 이번에는 "Email" 필드 옆에 "이메일 주소를 입력해 주세요"라는 메시지가 뜰 겁니다.
  3. 첫 번째 input 코드에 value="Bob"을 추가해 보세요. 코드를 새로고침 하면 첫 번째 필드에 처음부터 "Bob"이라는 값이 기본으로 채워져 있는 걸 확인할 수 있어요.

특수 목적의 텍스트 필드 입력들

방금 해보신 두 번째 실습이 아주 흥미로운 포인트를 짚어줍니다. 두 번째 입력 필드는 명확하게 '이메일 주소' 형식을 기대하고 있고, 사용자가 입력한 값이 그 형식에 맞는지 검증하고 있어요. 폼 코드를 다시 보시면 그 이유를 알 수 있죠. 두 번째 <input>type이 바로 email이기 때문입니다. 이렇게 특정 형태의 데이터를 처리하기 위해 디자인된 특수 텍스트 필드 타입들이 여러 개 준비되어 있답니다. 대표적으로 <input type="number">, <input type="password">, <input type="tel"> 등이 있죠.

위에 걸어둔 링크들을 따라가서 이 타입들이 언제 쓰이는지 한 번 알아보세요. MDN의 <input> 레퍼런스 문서를 쭉 둘러보시면서 또 다른 특수 텍스트 필드 타입이 있는지 찾아보는 것도 좋은 공부가 될 거예요.

<label> 요소

아까 말씀드린 대로, <label> 요소는 폼 컨트롤과 짝을 이루어서 거기에 어떤 데이터가 들어가야 하는지 설명해 주는 식별용 꼬리표 역할을 합니다. <label> 요소 안에는 어떤 텍스트를 넣어도 상관없지만, 연결된 폼 컨트롤이 기대하는 데이터가 무엇인지 정확하게 설명해 줘야 해요. 컨트롤에 id 속성을 주고, <label> 요소에는 그 id와 똑같은 값을 가지는 for 속성을 주면 서로 연결이 됩니다.

예를 들어볼게요:

<label for="name">Name (required):</label>
<input type="text" name="name" id="name" required />

<label> 요소가 중요한 이유는 여러 가지가 있지만, 가장 대표적인 두 가지를 꼽자면 다음과 같습니다.

  • 접근성(Accessibility): 시각 장애가 있는 사용자가 스크린 리더(화면 낭독기)를 이용해 웹 페이지를 탐색할 때, 스크린 리더가 각 컨트롤을 만날 때마다 연결된 레이블 텍스트를 읽어줍니다. 덕분에 사용자가 각 컨트롤에 무엇을 입력해야 하는지 쉽게 이해할 수 있어요.
  • 사용성(Usability): 텍스트 입력 칸이나 체크박스 자체를 누르지 않고, 그 옆의 레이블 텍스트를 클릭해도 해당 폼 컨트롤이 선택(Focus)되게 만들어 줍니다. 특히 모바일 환경에서 터치스크린으로 작은 폼 요소를 콕 집어 누르기 힘들 때, 레이블 텍스트까지 터치 영역(Hit area)을 넓혀주는 효과가 있어서 정말 유용합니다.

명시적(Explicit) 레이블과 암묵적(Implicit) 레이블

방금 보신 레이블 작성 방식이 바로 명시적 폼 레이블입니다. idfor 속성을 통해서 컨트롤과 레이블의 관계를 명시적으로 이어준 거죠. 또 다른 방법으로, 컨트롤 요소를 레이블 요소 안에 아예 중첩시켜버리는 암묵적 폼 레이블 방식도 있습니다. 이렇게요:

<label>
  Name (required):
  <input type="text" name="name" required />
</label>

요소를 감싸서 중첩시키면 브라우저가 알아서 컨트롤과 레이블을 암묵적으로 연결해 줍니다. 이렇게 하면 idfor 속성을 쓰지 않아도 되죠.

두 가지 방식 모두 괜찮지만, 저는 명시적 방식(for와 id 사용)을 쓰시기를 적극 권장합니다. HTML 코드가 길고 복잡해질수록 명시적으로 연결해 둔 것이 훨씬 더 파악하기 쉽고 이해하기 좋거든요. 게다가 일부 스크린 리더나 보조 기기들 중에는 암묵적 레이블을 완벽하게 처리하지 못하는 경우도 있습니다.

폼 레이블의 모범 사례에 대해 더 자세히 알고 싶으시다면, CSS-Tricks의 HTML Inputs and Labels: A Love Story (2021) 글을 읽어보시면 좋습니다.

<button> 요소

<form> 요소 안에 <button> 요소가 들어가 있으면, 버튼의 기본 동작은 폼 제출(Submit)이 됩니다. 물론 클라이언트 사이드 유효성 검사에 걸리는 잘못된 데이터가 없어야 제출이 완료되겠죠. 이 동작은 앞서 기본 폼 예제를 만져보시면서 이미 경험하셨을 거예요.

<button> 요소의 type 속성을 이용하면 다른 동작 방식을 지정해 줄 수도 있습니다.

  • <button type="submit">은 폼을 제출하는 버튼으로 동작하라고 명시적으로 선언하는 거예요. 폼 안에 버튼이 여러 개라서 어느 것이 진짜 제출 버튼인지 확실하게 구분해야 하는 상황이 아니라면, 굳이 명시적으로 적어줄 필요는 없습니다. 보통 이런 상황은 아주 드물어요.
  • <button type="type="reset">초기화 버튼을 만듭니다. 이 버튼을 누르면 폼 안의 모든 데이터가 즉시 지워지고 처음 상태로 리셋되죠. 하지만 가급적 리셋 버튼은 쓰지 마세요. 초창기 웹에서는 자주 쓰였지만, 사용자에게 도움을 주기보다는 짜증을 유발하는 경우가 훨씬 많습니다. 기껏 긴 폼을 정성껏 다 채웠는데, 실수로 제출 버튼 옆의 리셋 버튼을 누르는 바람에 처음부터 다시 써야 했던 악몽 같은 경험, 다들 한 번쯤은 있으실 거예요.
  • <button type="button"><form> 요소 바깥에 있는 일반 버튼과 똑같이 동작하게 만듭니다. 앞에서 봤듯이 기본적으로는 아무것도 하지 않기 때문에, 클릭했을 때 무언가 동작하게 만들려면 자바스크립트가 필요합니다.

물론 <input type="submit">, <input type="reset">, <input type="button">처럼 <input> 요소에 똑같은 type 값을 줘서 만들 수도 있어요. 하지만 <button> 태그를 쓰는 것에 비해 단점이 너무 많습니다. 그러니 그냥 맘 편하게 <button>을 사용하세요!

참고: MDN의 학습 파트너인 Scrimba에 The very basics of forms라는 무료 강의가 있어요. 지금까지 우리가 다룬 폼의 기초 내용들을 상호작용 방식으로 유용하게 복습할 수 있습니다.


접근성에 대한 여담

폼 레이블이 접근성에 얼마나 중요한지는 이미 충분히 강조했지만, 올바른 시맨틱 요소를 사용해서 폼을 만드는 것이 전반적으로 얼마나 중요한지 한 번 더 이야기하고 넘어갈게요. (예를 들어, 제출 버튼을 만들 때 진짜 <button> 태그를 써야지, <div> 태그를 억지로 꾸미고 스크립트를 짜서 버튼처럼 동작하게 만들면 안 된다는 말입니다.)

현업에서는 CSS와 자바스크립트만 잘 섞으면 사실상 어떤 HTML 요소든 폼 요소처럼 보이고 동작하게 만들 수 있어요. 일부 폼 컨트롤은 기본 디자인을 예쁘게 꾸미기(스타일링) 어렵다는 이유로, 개발자들이 종종 이런 식별 우회(꼼수)를 선택하곤 하죠.

하지만 이렇게 개발하면 결국 개발자 본인뿐만 아니라 웹사이트를 방문하는 사용자들을 모두 힘들게 만드는 길입니다. 브라우저는 자바스크립트나 추가 코드 없이도 기본적으로 제공하는 <button>과 폼 컨트롤 기능들을 가지고 있고, 이것들은 모든 사용자가 폼을 더 편리하게 사용할 수 있도록 최적화되어 있거든요.

예를 들면 이런 것들이죠:

  • 스크린 리더 같은 보조 기술은 시맨틱 요소를 정확하게 이해하고, 눈이 보이지 않는 사용자에게 그 의미를 제대로 전달해 줍니다.
  • 기본 폼 컨트롤과 버튼은 키보드만으로도 완벽하게 접근할 수 있습니다. 위 예제로 돌아가서 마우스 대신 키보드의 Tab 키와 Shift + Tab 키를 눌러보세요. 폼 요소들 사이를 이리저리 오갈 수 있죠? 이걸 "태빙(tabbing)"이라고 부릅니다.
  • 태빙으로 폼 요소 사이를 이동할 때마다, 현재 초점이 맞춰진(포커스된) 요소 주변에 파란색 윤곽선이 생기는 것도 눈여겨보세요. 이걸 포커스 아웃라인(focus outline)이라고 부르는데, 키보드로 웹을 탐색하는 사용자가 지금 자기가 화면 어디에 있는지 파악하기 위한 절대적으로 중요한 기능입니다.

만약 올바른 시맨틱 요소를 쓰지 않고 폼을 구현했다면? 이 모든 편의 기능과 접근성 기능들을 여러분이 자바스크립트 코드로 일일이 다시 구현해야 합니다. 안 그러면 사용자들이 예상하는 대로 폼이 동작하지 않아서 '망가진 사이트'라고 생각할 거예요. 이 모든 게 결국 개발 부채로 돌아옵니다.


다른 종류의 컨트롤들

폼 데이터를 수집하기 위해 쓸 수 있는 컨트롤의 종류는 꽤 다양합니다. 조금 더 복잡한 예제를 보면서 하나씩 풀어서 설명해 드릴게요.

참고: 이 예제에서는 사용자가 이미 회원가입을 하고 로그인한 상태라고 가정했습니다. 그래서 이름이나 이메일 같은 개인정보는 따로 받지 않아요.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Second form</title>
  </head>
  <body>
    <form action="./payment_page" method="get">
      <h2>Register for the meetup</h2>
      <fieldset>
        <legend>Choose hotel room type:</legend>
        <div>
          <input
            type="radio"
            id="hotelChoice1"
            name="hotel"
            value="economy"
            checked />
          <label for="hotelChoice1">Economy (+$0)</label>

          <input type="radio" id="hotelChoice2" name="hotel" value="superior" />
          <label for="hotelChoice2">Superior (+$50)</label>

          <input
            type="radio"
            id="hotelChoice3"
            name="hotel"
            value="penthouse"
            disabled />
          <label for="hotelChoice3">Penthouse (+$150)</label>
        </div>
      </fieldset>
      <fieldset>
        <legend>Choose classes to attend:</legend>
        <div>
          <input type="checkbox" id="yoga" name="yoga" />
          <label for="yoga">Yoga (+$10)</label>

          <input type="checkbox" id="coffee" name="coffee" />
          <label for="coffee">Coffee roasting (+$20)</label>

          <input type="checkbox" id="balloon" name="balloon" />
          <label for="balloon">Balloon animal art (+$5)</label>
        </div>
      </fieldset>
      <p>
        <label for="transport">How are you getting here:</label>
        <select name="transport" id="transport">
          <option value="">--Please choose an option--</option>
          <option value="plane">Plane</option>
          <option value="bike">Bike</option>
          <option value="walk">Walk</option>
          <option value="bus">Bus</option>
          <option value="train">Train</option>
          <option value="jetPack">Jet pack</option>
        </select>
      </p>
      <p>
        <label for="comments">Any other comments:</label>
        <textarea id="comments" name="comments" rows="5" cols="33"></textarea>
      </p>
      <p>
        <button>Continue to payment</button>
      </p>
    </form>
  </body>
</html>

화면에는 이렇게 렌더링됩니다:

다음 몇 개 섹션을 읽으실 때는 아까처럼 이 코드를 로컬 환경의 새 HTML 파일로 복사해서 브라우저 탭에 띄워놓고 보시기를 권장해요.

다음으로 넘어가기 전에 로컬에 띄운 폼 컨트롤들을 직접 이리저리 눌러보고 값도 선택해 보세요. 그리고 폼을 제출(Submit)해 보면서 브라우저 주소창(URL)에 데이터가 어떤 형태로 묻어서 넘어가는지 확인해 보는 것도 잊지 마세요!

라디오 버튼 (Radio buttons)

"Choose hotel room type" 항목을 선택하는 버튼들은 <input type="radio"> 컨트롤을 사용해 구현했어요. 이것들은 한 번에 딱 하나만 선택할 수 있는 버튼 세트를 만들어 줍니다. 여러 개를 동시에 선택하는 건 불가능해요. 옛날 아날로그 라디오에서 버튼 하나를 누르면 이전에 눌려있던 다른 버튼이 튀어나오던 방식과 똑같아서 '라디오 버튼'이라는 이름이 붙었답니다.

예제 코드는 다음과 같아요:

<fieldset>
  <legend>Choose hotel room type:</legend>
  <div>
    <input
      type="radio"
      id="hotelChoice1"
      name="hotel"
      value="economy"
      checked />
    <label for="hotelChoice1">Economy (+$0)</label>

    <input type="radio" id="hotelChoice2" name="hotel" value="superior" />
    <label for="hotelChoice2">Superior (+$50)</label>

    <input
      type="radio"
      id="hotelChoice3"
      name="hotel"
      value="penthouse"
      disabled />
    <label for="hotelChoice3">Penthouse (+$150)</label>
  </div>
</fieldset>

radio 입력 타입은 앞서 배운 text 타입과 대부분 비슷하게 동작하지만 약간의 차이점이 있습니다.

  • 라디오 버튼 묶음을 하나로 연결(그룹화)해주려면, 같은 세트에 속하는 라디오 버튼들의 name 속성 값이 모두 똑같아야 합니다. 만약 값이 다르면 별개의 그룹으로 인식되어서 나중에 서로 다른 값으로 제출되어 버려요.
  • 라디오 버튼마다 서버로 제출할 값을 담은 value 속성을 반드시 적어주어야 합니다. 폼이 제출될 때 이름/값 쌍이 넘어가는데, 이름(name)은 항상 같으니까 결국 hotel=economy 거나 hotel=superior 같은 식으로 제출되는 거죠.
  • 각 라디오 버튼의 <label>은 전체 선택지가 무엇인지 설명하는 게 아니라, 바로 그 특정 옵션이 무엇을 의미하는지 설명해야 해요. "호텔 방의 종류를 골라주세요" 같은 전체적인 설명은 <fieldset>으로 그룹을 감싼 뒤에 자식 요소인 <legend>를 이용해서 달아주는 것이 올바른 방법입니다.

참고: fieldset은 폼을 구조화하고 레이블을 다는 역할 외에도, 특정 컨트롤 묶음 전체를 한 번에 비활성화(disabled) 시키는 데도 아주 유용하게 쓰입니다.

또 한 가지 눈치채셨나요? 첫 번째 라디오 버튼에 checked 속성을 주었어요. 덕분에 페이지가 처음 로딩될 때 이 버튼이 미리 선택된 상태로 나타납니다. 즉, 사용자가 아무것도 만지지 않아도 항상 하나의 옵션은 선택되어 있는 상태가 유지되고, 다른 옵션을 선택하기 전에는 현재 선택을 임의로 해제할 수 없게 됩니다.

로컬 환경에 띄운 코드에서 첫 번째 라디오 버튼의 checked 속성을 지우고 새로고침해서 어떻게 달라지는지 한 번 확인해 보세요. 다음으로 넘어가기 전에는 원래대로 다시 돌려놓으시고요!

폼 컨트롤 비활성화하기

라디오 버튼 예제를 다시 보시면 세 번째 라디오 버튼에 disabled 속성이 세팅된 걸 볼 수 있어요. 이렇게 하면 해당 컨트롤이 회색으로 흐려지면서 클릭조차 할 수 없게 렌더링 됩니다. 평소에는 선택 가능하지만 지금 당장은 선택할 수 없는 옵션을 보여줄 때 아주 유용하죠. 쇼핑몰에서 품절된 상품을 표시할 때나, 우리 예제처럼 펜트하우스 스위트룸 예약이 꽉 찼을 때 쓰기 딱 좋아요.

disabled 속성은 <button>을 포함해서 어떤 폼 컨트롤에든 사용할 수 있습니다. <fieldset> 요소에 달아주는 것도 가능한데요, 이렇게 하면 그 그룹 안에 묶여 있는 모든 폼 컨트롤이 통째로 비활성화됩니다.

로컬 코드에서 두 개의 <fieldset> 요소에 직접 disabled 속성을 넣어보고 저장한 뒤 새로고침해서 어떻게 바뀌는지 확인해 보세요. 확인이 끝났으면 지우고 넘어가시면 됩니다.

체크박스 (Checkboxes)

"참석할 클래스"를 고르는 부분은 <input type="checkbox"> 컨트롤을 사용해서 만들었어요. 이 요소는 켜고 끄는(On/Off) 상태를 나타내는 체크박스들을 렌더링 해 줍니다. 한 번에 하나만 고를 수 있던 라디오 버튼과 달리, 원하는 만큼 다중 선택이 가능해요.

<fieldset>
  <legend>Choose classes to attend:</legend>
  <div>
    <input type="checkbox" id="yoga" name="yoga" />
    <label for="yoga">Yoga (+$10)</label>

    <input type="checkbox" id="coffee" name="coffee" />
    <label for="coffee">Coffee roasting (+$20)</label>

    <input type="checkbox" id="balloon" name="balloon" />
    <label for="balloon">Balloon animal art (+$5)</label>
  </div>
</fieldset>

코드 조각을 보시면 아시겠지만, 체크박스와 라디오 버튼은 코드를 짜는 방식이 매우 비슷합니다. (체크박스도 페이지 로드 시 처음부터 체크되어 있게 하려면 checked 속성을 쓰면 됩니다.) 동작 방식도 꽤 비슷하죠. 라디오 버튼은 여러 개 중에 아예 안 고르거나 1개만 고를 수 있다면, 체크박스는 여러 개 중에 안 고르거나 여러 개를 왕창 고를 수 있다는 점이 다를 뿐이에요.

가장 결정적인 차이는 (type 값이 다르다는 거 빼고요!) 각각의 체크박스가 모두 서로 다른 name 값을 가진다는 점, 그리고 일반적으로 value 속성을 따로 주지 않는다는 점입니다.

기능적으로 보면, 체크박스는 하나하나가 각기 다른 데이터 값을 뜻해요. 라디오 버튼 한 세트가 통째로 하나의 데이터를 나타냈던 것과는 다르죠. 폼을 제출할 때 체크박스에 체크가 되어있다면, 각 이름의 값은 on으로 세팅되어 서버로 넘어갑니다. 예를 들어 yoga=on, balloon=on 처럼요.

참고: 체크박스에도 value 속성을 줘서 전송되는 값을 직접 바꿀 수는 있습니다. 예를 들어 <input type="checkbox" id="yoga" name="yoga" value="yes" /> 라고 적으면, 체크했을 때 yoga=yes 로 제출되겠죠. 하지만 굳이 이렇게 할 필요는 거의 없어요. 어차피 체크박스는 체크하면 값이 하나 전송되고, 체크를 풀면 아예 서버로 값이 넘어가지 않거든요. 전송되는 텍스트 자체가 무엇인지는 그리 중요하지 않아요.

드롭다운 메뉴 (Drop-down menus)

우리 예제에서 "이동 수단"을 고르는 부분 같은 드롭다운 메뉴는 <input> 타입으로 만들지 않아요. 대신 <select> 요소와 <option> 요소를 조합해서 만듭니다.

<label for="transport">How are you getting here:</label>
<select name="transport" id="transport">
  <option value="">--Please choose an option--</option>
  <option value="plane">Plane</option>
  <option value="bike">Bike</option>
  <option value="walk">Walk</option>
  <option value="bus">Bus</option>
  <option value="train">Train</option>
  <option value="jetPack">Jet pack</option>
</select>

<select> 요소는 사용자가 고를 수 있는 모든 선택지를 크게 한 바퀴 감싸주는 껍데기예요. 레이블과 묶어줄 id 속성, 그리고 제출될 데이터의 이름을 결정하는 name 속성도 바로 여기에 적어줍니다.

그리고 드롭다운 목록 안에서 고를 수 있는 각각의 선택지 항목들은 <select> 요소 안에 자식으로 들어가는 <option> 요소로 만들어줍니다. 각각의 <option> 요소에 value 속성을 주면, 사용자가 그 항목을 골랐을 때 그 값이 서버로 제출됩니다. 만약 value 속성을 깜빡하고 적지 않았다면? 브라우저가 똑똑하게도 <option></option> 태그 사이에 있는 텍스트를 그대로 값으로 써준답니다.

참고: 페이지가 뜰 때 특정 옵션이 처음부터 선택되어 있게 만들고 싶다면, 원하는 <option> 요소에 selected 속성을 추가해 주면 돼요. (체크박스의 checked랑 같은 역할입니다!)

여러 줄 텍스트 입력 필드 (Multi-line text input fields)

짧은 문장이 아니라 여러 줄로 된 긴 글을 입력받고 싶을 때는 <textarea> 요소를 사용합니다.

<label for="comments">Any other comments:</label>
<textarea id="comments" name="comments" rows="5" cols="33"></textarea>

기본적인 동작은 <input type="text">와 똑같은데, 글을 줄바꿈 해서 여러 줄 넣을 수 있다는 점이 다르죠. rows 속성을 쓰면 텍스트 에어리어가 화면에 몇 줄 높이로 나타날지 지정할 수 있고, cols 속성을 쓰면 몇 글자 폭으로 렌더링 될지 설정할 수 있습니다. 따로 지정해주지 않으면 브라우저가 기본값으로 cols="20", rows="2" 정도의 크기로 만들어 줄 거예요.

대부분의 브라우저는 텍스트 에어리어 우측 하단 구석에 마우스로 드래그해서 크기를 늘리거나 줄일 수 있는 '드래그 핸들'을 함께 표시해 줍니다. 로컬에 띄운 데모에서 직접 마우스로 잡고 크기를 바꿔보세요.


폼 유효성 검사 (Form validation)

앞에서 브라우저가 자체적으로 제공하는 기본적인 클라이언트 사이드 폼 유효성 검사를 잠깐 맛보았죠? 폼을 제출하기 전에 텅 비어있는 필드는 없는지 검사하는 required 속성이나, 이메일 주소, URL, 숫자 등 특정 타입에 맞게 데이터가 쓰였는지 체크해 주는 기능들 말이에요.

이렇게 유효성 검사를 하는 이유는 크게 두 가지입니다.

  • 사용자가 입력한 데이터 형식이 우리가 만든 애플리케이션에 에러를 일으키지 않도록 사전에 방지하기 위해서예요.
  • 잘못된 데이터가 보안 문제를 일으키는 것을 막기 위해서예요. 악의적인 해커들은 시스템의 약점을 뚫고 데이터베이스를 날려버리거나 시스템 권한을 뺏기 위해 아주 교묘하게 조작된 데이터를 폼에 실어서 전송하거든요.

폼 유효성 검사라는 주제는 정말 거대해서 이 문서 하나에서 다루기에는 벅찹니다. 그래서 여기서는 깊게 들어가지 않을게요. 하지만 이거 하나만은 꼭 기억해 두세요! 폼 유효성 검사는 두 가지 종류가 있다는 것을요.

  • 클라이언트 사이드 (Client-side) 유효성 검사: 사용자의 브라우저 단에서 이뤄지는 검사입니다. 브라우저의 내장 검사 속성(예: required)과 자바스크립트를 조합해서 구현하죠. 사용자가 무언가를 잘못 입력했을 때 즉각적으로 피드백(힌트)을 주기에는 최고입니다. 하지만 악의적으로 조작된 데이터를 100% 막아내는 방패로는 쓰일 수 없어요. 해커가 브라우저의 자바스크립트 기능을 꺼버리거나 클라이언트 코드를 위조해서 검사 로직을 무력화시키기 너무 쉽거든요.
  • 서버 사이드 (Server-side) 유효성 검사: 서버 단에서 이뤄지는 검사로, 서버를 구성하는 백엔드 언어로 구현됩니다. 잘못된 형태의 메시지는 실수로 넘어올 수도 있지만, 누군가 고의로 보낼 수도 있죠. 소프트웨어 업계의 철칙은 '클라이언트가 보내는 그 어떤 데이터도 서버는 절대 맹신해서는 안 된다'입니다. 조작된 메시지로 인한 버그나 보안 이슈를 막으려면 철저히 의심해야 하니까요. 이 방식은 서버에 올라가 있는 코드를 해커가 쉽게 조작할 수 없기 때문에 악의적인 공격을 막는 데 매우 훌륭합니다. 하지만 폼 데이터를 일단 서버까지 보내서 검증한 뒤 그 결과를 다시 클라이언트로 돌려보내야 하기 때문에, 사용자에게 "틀렸어요!"라는 피드백을 실시간으로 주기에는 클라이언트 사이드 검사보다 조금 답답하고 느릴 수밖에 없죠.

요약하자면, 둘 중 뭘 써야 할지 고민할 필요가 없습니다. 무조건 둘 다 쓰셔야 해요. 사용자의 편의성을 위해 친절한 피드백을 주는 '클라이언트 사이드 검사'도 필요하고, 서버를 튼튼하게 지키기 위한 '서버 사이드 검사'도 반드시 필요합니다. 유효성 검사에 대해 각 잡고 파고들어 보고 싶으시다면, 클라이언트 사이드 폼 유효성 검사 (Client-side form validation) 문서를 읽어보시는 걸 추천해 드려요!

profile
프론트에_가까운_풀스택_개발자

0개의 댓글