본문으로 건너뛰기

HTML Form의 Submit

작성일 : 2025.03.12

HTML 폼(Form)은 웹사이트에서 사용자 입력을 수집하는 핵심 도구입니다. 폼의 최종 목적지는 대개 '제출(Submit)'입니다. 이 과정은 겉보기에 단순해 보이지만, 내부적으로는 복잡한 메커니즘이 작동하고 있으며 다양한 방식으로 구현할 수 있습니다. 이 글에서는 HTML 폼 제출의 모든 것을 자세히 알아보겠습니다.

기본 개념: HTML Form과 Submit​

HTML 폼은 <form> 태그로 시작하여 내부에 다양한 입력 요소(input, select, textarea 등)를 포함합니다. 폼 제출(submit)은 사용자가 입력한 데이터를 서버로 전송하는 행위를 말합니다.

가장 기본적인 형태의 HTML 폼은 다음과 같습니다:

<form action="/submit-url" method="post">
  <label for="name">이름:</label>
  <input type="text" id="name" name="name">
  
  <label for="email">이메일:</label>
  <input type="email" id="email" name="email">
  
  <input type="submit" value="제출하기">
</form>

이 폼이 제출되면 사용자가 입력한 이름과 이메일 데이터가 /submit-url 경로로 POST 방식을 통해 전송됩니다.

HTML Form의 기본 속성​

<form> 태그에는 제출 방식을 결정하는 여러 중요한 속성이 있습니다:

1. action 속성​

폼 데이터가 전송될 URL을 지정합니다.

<form action="/process-form">
  <!-- 폼 내용 -->
</form>

만약 action 속성을 생략하면, 현재 페이지의 URL로 폼이 제출됩니다.

2. method 속성​

폼 데이터를 서버로 전송하는 HTTP 메서드를 지정합니다. 주로 사용되는 값은 다음과 같습니다:

  • get: URL의 쿼리 매개변수로 데이터 전송 (기본값)
  • post: HTTP 요청 본문에 데이터 전송
  • dialog: HTML dialog 요소와 함께 사용 (HTML5.2에서 추가)
<form method="post">
  <!-- POST 방식으로 데이터 전송 -->
</form>

3. enctype 속성​

폼 데이터의 인코딩 타입을 지정합니다. 주요 값:

  • application/x-www-form-urlencoded: 기본값. 모든 문자를 인코딩하여 전송
  • multipart/form-data: 파일 업로드 시 필요. 바이너리 데이터 전송 가능
  • text/plain: 공백만 인코딩하고 나머지는 그대로 전송
<form enctype="multipart/form-data">
  <!-- 파일 업로드 시 사용 -->
</form>

4. target 속성​

폼 제출 후 응답을 표시할 위치를 지정합니다:

  • _self: 현재 창에 표시 (기본값)
  • _blank: 새 창이나 탭에 표시
  • _parent: 부모 프레임에 표시
  • _top: 전체 창에 표시
  • 또는 iframe의 name 값
<form target="_blank">
  <!-- 응답이 새 탭에 표시됨 -->
</form>

5. novalidate 속성​

HTML5 자동 유효성 검사를 비활성화합니다.

<form novalidate>
  <!-- 브라우저의 기본 유효성 검사가 수행되지 않음 -->
</form>

6. autocomplete 속성​

브라우저의 자동 완성 기능을 활성화/비활성화합니다.

<form autocomplete="off">
  <!-- 자동 완성 기능 비활성화 -->
</form>

Submit 버튼의 다양한 구현 방법​

폼을 제출하는 방법은 여러 가지가 있습니다:

1. <input type="submit"> 사용​

가장 기본적인 제출 버튼입니다.

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

2. <button type="submit"> 사용​

더 다양한 스타일링과 내부 콘텐츠를 포함할 수 있습니다.

<button type="submit">
  <img src="icon.png" alt=""> 제출하기
</button>

3. <input type="image"> 사용​

이미지를 제출 버튼으로 사용합니다.

<input type="image" src="submit-button.png" alt="제출하기">

4. 자바스크립트로 폼 제출​

특별한 경우에는 자바스크립트를 사용하여 폼을 프로그래매틱하게 제출할 수 있습니다.

// form 요소에 접근하여 submit() 메서드 호출
document.getElementById('myForm').submit();

5. Enter 키 동작​

대부분의 브라우저에서는 폼 내부의 텍스트 입력 필드에 포커스가 있을 때 Enter 키를 누르면 자동으로 폼이 제출됩니다. 단, 폼 내에 type="submit" 버튼이 하나만 있을 경우에 작동합니다.

Form 데이터 전송 방식​

폼 데이터는 method 속성에 따라 다르게 전송됩니다:

GET 방식​

  • URL의 쿼리 매개변수로 데이터가 전송됩니다.
  • 예: http://example.com/submit?name=홍길동&email=hong@example.com
  • 특징:
    • URL에 데이터가 노출됨
    • 데이터 양에 제한이 있음 (~2048자)
    • 브라우저 히스토리에 저장됨
    • 북마크 가능
    • 캐시될 수 있음
    • 보안에 취약함
  • 주로 검색이나 필터링 같은 데이터 조회에 사용

POST 방식​

  • HTTP 요청 본문에 데이터가 포함됩니다.
  • 특징:
    • URL에 데이터가 노출되지 않음
    • 데이터 양에 실질적 제한이 없음
    • 브라우저 히스토리에 저장되지 않음
    • 북마크 불가
    • 캐시되지 않음
    • GET보다 상대적으로 더 안전함
  • 주로 데이터 생성, 수정, 삭제와 같은 작업에 사용

Submit 이벤트와 자바스크립트​

폼이 제출될 때 submit 이벤트가 발생합니다. 이 이벤트를 활용하면 폼 제출 전에 추가 작업을 수행할 수 있습니다:

const form = document.getElementById('myForm');

form.addEventListener('submit', function(event) {
  // 기본 제출 동작 중지
  event.preventDefault();
  
  // 폼 데이터 유효성 검사
  if (!validateForm()) {
    // 유효성 검사 실패 시 처리
    return;
  }
  
  // 추가 작업 수행
  console.log('폼 제출 전 작업 수행');
  
  // 프로그래매틱하게 폼 제출
  this.submit();
});

function validateForm() {
  // 유효성 검사 로직
  return true; // 유효한 경우
}

FormData 객체 활용​

FormData 객체를 사용하면 폼 데이터를 쉽게 수집하고 처리할 수 있습니다:

form.addEventListener('submit', function(event) {
  event.preventDefault();
  
  const formData = new FormData(this);
  
  // FormData의 모든 필드와 값 출력
  for (const [key, value] of formData.entries()) {
    console.log(`${key}: ${value}`);
  }
  
  // 데이터 추가 또는 수정
  formData.append('timestamp', Date.now());
  
  // 서버로 데이터 전송 (Ajax)
  // ...
});

Form 유효성 검사​

폼 제출 전에 입력 데이터가 유효한지 검사하는 것은 중요합니다. 유효성 검사는 클라이언트 측과 서버 측 모두에서 수행되어야 합니다.

1. HTML5 내장 유효성 검사​

HTML5는 다양한 내장 유효성 검사 기능을 제공합니다:

<form>
  <!-- required 속성: 필수 입력 필드 -->
  <input type="text" name="username" required>
  
  <!-- type 속성: 특정 형식 강제 -->
  <input type="email" name="email">
  
  <!-- pattern 속성: 정규식 패턴 지정 -->
  <input type="text" name="zipcode" pattern="[0-9]{5}" title="5자리 우편번호">
  
  <!-- min, max, minlength, maxlength 속성 -->
  <input type="number" name="age" min="18" max="100">
  <input type="password" name="password" minlength="8">
  
  <button type="submit">제출</button>
</form>

2. 자바스크립트 유효성 검사​

더 복잡한 유효성 검사는 자바스크립트로 구현할 수 있습니다:

form.addEventListener('submit', function(event) {
  const password = document.getElementById('password').value;
  const confirm = document.getElementById('confirm').value;
  
  if (password !== confirm) {
    event.preventDefault();
    alert('비밀번호가 일치하지 않습니다.');
  }
  
  // 추가 유효성 검사...
});

3. 제약 조건 검증 API​

HTML5 제약 조건 검증 API를 사용한 유효성 검사:

form.addEventListener('submit', function(event) {
  // 모든 필드의 유효성 검사
  if (!this.checkValidity()) {
    event.preventDefault();
    
    // 사용자 정의 유효성 검사 메시지 표시
    const invalidFields = this.querySelectorAll(':invalid');
    invalidFields.forEach(field => {
      // 필드별 오류 메시지 표시
      showError(field, field.validationMessage);
    });
  }
});

// 특정 필드의 유효성 검사
const emailField = document.getElementById('email');
if (emailField.validity.typeMismatch) {
  showError(emailField, '유효한 이메일 주소를 입력하세요');
}

비동기 폼 제출: Ajax와 Fetch API​

전통적인 폼 제출은 페이지를 새로고침합니다. 비동기 제출을 사용하면 페이지 새로고침 없이 데이터를 서버로 전송할 수 있습니다.

1. Fetch API 사용​

현대적인 웹 애플리케이션에서는 Fetch API를 사용한 비동기 폼 제출이 일반적입니다:

form.addEventListener('submit', async function(event) {
  event.preventDefault();
  
  try {
    const formData = new FormData(this);
    
    const response = await fetch(this.action, {
      method: this.method,
      body: formData,
      headers: {
        'X-Requested-With': 'XMLHttpRequest'
      }
    });
    
    if (!response.ok) {
      throw new Error('서버 응답 오류');
    }
    
    const result = await response.json();
    console.log('성공:', result);
    
    // 성공 메시지 표시 또는 리디렉션
    showSuccess('폼이 성공적으로 제출되었습니다.');
    
  } catch (error) {
    console.error('오류 발생:', error);
    showError(document.getElementById('form-error'), '제출 중 오류가 발생했습니다.');
  }
});

2. JSON 형식으로 데이터 전송​

현대적인 API는 종종 JSON 형식의 데이터를 기대합니다:

form.addEventListener('submit', async function(event) {
  event.preventDefault();
  
  const formData = new FormData(this);
  const formDataObject = Object.fromEntries(formData.entries());
  
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCsrfToken()
      },
      body: JSON.stringify(formDataObject)
    });
    
    const result = await response.json();
    // 응답 처리
  } catch (error) {
    // 오류 처리
  }
});

멀티파트 폼 제출과 파일 업로드​

파일 업로드를 포함하는 폼은 enctype="multipart/form-data"를 사용해야 합니다:

<form action="/upload" method="post" enctype="multipart/form-data">
  <label for="profile">프로필 사진:</label>
  <input type="file" id="profile" name="profile" accept="image/*">
  
  <button type="submit">업로드</button>
</form>

자바스크립트로 파일 업로드 처리​

form.addEventListener('submit', async function(event) {
  event.preventDefault();
  
  const fileInput = document.getElementById('profile');
  if (fileInput.files.length === 0) {
    alert('파일을 선택해주세요.');
    return;
  }
  
  const formData = new FormData(this);
  
  try {
    const response = await fetch('/upload', {
      method: 'POST',
      body: formData
      // multipart/form-data를 사용할 때는 Content-Type 헤더를 설정하지 않음
      // 브라우저가 자동으로 적절한 경계(boundary)를 포함한 헤더를 설정
    });
    
    const result = await response.json();
    if (result.success) {
      // 업로드 성공 처리
      document.getElementById('preview').src = result.fileUrl;
    }
  } catch (error) {
    // 오류 처리
  }
});

다중 파일 업로드​

<input type="file" id="gallery" name="gallery[]" multiple>
const fileInput = document.getElementById('gallery');
const formData = new FormData();

// 여러 파일 추가
for (const file of fileInput.files) {
  formData.append('gallery[]', file);
}

// 또는 더 간단하게
formData.append('gallery[]', fileInput.files[0]);
formData.append('gallery[]', fileInput.files[1]);
// ...

모던 웹 개발에서의 폼 제출 패턴​

1. SPA(Single Page Application)에서의 폼 제출​

리액트, 뷰, 앵귤러와 같은 프레임워크를 사용하는 SPA에서는 폼 제출이 다르게 처리됩니다:

React 예제:

import { useState } from 'react';

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: ''
  });
  
  const handleChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value
    });
  };
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formData)
      });
      
      const data = await response.json();
      // 응답 처리
    } catch (error) {
      // 오류 처리
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        value={formData.name}
        onChange={handleChange}
        required
      />
      {/* 기타 입력 필드 */}
      <button type="submit">제출</button>
    </form>
  );
}

2. 폼 라이브러리 활용​

복잡한 폼 관리를 위해 라이브러리를 사용하는 경우도 많습니다:

React Hook Form 예제:

import { useForm } from 'react-hook-form';

function RegisterForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();
  
  const onSubmit = async (data) => {
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
      });
      
      // 응답 처리
    } catch (error) {
      // 오류 처리
    }
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('username', { required: '사용자명은 필수입니다' })}
      />
      {errors.username && <p>{errors.username.message}</p>}
      
      {/* 기타 입력 필드 */}
      <button type="submit">가입하기</button>
    </form>
  );
}

보안 고려사항​

폼 제출 시 보안은 매우 중요합니다. 주요 보안 위험과 대응 방법:

1. CSRF(Cross-Site Request Forgery) 방지​

CSRF 토큰을 사용하여 폼이 신뢰할 수 있는 소스에서 제출되었는지 확인:

<form method="post" action="/submit">
  <input type="hidden" name="csrf_token" value="123456abcdef">
  <!-- 나머지 폼 필드 -->
</form>

서버에서는 이 토큰이 유효한지 확인합니다.

2. XSS(Cross-Site Scripting) 방지​

사용자 입력을 항상 이스케이프 처리하고, 콘텐츠 보안 정책(CSP)을 구현합니다:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

3. 입력 유효성 검사​

서버 측에서도 반드시 모든 입력 데이터의 유효성을 검사해야 합니다. 클라이언트 측 유효성 검사만으로는 충분하지 않습니다.

접근성을 고려한 폼 제출​

접근성이 좋은 폼은 모든 사용자가 쉽게 사용할 수 있습니다:

1. 레이블 사용​

모든 입력 필드에 관련 레이블을 제공:

<label for="name">이름:</label>
<input type="text" id="name" name="name">

2. 오류 처리 및 피드백​

오류 메시지를 명확하게 표시하고 스크린 리더 사용자를 고려:

<div class="form-group">
  <label for="password">비밀번호:</label>
  <input type="password" id="password" aria-describedby="password-error">
  <div id="password-error" class="error" role="alert" aria-live="assertive"></div>
</div>
function showError(field, message) {
  const errorElement = document.getElementById(`${field.id}-error`);
  errorElement.textContent = message;
  field.setAttribute('aria-invalid', 'true');
}

3. 제출 버튼 접근성​

제출 버튼이 키보드로 접근 가능하고 목적이 명확해야 합니다:

<button type="submit" aria-label="연락처 양식 제출">제출하기</button>

실전 예제​

완전한 접근성을 갖춘 연락처 폼​

<form id="contactForm" action="/contact" method="post" novalidate>
  <div class="form-group">
    <label for="name">이름:</label>
    <input type="text" id="name" name="name" required aria-describedby="name-error">
    <div id="name-error" class="error" role="alert" aria-live="assertive"></div>
  </div>
  
  <div class="form-group">
    <label for="email">이메일:</label>
    <input type="email" id="email" name="email" required aria-describedby="email-error">
    <div id="email-error" class="error" role="alert" aria-live="assertive"></div>
  </div>
  
  <div class="form-group">
    <label for="message">메시지:</label>
    <textarea id="message" name="message" required aria-describedby="message-error"></textarea>
    <div id="message-error" class="error" role="alert" aria-live="assertive"></div>
  </div>
  
  <div class="form-group">
    <button type="submit">메시지 보내기</button>
    <div id="form-status" role="status" aria-live="polite"></div>
  </div>
</form>
document.addEventListener('DOMContentLoaded', function() {
  const form = document.getElementById('contactForm');
  
  form.addEventListener('submit', async function(event) {
    event.preventDefault();
    
    // 모든 오류 메시지 초기화
    clearErrors();
    
    // 유효성 검사
    let isValid = true;
    
    const nameField = document.getElementById('name');
    if (!nameField.value.trim()) {
      showError(nameField, '이름을 입력해주세요.');
      isValid = false;
    }
    
    const emailField = document.getElementById('email');
    if (!emailField.value.trim()) {
      showError(emailField, '이메일을 입력해주세요.');
      isValid = false;
    } else if (!isValidEmail(emailField.value)) {
      showError(emailField, '유효한 이메일 주소를 입력해주세요.');
      isValid = false;
    }
    
    const messageField = document.getElementById('message');
    if (!messageField.value.trim()) {
      showError(messageField, '메시지를 입력해주세요.');
      isValid = false;
    }
    
    if (!isValid) {
      // 첫 번째 오류 필드로 포커스 이동
      form.querySelector('[aria-invalid="true"]').focus();
      return;
    }
    
    // 제출 중 상태 표시
    const statusElement = document.getElementById('form-status');
    statusElement.textContent = '메시지를 보내는 중...';
    
    try {
      const formData = new FormData(form);
      
      const response = await fetch(form.action, {
        method: form.method,
        body: formData,
        headers: {
          'X-Requested-With': 'XMLHttpRequest'
        }
      });
      
      const result = await response.json();
      
      if (result.success) {
        // 성공 메시지 표시
        statusElement.textContent = '메시지가 성공적으로 전송되었습니다!';
        form.reset();
      } else {
        // 서버 오류 메시지 표시
        statusElement.textContent = result.message || '메시지 전송 중 오류가 발생했습니다.';
      }
    } catch (error) {
      console.error('오류:', error);
      statusElement.textContent = '메시지 전송 중 오류가 발생했습니다.';
    }
  });
  
  function clearErrors() {
    const errorElements = form.querySelectorAll('.error');
    errorElements.forEach(element => {
      element.textContent = '';
    });
    
    const fields = form.querySelectorAll('input, textarea');
    fields.forEach(field => {
      field.removeAttribute('aria-invalid');
    });
  }
  
  function showError(field, message) {
    const errorElement = document.getElementById(`${field.id}-error`);
    errorElement.textContent = message;
    field.setAttribute('aria-invalid', 'true');
  }
  
  function isValidEmail(email) {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  }
});

결론​

HTML 폼 제출은 단순해 보이지만, 사용자 경험과 보안을 위해 많은 고려사항이 필요합니다. 이 글에서 다룬 주요 포인트를 요약하면:

  1. 기본 원칙 이해하기: 폼의 기본 속성(action, method, enctype 등)과 그 의미를 이해합니다.
  2. 적절한 제출 방식 선택하기: 전통적인 폼 제출과 비동기 방식의 장단점을 고려합니다.
  3. 유효성 검사 구현하기: 클라이언트 측과 서버 측 모두에서 철저한 유효성 검사를 수행합니다.

서버와 통신 없이 HTML 폼의 데이터를 추출하는 방법은 매우 간단합니다. JavaScript를 사용해서 폼 데이터를 추출할 수 있는 여러 가지 방법을 알려드리겠습니다.

1. FormData 객체 사용하기​

FormData 객체는 폼 데이터를 쉽게 추출할 수 있는 가장 현대적인 방법입니다:

// HTML에 있는 form 요소 가져오기
const form = document.getElementById('myForm');

// form 요소로부터 FormData 객체 생성
const formData = new FormData(form);

// 모든 폼 데이터 확인하기
for (const [name, value] of formData.entries()) {
  console.log(`${name}: ${value}`);
}

// 특정 필드 값 가져오기
const username = formData.get('username');
console.log('사용자명:', username);

// 같은 이름을 가진 여러 값(예: 체크박스) 가져오기
const interests = formData.getAll('interests');
console.log('관심사:', interests);

2. FormData를 객체로 변환하기​

폼 데이터를 일반 JavaScript 객체로 변환하려면:

const form = document.getElementById('myForm');
const formData = new FormData(form);

// FormData를 일반 객체로 변환
const formDataObject = Object.fromEntries(formData);
console.log(formDataObject);

// 또는 더 복잡한 처리를 위한 방법
const formDataObject2 = {};
for (const [name, value] of formData.entries()) {
  // 같은 이름의 필드가 여러 개인 경우 배열로 처리
  if (formDataObject2[name]) {
    if (!Array.isArray(formDataObject2[name])) {
      formDataObject2[name] = [formDataObject2[name]];
    }
    formDataObject2[name].push(value);
  } else {
    formDataObject2[name] = value;
  }
}
console.log(formDataObject2);

3. submit 이벤트 리스너 사용하기​

폼이 제출될 때 데이터를 추출하려면:

document.getElementById('myForm').addEventListener('submit', function(e) {
  // 폼의 기본 제출 동작 방지
  e.preventDefault();
  
  // FormData 생성
  const formData = new FormData(this);
  
  // 데이터 처리
  const formDataObject = Object.fromEntries(formData);
  console.log('폼 데이터:', formDataObject);
  
  // 여기서 데이터를 사용할 수 있음
  // 예: 로컬 저장소에 저장
  localStorage.setItem('formData', JSON.stringify(formDataObject));
  
  // 사용자에게 피드백 제공
  alert('폼 데이터가 저장되었습니다!');
});

4. 개별 입력 요소에서 값 추출하기​

DOM 선택자를 사용하여 각 입력 요소의 값을 직접 가져올 수도 있습니다:

function collectFormData() {
  const data = {
    username: document.getElementById('username').value,
    email: document.getElementById('email').value,
    password: document.getElementById('password').value
  };
  
  // 체크박스 처리
  data.newsletter = document.getElementById('newsletter').checked;
  
  // 라디오 버튼 처리
  const genderRadios = document.getElementsByName('gender');
  for (const radio of genderRadios) {
    if (radio.checked) {
      data.gender = radio.value;
      break;
    }
  }
  
  // 다중 선택 (셀렉트 박스)
  const interestsSelect = document.getElementById('interests');
  data.interests = Array.from(interestsSelect.selectedOptions)
    .map(option => option.value);
  
  return data;
}

// 버튼 클릭 시 데이터 수집
document.getElementById('collectButton').addEventListener('click', function() {
  const formData = collectFormData();
  console.log('수집된 데이터:', formData);
});

전체 예제​

아래는 서버 통신 없이 폼 데이터를 추출하고 표시하는 전체 예제입니다:

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>폼 데이터 추출 예제</title>
  <style>
    body { font-family: Arial, sans-serif; line-height: 1.6; padding: 20px; }
    form { max-width: 500px; margin: 0 auto; }
    .form-group { margin-bottom: 15px; }
    label { display: block; margin-bottom: 5px; }
    input, select, textarea { width: 100%; padding: 8px; box-sizing: border-box; }
    button { padding: 10px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; }
    #result { margin-top: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; }
    pre { background: #f4f4f4; padding: 10px; overflow: auto; }
  </style>
</head>
<body>
  <h1>폼 데이터 추출 예제</h1>
  
  <form id="userForm">
    <div class="form-group">
      <label for="username">사용자명:</label>
      <input type="text" id="username" name="username">
    </div>
    
    <div class="form-group">
      <label for="email">이메일:</label>
      <input type="email" id="email" name="email">
    </div>
    
    <div class="form-group">
      <label>성별:</label>
      <div>
        <input type="radio" id="male" name="gender" value="male">
        <label for="male">남성</label>
        <input type="radio" id="female" name="gender" value="female">
        <label for="female">여성</label>
        <input type="radio" id="other" name="gender" value="other">
        <label for="other">기타</label>
      </div>
    </div>
    
    <div class="form-group">
      <label>관심사: (여러 개 선택 가능)</label>
      <div>
        <input type="checkbox" id="tech" name="interests" value="tech">
        <label for="tech">기술</label>
        <input type="checkbox" id="sports" name="interests" value="sports">
        <label for="sports">스포츠</label>
        <input type="checkbox" id="movies" name="interests" value="movies">
        <label for="movies">영화</label>
        <input type="checkbox" id="music" name="interests" value="music">
        <label for="music">음악</label>
      </div>
    </div>
    
    <div class="form-group">
      <label for="country">국가:</label>
      <select id="country" name="country">
        <option value="">선택하세요</option>
        <option value="kr">대한민국</option>
        <option value="us">미국</option>
        <option value="jp">일본</option>
        <option value="cn">중국</option>
      </select>
    </div>
    
    <div class="form-group">
      <label for="message">메시지:</label>
      <textarea id="message" name="message" rows="4"></textarea>
    </div>
    
    <button type="button" id="extractButton">데이터 추출하기</button>
  </form>
  
  <div id="result">
    <h2>추출된 데이터:</h2>
    <pre id="output">여기에 데이터가 표시됩니다.</pre>
  </div>
  
  <script>
    document.getElementById('extractButton').addEventListener('click', function() {
      // 1. FormData 사용
      const form = document.getElementById('userForm');
      const formData = new FormData(form);
      
      // 2. 객체로 변환
      const formDataObject = {};
      
      for (const [name, value] of formData.entries()) {
        // 체크박스와 같이 같은 이름이 여러 개인 필드 처리
        if (name === 'interests') {
          if (!formDataObject[name]) {
            formDataObject[name] = [];
          }
          formDataObject[name].push(value);
        } else {
          formDataObject[name] = value;
        }
      }
      
      // 3. 결과 표시
      document.getElementById('output').textContent = 
        JSON.stringify(formDataObject, null, 2);
    });
  </script>
</body>
</html>

이 예제는 사용자가 폼을 작성한 후 "데이터 추출하기" 버튼을 클릭하면 모든 폼 데이터를 JSON 형식으로 변환하여 페이지에 표시합니다. 서버로 데이터를 전송하지 않고도 클라이언트 측에서 폼 데이터를 완전히 처리할 수 있습니다.

FormData 객체

이번 챕터에선 파일 여부나 추가 필드 여부 등과 상관없이 통용되는 HTML 폼(form) 전송 방법에 대해 알아보겠습니다.

FormData는 폼을 쉽게 보내도록 도와주는 객체입니다. 이름을 보고 유추하셨듯이 FormData 객체는 HTML 폼 데이터를 나타냅니다.

생성자는 다음과 같습니다.

`let` formData `=` `new` `FormData``(``[`form`]``)``;`

HTML에 form 요소가 있는 경우, 위와 같은 코드를 작성하면 해당 폼 요소의 필드 전체가 자동 반영됩니다.

fetch 등의 네트워크 메서드가 FormData 객체를 바디로 받는다는 건 FormData의 특징입니다. 이때 브라우저가 보내는 HTTP 메시지는 인코딩되고 Content-Type 속성은 multipart/form-data로 지정된 후 전송됩니다.

서버 관점에선 FormData를 사용한 방식과 일반 폼 전송 방식에 차이가 없습니다

간단한 폼 전송하기​

아주 간단한 폼을 전송한다고 가정해봅시다.

보시다시피 아주 짧은 코드로도 전송 처리가 가능합니다.

``` `` `<`form `` `id```` `=``"`formElem`"` ````>` ``` ````` `` `<`input `` `type```` `=``"`text`"` ``` `name```` `=``"`name`"` ``` `value```` `=``"`Bora`"` ````>` ````` ````` `` `<`input `` `type```` `=``"`text`"` ``` `name```` `=``"`surname`"` ``` `value```` `=``"`Lee`"` ````>` ````` ``` `` `<`input `` `type```` `=``"`submit`"` ````>` ``` ```` `` `</`form ```>` ```` ```` `` `<`script ```>` ```` ```` ``` formElem`.``onsubmit` `=` `async` `(``e``)` `=>` `{` e`.``preventDefault``(``)``;` `let` response `=` `await` `fetch``(``'/article/formdata/post/user'``,` `{` `method``:` `'POST'``,` _`body``:` `new` `FormData``(`formElem`)`_ `}``)``;` `let` result `=` `await` response`.``json``(``)``;` `alert``(`result`.`message`)``;` `}``;` ``` ```` ```` `` `</`script ```>` ````

요청을 받아 처리하는 서버 측 코드는 튜토리얼 범위를 넘어서서 추가하진 않았는데, 서버는 POST 요청을 받아 '저장 성공’이라는 응답을 보내준다고 정도만 알고 계시면 됩니다.

FormData 메서드​

FormData에 속하는 필드는 아래와 같은 메서드로 수정할 수 있습니다.

  • formData.append(name, value) – name과 value를 가진 폼 필드를 추가
  • formData.append(name, blob, fileName) – <input type="file">형태의 필드를 추가. 세 번째 인수 fileName은 (필드 이름이 아니고) 사용자가 해당 이름을 가진 파일을 폼에 추가한 것처럼 설정해줌
  • formData.delete(name) – name에 해당하는 필드를 삭제
  • formData.get(name) – name에 해당하는 필드의 값을 가져옴
  • formData.has(name) – name에 해당하는 필드가 있으면 true를, 그렇지 않으면 false를 반환

폼은 이름(name)이 같은 필드 여러 개를 허용하기 때문에 append 메서드를 여러 번 호출해 이름이 같은 필드를 계속 추가해도 문제가 없습니다.

append 메서드 이외에 필드 추가 시 사용할 수 있는 메서드로 set도 있습니다. set이 append 메서드와 다른 점은 set은 name과 동일한 이름을 가진 필드를 모두 제거하고 새로운 필드 하나를 추가한다는 데 있습니다. 따라서 set 메서드를 쓰면 name을 가진 필드가 단 한 개만 있게끔 보장할 수 있습니다. 이 외에 다른 기능은 append 메서드와 동일합니다.

  • formData.set(name, value)
  • formData.set(name, blob, fileName)

참고로 폼 데이터 필드에 반복 작업을 할땐 for..of 루프를 사용할 수 있습니다.

`let` formData `=` `new` `FormData``(``)``;` formData`.``append``(``'key1'``,` `'value1'``)``;` formData`.``append``(``'key2'``,` `'value2'``)``;` `// key/value 쌍이 담긴 리스트` `for``(``let` `[`name`,` value`]` `of` formData`)` `{` `alert``(```` `` ` ```` `${`name`}` `` `=` `` `${`value`}` ```` ` `` ````)``;` `// key1 = value1, then key2 = value2` `}`

파일이 있는 폼 전송하기​

폼을 전송할 때 HTTP 메시지의 Content-Type 속성은 항상 multipart/form-data이고 메시지는 인코딩되어 전송됩니다. 파일이 있는 폼도 당연히 이 규칙을 따르기 때문에 <input type="file">로 지정한 필드 역시 일반 폼을 전송할 때와 유사하게 전송됩니다.

파일이 있는 폼 예시를 살펴봅시다.

``` `` `<`form `` `id```` `=``"`formElem`"` ````>` ``` ````` `` `<`input `` `type```` `=``"`text`"` ``` `name```` `=``"`firstName`"` ``` `value```` `=``"`Bora`"` ````>` ````` Picture: ````` `` `<`input `` `type```` `=``"`file`"` ``` `name```` `=``"`picture`"` ``` `accept```` `=``"`image/*`"` ````>` ````` ``` `` `<`input `` `type```` `=``"`submit`"` ````>` ``` ```` `` `</`form ```>` ```` ```` `` `<`script ```>` ```` ```` ``` formElem`.``onsubmit` `=` `async` `(``e``)` `=>` `{` e`.``preventDefault``(``)``;` `let` response `=` `await` `fetch``(``'/article/formdata/post/user-avatar'``,` `{` `method``:` `'POST'``,` _`body``:` `new` `FormData``(`formElem`)`_ `}``)``;` `let` result `=` `await` response`.``json``(``)``;` `alert``(`result`.`message`)``;` `}``;` ``` ```` ```` `` `</`script ```>` ````

Blob 데이터가 있는 폼 전송하기​

fetch 챕터에서 살펴본 바와 같이 이미지 같은 동적으로 생성된 바이너리 파일은 Blob 객체를 사용해 쉽게 전송할 수 있습니다. 이때 Blob 객체는 fetch 메서드의 body 매개변수에 바로 넘겨줄 수 있죠.

그런데 실제 코딩을 하다 보면 이미지를 별도로 넘겨주는 것보다 폼에 필드를 추가하고 여기에 이미지 ‘이름’ 등의 메타데이터를 같이 실어 넘겨주는 게 좀 더 편리합니다.

서버 입장에서도 원시 바이너리 데이터를 받는 것보다 multipart-encoded 폼을 받는 게 좀 더 적합하죠.

아래는 <canvas>를 사용해 만든 이미지를 FormData를 사용해 폼 형태로 다른 추가 필드와 함께 전송하는 예시입니다.

``````` `` `<`body `` ````` `style```` `=``"```` `margin``:``0` ````"` ``` ``````>` ``````` ``````` `` `<`canvas `` `id```` `=``"`canvasElem`"` ``` `width```` `=``"`100`"` ``` `height```` `=``"`80`"` ``` ````` `style```` `=``"```` `border``:``1``px` solid ````"` ``` ``````>` ``````````` `` `</`canvas ```>` ```` ``````` `` `<`input `` `type```` `=``"`button`"` ``` `value```` `=``"`이미지 전송`"` ``` ````` `onclick```` `=``"```` `submit``(``)` ````"` ``` ``````>` ``````` ```` `` `<`script ```>` ```` ```` ``` canvasElem`.``onmousemove` `=` `function``(``e``)` `{` `let` ctx `=` canvasElem`.``getContext``(``'2d'``)``;` ctx`.``lineTo``(`e`.`clientX`,` e`.`clientY`)``;` ctx`.``stroke``(``)``;` `}``;` `async` `function` `submit``(``)` `{` `let` imageBlob `=` `await` `new` `Promise``(``resolve` `=>` canvasElem`.``toBlob``(`resolve`,` `'image/png'``)``)``;` _`let` formData `=` `new` `FormData``(``)``;` formData`.``append``(``"firstName"``,` `"Bora"``)``;` formData`.``append``(``"image"``,` imageBlob`,` `"image.png"``)``;`_ `let` response `=` `await` `fetch``(``'/article/formdata/post/image-form'``,` `{` `method``:` `'POST'``,` `body``:` formData `}``)``;` `let` result `=` `await` response`.``json``(``)``;` `alert``(`result`.`message`)``;` `}` ``` ```` ```` `` `</`script ```>` ```` ```` `` `</`body ```>` ````

예시에서 이미지 Blob을 추가한 코드를 다시 봅시다.

formData`.``append``(``"image"``,` imageBlob`,` `"image.png"``)``;`

이 코드는 폼에 <input type="file" name="image"> 태그가 있고, 사용자 기기의 파일 시스템에서 파일명이 "image.png"(3번째 인수 참고)인 imageBlob 데이터(2번째 인수 참고)를 추가한 것과 동일한 효과를 줍니다.

요청을 받은 서버는 일반 폼과 동일하게 폼 데이터와 파일을 읽고 처리합니다.

요약​

FormData 객체는 fetch 등의 네트워크 메서드를 통해 HTML 폼을 보내는데 사용됩니다.

FormData 객체는 HTML 폼(form)을 직접 넘겨 new FormData(form)으로 만들 수도 있고, HTML 폼 없이 다음과 같은 메서드로 필드를 추가해 만들 수도 있습니다.

  • formData.append(name, value)
  • formData.append(name, blob, fileName)
  • formData.set(name, value)
  • formData.set(name, blob, fileName)

메서드를 사용할 때 주의할 점 2가지가 있습니다.

  1. set 메서드는 name이 같은 필드 모두를 지우고 append는 그렇지 않습니다. 다른 차이는 없습니다.
  2. 파일을 보낼 땐 세 번째 인수가 필요한데 이 인수는 사용자 파일 시스템에서 지정한 파일명과 동일하게 지정됩니다.

이외에도 다음과 같은 메서드가 있습니다

  • formData.delete(name)
  • formData.get(name)
  • formData.has(name)

다룰 내용은 여기까지입니다!