Skip to content
This page has been auto-translated and may contain errors.View in English

폼과 입력

docs.scrimba.com

무언가를 처리해주는 웹사이트라면, 검색창이든 로그인이든 결제든 댓글 입력창이든, 거의 모두 폼을 기반으로 만들어집니다. 폼은 페이지가 방문자에게 말만 하는 것을 멈추고 방문자의 말을 듣기 시작하는 방식입니다. 즉 누군가가 입력하거나, 체크하거나, 선택한 것을 모아서 처리할 수 있도록 넘겨주는 역할을 합니다. 이 장에서는 이것을 가능하게 하는 HTML을 다룹니다.

form 엘리먼트

**폼(form)**은 방문자가 채워 넣는 것들을 한데 묶는 컨테이너입니다. <form> 엘리먼트 안에 입력 필드들과 버튼을 감싸 넣으면, 폼은 방문자가 다 채웠을 때 모든 것을 모아 한꺼번에 전송하는 역할을 합니다.

html
<form>
  <label>Your name</label>
  <input>
  <button>Send</button>
</form>

병원에서 작성하는 종이 접수 양식을 떠올려 보세요. 그 종이 한 장에 여러 칸이 있고, 각 칸에 내용을 적은 다음, 맨 아래에서 그 종이 전체를 한 번에 제출합니다. <form> 엘리먼트가 바로 그 종이입니다.

<form>은 자신이 담고 있는 컨트롤들을 모아서 하나의 묶음으로 제출하는 컨테이너입니다. 제출 시 벌어지는 일은 두 개의 속성이 결정합니다. action은 데이터를 보낼 주소이고, method는 데이터를 보내는 방식입니다.

html
<form action="/subscribe" method="post">
  <label for="email">Email address</label>
  <input id="email" name="email" type="email">
  <button type="submit">Subscribe</button>
</form>

방문자가 제출을 실행하면, 브라우저는 폼 안의 이름이 붙은 모든 컨트롤을 수집하고, 그 값들을 패키징해서 action 주소로 요청을 보냅니다. 이 과정을 직접 구현할 필요는 전혀 없습니다. <form> 엘리먼트는 이 동작을 그냥 제공해 주는데, 이것이 바로 낱개의 입력 요소를 페이지에 흩어놓는 것보다 실제 폼으로 컨트롤들을 묶는 방식이 더 나은 이유입니다.

<form>은 네이티브 제출(submission) 메커니즘입니다. 즉 폼의 컨트롤들을 모아 요청으로 만들어 전송하는, 브라우저에 내장된 동작이며 별도의 스크립트가 필요하지 않습니다. 제출이 일어나면 브라우저는 action(요청이 향할 URL)과 method(get 또는 post, 이 장 뒤에서 다룹니다)을 읽어서 컨트롤들을 요청으로 직렬화하고 페이지를 이동시킵니다. 이 기본 동작이 있기 때문에 JavaScript가 꺼져 있거나 아직 로드되지 않은 상태에서도 폼이 동작하는 것이며, 이는 여러분이 대체해야 할 대상이 아니라 그 위에 쌓아 올려야 할 기반입니다.

html
<form action="/subscribe" method="post">
  <label for="email">Email address</label>
  <input id="email" name="email" type="email">
  <button type="submit">Subscribe</button>
</form>

여기서 두 가지 동작을 놓치기 쉽습니다. 첫째, 폼은 단일 라인 텍스트 필드에 포커스가 있을 때 버튼 클릭이 아니어도 Enter 키만으로 제출되므로, 키보드로 제출하는 경로도 기본으로 갖춰져 있고 테스트해 볼 가치가 있습니다. 둘째, 기본 제출은 페이지를 새로고침하거나 다른 곳으로 이동시키는데, JavaScript로 폼을 직접 다룰 때는 event.preventDefault()를 호출해서 이 이동을 막고 처리를 넘겨받습니다. 네이티브 제출을 최저 기준으로 여기세요. 즉 어떤 스크립트도 실행되기 전에도 그 자체로 말이 되게 동작해야 합니다. 점진적 향상(progressive enhancement), 즉 잘 동작하는 HTML 위에 스크립트를 겹겹이 쌓아 올리는 방식이 바로 여기서 시작해야 하는 이유입니다.

Junoform 엘리먼트<form>은 입력 필드들과 버튼을 담는 컨테이너이며, 누군가 제출하면 모든 것을 한꺼번에 보냅니다. 책상 위 종이 접수 양식을 떠올려 보세요. 종이 한 장에 여러 칸이 있고, 한 번에 제출합니다. 필드들을 폼으로 감싸기만 하면 별다른 수고 없이 이런 수집-후-전송 동작을 얻게 됩니다.
Junoform 엘리먼트<form>은 자신의 컨트롤들을 묶어서 제출합니다. action은 어디로 보낼지를, method는 어떻게 보낼지를 말해줍니다. 수집과 전송을 공짜로 얻는 셈이니, 낱개 입력 요소에 직접 클릭 핸들러를 붙이는 대신 진짜 폼을 사용하세요. 코드도 아끼고, 스크립트가 로드되기 전에도 동작합니다.
Junoform 엘리먼트 네이티브 제출은 스크립트 없이 컨트롤들을 모아 action으로 요청을 보내는데, 이것이 바로 JavaScript가 꺼져 있어도 폼이 동작하는 이유입니다. 텍스트 필드에서는 클릭이 아니라 Enter로도 제출된다는 점을 기억하세요. JavaScript로 처리를 넘겨받을 때는 event.preventDefault()가 기본 이동을 막아주며, 그 아래에 있는 평범한 폼은 여전히 안전망 역할을 합니다.

입력과 레이블

<input>은 방문자가 입력하는 상자입니다. 하지만 그 자체만으로는 무엇을 위한 것인지 알 수 없는 빈 상자일 뿐입니다. 바로 이때 **레이블(label)**이 필요합니다. 레이블은 방문자에게 이 상자에 무엇을 넣어야 하는지 알려주는 텍스트입니다.

html
<label for="city">City</label>
<input id="city">

모든 입력에는 레이블이 필요합니다. 두 요소는 입력에 id를 부여하고, 레이블에서 for로 같은 값을 지정해서 서로 연결합니다. 이렇게 연결해 두면 레이블을 클릭했을 때 커서가 상자 안으로 이동하는데, 이런 작은 배려가 폼을 모두에게 더 쓰기 편하게 만들어 줍니다.

<input>은 하나의 값을 받아들이고, <label>은 그 값에 이름을 붙입니다. 레이블의 for 속성을 입력의 id와 일치시켜 서로 연결합니다.

html
<label for="username">Username</label>
<input id="username" name="username" type="text" required>

대부분의 입력에는 몇 가지 속성을 붙일 가치가 있습니다. name은 값이 전송될 때 붙는 키입니다(뒤의 이름 붙이기 절에서 다룹니다). required는 필드가 채워지기 전까지 제출을 막습니다. placeholder는 상자 안에 흐릿한 힌트 텍스트를 보여주지만, placeholder는 레이블이 아닙니다. 누군가 입력을 시작하는 순간 사라지기 때문에 진짜 레이블을 대신할 수 없습니다. 모든 입력에는 여전히 자기만의 <label>이 필요합니다.

<label>을 대상 컨트롤과 연결하는 방법은 두 가지이며, 이 차이는 접근성 측면에서 중요합니다. 첫 번째는 **명시적 연결(explicit association)**입니다. 레이블이 값이 컨트롤의 id와 같은 for 속성을 갖는 방식입니다. 두 번째는 **암묵적 연결(implicit association)**입니다. 컨트롤을 레이블 엘리먼트 안에 감싸 넣는 방식이며, 이때는 forid가 필요 없습니다.

html
<!-- explicit: for matches id -->
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel">

<!-- implicit: the input sits inside the label -->
<label>
  Phone
  <input name="phone" type="tel">
</label>

두 방식 모두 컨트롤에 **접근 가능한 이름(accessible name)**을 부여합니다. 이는 필드가 포커스를 받았을 때 스크린 리더가 읽어주는 텍스트로, 화면 구성을 볼 수 없는 방문자도 무엇을 입력해야 하는지 알 수 있게 해줍니다. 명시적 연결을 우선적으로 사용하세요. 레이블을 입력에서 떨어뜨려 놓는 CSS 레이아웃에서도 살아남고, 스타일링 때문에 둘이 강제로 분리되는 경우에도 문제없이 동작합니다. 연결된 레이블이 없는 필드는 목적을 알 수 없는 빈 입력 상자로 읽히는데, 이는 웹에서 가장 흔한 접근성 실패 중 하나입니다. 이 내용은 접근성 장에서 더 깊게 다룹니다.

Juno입력과 레이블<input>은 사람들이 입력하는 상자이고, <label>은 그 안에 무엇을 넣어야 하는지 알려주는 텍스트입니다. forid를 일치시켜 연결하면 레이블을 클릭하는 것만으로 바로 상자로 이동합니다. 모든 입력 하나하나에 레이블을 달아주세요. 옆에 아무 설명도 없는 외로운 상자는 사람들을 헷갈리게 만들 뿐입니다.
Juno입력과 레이블 레이블의 for를 입력의 id에 맞추면 둘이 연결됩니다. required를 활용해 빈 값으로 제출되는 것을 막되, placeholder가 레이블 대신 쓰이게 두지는 마세요. 누군가 입력을 시작하는 순간 사라져 버리니까요. 모든 필드에 진짜 레이블을, 예외 없이 달아주세요.
Juno입력과 레이블 레이블을 연결하는 방법은 두 가지입니다. forid에 맞추거나, 입력을 레이블 안에 감싸는 것입니다. 두 방식 모두 필드에 접근 가능한 이름, 즉 스크린 리더가 읽어주는 텍스트를 부여하며, CSS가 요소들을 흩어놓는 상황에서는 명시적인 for/id 연결이 더 안정적입니다. 레이블이 없는 필드는 이름 없는 상자로 읽히는데, 이는 세상에서 가장 흔한 접근성 실수 중 하나입니다.

그 밖의 컨트롤

모든 것이 평범한 텍스트 상자로 표현되는 건 아닙니다. 어떤 질문은 체크하거나, 고르거나, 목록에서 선택하는 방식이 더 잘 맞습니다. 체크 상자(checkbox)는 켜짐/꺼짐을 나타내는 스위치이고, 드롭다운은 여러 옵션 중 하나를 고르게 해줍니다.

html
<label>
  <input type="checkbox"> Send me the newsletter
</label>

<label for="size">Size</label>
<select id="size">
  <option>Small</option>
  <option>Medium</option>
  <option>Large</option>
</select>

지금 이걸 외울 필요는 없습니다. 여기서 중요한 것은, 폼이 상황에 맞는 방식으로 질문할 수 있다는 점입니다. 입력받거나, 체크받거나, 목록에서 고르게 하거나.

<input> 엘리먼트는 type 속성에 따라 모습이 달라지며, 알맞은 타입을 쓰면 더 나은 키보드, 내장 유효성 검사, 그리고 네이티브 선택 도구까지 공짜로 얻게 됩니다.

html
<input type="email">     <!-- validates the @ shape, email keyboard on mobile -->
<input type="number">    <!-- numeric keypad, up/down steppers -->
<input type="password">  <!-- masks the characters -->
<input type="date">      <!-- native date picker -->
<input type="checkbox">  <!-- a single on/off toggle -->
<input type="radio">     <!-- pick one from a group -->

<input> 외에도 나머지를 채워주는 세 가지 엘리먼트가 있습니다. <textarea>는 여러 줄에 걸친 긴 글을 위한 텍스트 상자입니다. <option>을 자식으로 가진 <select>는 드롭다운 목록입니다. 그리고 관련된 필드들을 시각적으로나 의미적으로 묶어야 할 때는, 그룹의 이름을 붙인 <legend>와 함께 <fieldset>으로 감싸면 됩니다.

html
<fieldset>
  <legend>Delivery speed</legend>
  <label><input type="radio" name="speed" value="standard"> Standard</label>
  <label><input type="radio" name="speed" value="express"> Express</label>
</fieldset>

<label for="message">Message</label>
<textarea id="message" name="message" rows="4"></textarea>

버튼에도 type이 있습니다. type="submit"은 폼을 제출하며(폼 안의 버튼에서 기본값입니다), type="reset"은 모든 필드를 초깃값으로 되돌리고, type="button"은 그 자체로는 아무 동작도 하지 않으며 JavaScript가 연결해서 쓸 수 있도록 존재합니다.

각 컨트롤 타입에는 실제로 사용하기 전에 알아두어야 할 동작이 있습니다. 라디오 버튼은 같은 name을 공유하는 것으로 그룹화되며, 한 그룹 안에서는 오직 하나의 라디오만 선택할 수 있습니다. 이 그룹화가 바로 라디오 버튼을 서로 배타적으로 만드는 원리이므로, name이 빠지거나 잘못 지정되면 조용히 그룹이 깨져 각각 독립적인 토글이 되어버립니다.

html
<fieldset>
  <legend>Delivery speed</legend>
  <label><input type="radio" name="speed" value="standard" checked> Standard</label>
  <label><input type="radio" name="speed" value="express"> Express</label>
</fieldset>

<fieldset><legend>는 단순히 시각적인 요소가 아닙니다. <legend>는 스크린 리더에 의해 그 집합 안의 모든 컨트롤에 대한 맥락으로 안내되므로, "Delivery speed" fieldset 안의 라디오는 "Delivery speed, Standard"로 읽히게 됩니다. 이는 이해할 수 있는 폼과, 맥락 없이 떠 있는 옵션 목록 사이의 차이를 만들어냅니다. 실제 서비스에서 알아두면 좋은 두 가지가 더 있습니다. 체크박스는 체크되었을 때만 값을 전송하며, 체크되지 않은 상자는 제출 자체에서 완전히 빠지므로 서버는 false를 보는 것이 아니라 아예 아무것도 받지 못합니다. 이 점이 반대편에서 데이터를 읽는 방식을 결정짓습니다. 그리고 type="submit"은 폼 안의 <button>의 기본 타입이므로, JavaScript 트리거로 쓰려던 평범한 <button>type="button"을 지정하지 않으면 폼을 제출하고 페이지를 새로고침해버립니다. 이 기본값에 걸려 넘어지는 사람이 끊이지 않습니다.

Juno그 밖의 컨트롤 폼은 여러 방식으로 물어볼 수 있습니다. 예/아니오에는 체크 상자를, 목록에서 하나를 고를 때는 드롭다운을 쓰는 식입니다. 체크박스는 <input type="checkbox">이고, 드롭다운은 <option> 항목들을 담은 <select>입니다. 외울 필요 없습니다. 질문에 필요하다면 폼은 평범한 상자보다 더 다양한 표현 방법을 갖고 있다는 것만 알면 됩니다.
Juno그 밖의 컨트롤 입력의 type은 모양 자체를 바꿔놓습니다. email, number, date, password, checkbox, radio는 각각 알맞은 키보드나 선택 도구를 데려옵니다. 긴 텍스트에는 <textarea>, 드롭다운에는 <select>, 관련된 필드를 묶을 때는 <legend>가 있는 <fieldset>을 사용하세요. 버튼 타입도 주의하세요. 폼 안에서는 submit이 기본값입니다.
Juno그 밖의 컨트롤 라디오는 같은 name을 공유하는 것으로 그룹화되고, <legend>는 fieldset 안의 모든 컨트롤에 음성으로 전달되는 맥락을 부여합니다. 즉 이건 장식이 아니라 구조입니다. 두 가지 함정이 있습니다. 체크되지 않은 체크박스는 false가 아니라 아예 아무것도 전송하지 않으며, 평범한 <button>은 기본값이 type="submit"이라서 type="button"을 지정하지 않으면 페이지를 새로고침해버립니다.

폼 데이터는 어떻게 이름 붙여지고 전송되는가

폼이 전송될 때, 각 답변에는 그것을 받는 쪽이 어느 상자에서 온 값인지 알 수 있도록 레이블이 필요합니다. 그 레이블이 바로 입력의 name입니다. 한 번 설정해두면 값과 함께 따라다닙니다.

html
<label for="city">City</label>
<input id="city" name="city">

방문자가 그 상자에 "리스본"이라고 입력하면, 폼은 "city는 리스본이다"라는 짝을 전송합니다. name은 받는 쪽이 여러 답변을 서로 구분할 수 있게 해주는 수단입니다. name이 없는 입력은 제출 결과에서 아예 빠져버리므로, 이 작은 속성이 실제로 데이터가 화면에 나타나게 만드는 주인공입니다.

폼 데이터는 **이름/값 쌍(name/value pairs)**의 집합으로 전송됩니다. 각 컨트롤의 name이 키가 되고, 방문자가 입력한 내용이 값이 됩니다. name이 있는 컨트롤만 포함되므로, name이야말로 컨트롤을 실제로 제출되는 데이터로 만들어주는 요소입니다.

html
<form action="/search" method="get">
  <label for="q">Search</label>
  <input id="q" name="q" type="search">
  <label for="sort">Sort by</label>
  <select id="sort" name="sort">
    <option value="recent">Most recent</option>
    <option value="top">Top rated</option>
  </select>
  <button type="submit">Search</button>
</form>

method 속성은 이 쌍들이 이동하는 방식을 결정합니다. method="get"은 그것들을 쿼리 문자열로 URL에 붙입니다(/search?q=boots&sort=recent). 이는 북마크하거나 공유할 수 있는 검색이나 필터에 적합합니다. method="post"는 그것들을 눈에 보이지 않는 요청 본문에 담습니다. 이는 데이터를 변경하는 작업이나, 비밀번호나 결제 정보처럼 URL에 노출되어서는 안 되는 것들에 적합합니다.

모든 제출은 이름/값 쌍의 집합이며, 이 쌍들이 어떻게 인코딩되고 전송되는지는 세 가지 요소가 결정합니다.

첫 번째는 method입니다. get은 쌍들을 쿼리 문자열, 즉 주소 뒤에 붙는 ?key=value&key=value 형태의 텍스트로 직렬화해서 URL에 담습니다. 이 덕분에 요청을 그대로 재현하거나 북마크할 수 있지만, 그만큼 값이 URL과 브라우저 기록, 서버 로그에 그대로 노출된다는 뜻이기도 합니다. 따라서 get은 조회(검색, 필터)를 위한 것이며 절대 비밀 정보에 사용해서는 안 됩니다. post는 그 쌍들을 URL이 아니라 요청 본문에 담는데, 상태를 변경하거나 민감한 작업에는 이 방식을 선택해야 합니다.

두 번째는 enctype입니다. 이는 본문이 어떤 형식으로 구성되는지를 나타내는 인코딩입니다. 기본값인 application/x-www-form-urlencoded는 쌍들을 하나의 key=value&... 문자열로 압축하는데, 텍스트에는 이 방식으로 충분합니다. 선택한 파일을 전송하려면 enctype="multipart/form-data"로 바꿔야 합니다. 이 형식은 파일의 바이트 데이터를 텍스트 필드와 함께 담을 수 있는 형식이며, 이것을 지정하지 않으면 파일 입력이 제대로 업로드되지 않습니다.

html
<form action="/upload" method="post" enctype="multipart/form-data">
  <label for="avatar">Profile picture</label>
  <input id="avatar" name="avatar" type="file">
  <button type="submit">Upload</button>
</form>

세 번째는 autocomplete이며, 여기서 이름을 잘 붙이는 것이 UX와 접근성 측면에서 보답으로 돌아옵니다. autocomplete 토큰은 브라우저에게 이 필드가 표준적인 의미에서 무엇을 뜻하는지 알려주어, 알맞은 저장된 값을 제안할 수 있게 합니다. 예를 들어 autocomplete="email", autocomplete="name", autocomplete="current-password", autocomplete="street-address" 같은 것들이 있습니다. 이 토큰들은 자유롭게 쓸 수 있는 텍스트가 아니라 정해진 어휘이며, 이를 정확히 지정해두면 브라우저나 비밀번호 관리자가 폼을 한 번의 탭으로 채워줄 수 있습니다. 이는 휴대폰으로 입력하는 사람이나 보조 기술을 사용하는 사람에게 가장 중요합니다.

html
<label for="email">Email</label>
<input id="email" name="email" type="email" autocomplete="email">

내장된 검사가 제대로 작동하도록 유지하는 것은 별개의 작업이며, 이는 폼 유효성 검사 장에서 다룹니다. 이 절은 데이터가 유효할 때 그것이 어떤 형태로 어떻게 전송되는지에 관한 내용입니다.

Juno폼 데이터는 어떻게 이름 붙여지고 전송되는가 각 입력의 name은 그 답변에 붙는 태그이므로, 받는 쪽은 어떤 값이 city 상자에서 왔고 어떤 값이 email 상자에서 왔는지 알 수 있습니다. name을 설정하면 상자의 값이 이름이 붙은 짝으로 전송됩니다. 잊어버리면 그 필드는 조용히 빠져버리는데, 이 때문에 폼이 처음으로 아무것도 돌려주지 않을 때 많은 사람들이 헤매게 됩니다.
Juno폼 데이터는 어떻게 이름 붙여지고 전송되는가 제출되는 데이터는 이름/값 쌍이며, name이 있는 컨트롤만 포함됩니다. 목적에 맞게 method를 고르세요. get은 북마크할 수 있는 검색이나 필터를 위해 값을 URL에 담고, post는 데이터를 변경하거나 비밀 정보를 담는 작업을 위해 값을 본문에 숨깁니다. URL에 비밀번호를 넣는 것은 절대 하지 말아야 할 실수입니다.
Juno폼 데이터는 어떻게 이름 붙여지고 전송되는가get은 쌍들을 URL에 써넣으므로 조회용이며 절대 비밀 정보에는 쓰지 마세요. post는 그것들을 본문에 담습니다. enctypemultipart/form-data로 바꾸지 않으면 파일 입력이 업로드되지 않습니다. 그리고 email이나 current-password 같은 autocomplete 토큰에 신경 쓰세요. 이것들은 정해진 어휘이며, 정확히 지정하면 휴대폰이나 비밀번호 관리자가 폼을 한 번의 탭으로 채워줄 수 있습니다.