파일 업로드 폼을 만들 때 개발자들은 보통 두 가지 방식 중 하나를 선택한다. 첫째, 전통적인 form 태그 submit을 쓰거나, 둘째, AJAX로 직접 데이터를 조작하려다 multipart/form-data 인코딩 때문에 막힌다. 다만 대부분의 개발자는 FormData API가 정확히 뭐고 왜 필요한지, 어떻게 쓰는지 완벽하게 이해하지 못한 채 스택오버플로우에서 가져온 코드를 그냥 복사해 붙인다. 이번에는 FormData가 정확히 뭔지, 왜 필요한지, 어떻게 쓰는지 실무에서 즉시 활용할 수 있도록 완벽하게 정리해서 소개하겠다.

 

1단계: FormData의 기초 개념 이해하기

FormData는 JavaScript에서 HTTP 요청의 multipart/form-data 인코딩을 자동으로 처리해주는 빌트인 API다. 전통적인 AJAX로 파일을 업로드하려면 개발자가 바이너리 데이터와 텍스트 데이터를 섞어서 올바른 형식으로 인코딩해야 하는데, FormData를 쓰면 브라우저가 알아서 처리해준다.

일반적인 form submit은 페이지 새로고침을 유발하지만, FormData를 fetch나 XMLHttpRequest(또는 jQuery ajax)와 조합하면 페이지 새로고침 없이 파일과 텍스트를 함께 업로드할 수 있다.

FormData가 필요한 상황:

  • 파일 + 텍스트 데이터를 동시에 전송해야 할 때
  • 여러 파일을 한 번에 업로드할 때
  • 페이지 새로고침 없이 폼을 제출하고 싶을 때
  • 진행률(progress) 이벤트를 모니터링하고 싶을 때

 

2단계: FormData 객체 생성과 데이터 추가

FormData 객체를 만드는 방법은 두 가지다.

방법 A: 빈 FormData 객체를 만들고 append() 메서드로 데이터 추가

const formData = new FormData();
formData.append('username', 'john_doe');
formData.append('email', 'john@example.com');
formData.append('profilePic', document.getElementById('fileInput').files[0]);

append() 메서드를 호출할 때마다 필드가 하나씩 추가된다. 같은 이름의 필드를 여러 개 추가할 수도 있다(예: 여러 파일).

방법 B: 기존 form 요소에서 FormData 객체 자동 생성

const form = document.getElementById('myForm');
const formData = new FormData(form);
// form 안의 모든 input, textarea, select 값이 자동으로 formData에 포함됨

방법 B가 더 간편하지만, 특정 필드만 선별해서 추가하고 싶다면 방법 A를 써야 한다.

 

3단계: FormData 데이터 검증 및 수정

FormData에 추가한 데이터를 읽거나 수정하는 메서드들도 있다.

// ✓ 특정 필드 값 읽기
const email = formData.get('email'); // 'john@example.com'

// ✓ 같은 이름의 모든 값 가져오기 (배열 형태)
const files = formData.getAll('attachments'); // [File, File, ...]

// ✓ 특정 필드가 존재하는지 확인
if (formData.has('username')) {
  console.log('username 필드 존재함');
}

// ✓ 특정 필드 수정
formData.set('username', 'jane_doe'); // 기존 값을 덮어씀

// ✓ 특정 필드 삭제
formData.delete('email');

// ✓ 모든 필드 순회
for (const [key, value] of formData.entries()) {
  console.log(key, value);
}

주의: set() vs append() 차이

  • append(): 같은 이름의 필드가 여러 개 있어도 모두 추가됨 (배열처럼 쌓임)
  • set(): 같은 이름의 필드가 있으면 전부 삭제하고 새로운 값 하나로 교체함

 

4단계: FormData를 fetch로 전송하기

FormData의 진정한 가치는 fetch나 AJAX와 조합했을 때 나타난다.

✗ 잘못된 방법: Content-Type을 명시적으로 application/json으로 설정

const formData = new FormData();
formData.append('username', 'john');
formData.append('profilePic', document.getElementById('fileInput').files[0]);

fetch('/upload', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json' // ✗ 잘못됨!
  },
  body: JSON.stringify(formData) // ✗ FormData를 JSON.stringify하면 빈 객체가 됨
});

이 코드는 작동하지 않는다. FormData를 JSON.stringify하면 파일 정보가 모두 손실된다.

✓ 올바른 방법: Content-Type을 생략하거나, 브라우저가 자동으로 설정하게 함

const formData = new FormData();
formData.append('username', 'john');
formData.append('profilePic', document.getElementById('fileInput').files[0]);

fetch('/upload', {
  method: 'POST',
  body: formData
  // Content-Type 헤더를 명시하지 않음 → 브라우저가 자동으로 multipart/form-data로 설정
})
.then(response => response.json())
.then(data => console.log('업로드 성공:', data))
.catch(error => console.error('업로드 실패:', error));

이게 정답이다. FormData를 body에 넣으면 브라우저가 자동으로 Content-Type 헤더를 multipart/form-data로 설정하고, boundary(필드 구분자)까지 생성한다.

 

5단계: 실전 예제 - 파일 + 텍스트 함께 업로드

HTML 폼:

<form id="uploadForm">
  <input type="text" name="title" placeholder="제목" required />
  <input type="text" name="description" placeholder="설명" />
  <input type="file" id="fileInput" name="attachment" />
  <button type="button" id="submitBtn">업로드</button>
</form>
<div id="progressBar" style="display:none; width:300px; background:#ddd; height:20px;">
  <div id="progressFill" style="width:0%; background:#4CAF50; height:100%;"></div>
</div>

JavaScript:

document.getElementById('submitBtn').addEventListener('click', async function() {
  const formElement = document.getElementById('uploadForm');
  const fileInput = document.getElementById('fileInput');
  
  // FormData 객체 생성
  const formData = new FormData(formElement);
  
  // 파일 유효성 검증
  if (fileInput.files.length === 0) {
    alert('파일을 선택해주세요');
    return;
  }
  
  const file = fileInput.files[0];
  const maxSize = 10 * 1024 * 1024; // 10MB
  
  if (file.size > maxSize) {
    alert('파일 크기가 10MB를 초과했습니다');
    return;
  }
  
  // FormData 전송
  try {
    const xhr = new XMLHttpRequest();
    
    // 진행률 이벤트 모니터링 (fetch는 진행률 미지원, XMLHttpRequest 사용)
    xhr.upload.addEventListener('progress', function(e) {
      if (e.lengthComputable) {
        const percentComplete = (e.loaded / e.total) * 100;
        document.getElementById('progressFill').style.width = percentComplete + '%';
        document.getElementById('progressBar').style.display = 'block';
      }
    });
    
    xhr.addEventListener('load', function() {
      if (xhr.status === 200) {
        const response = JSON.parse(xhr.responseText);
        console.log('업로드 성공:', response);
        alert('업로드가 완료되었습니다!');
        formElement.reset();
        document.getElementById('progressBar').style.display = 'none';
        document.getElementById('progressFill').style.width = '0%';
      } else {
        alert('업로드 실패: ' + xhr.status);
      }
    });
    
    xhr.addEventListener('error', function() {
      alert('네트워크 오류가 발생했습니다');
    });
    
    // POST 요청 전송
    xhr.open('POST', '/api/upload', true);
    xhr.send(formData);
    
  } catch (error) {
    console.error('에러:', error);
  }
});

 

6단계: 흔한 실수 vs 올바른 사용법
상황✗ 잘못된 코드✓ 올바른 코드
FormData 전송 시 Content-Typeheaders: { 'Content-Type': 'application/json' }headers를 생략 또는 멀티파트 자동 설정
FormData 직렬화JSON.stringify(formData)formData를 그대로 body에 넣음
파일 접근formData.get('fileInput')formData.get('attachment') (name 속성 사용)
진행률 모니터링fetch 사용XMLHttpRequest 사용 (fetch는 미지원)
여러 파일 업로드append() 한 번만 호출루프로 여러 번 append() 호출

 

7단계: 고급 활용 - 여러 파일 한 번에 업로드
const fileInputs = document.getElementById('multiFileInput');
const formData = new FormData();

// 추가 텍스트 필드
formData.append('projectName', 'My Project');

// 선택된 모든 파일 추가
for (let i = 0; i < fileInputs.files.length; i++) {
  formData.append('files', fileInputs.files[i]); // 같은 이름으로 여러 번 append
}

// PHP/Node.js 서버에서는 $_FILES['files'] 또는 req.files.files로 배열처럼 접근 가능
fetch('/api/upload-multiple', {
  method: 'POST',
  body: formData
})
.then(response => response.json())
.then(data => console.log('모든 파일 업로드 완료:', data));

 

8단계: 서버 측 처리 예제 (PHP)
<?php
// Content-Type이 multipart/form-data이므로 $_FILES와 $_POST에서 직접 접근

// 텍스트 필드 접근
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';

// 파일 접근
if (isset($_FILES['attachment'])) {
  $file = $_FILES['attachment'];
  $fileName = $file['name'];
  $fileTmpPath = $file['tmp_name'];
  $fileError = $file['error'];
  $fileSize = $file['size'];
  
  // 파일 유효성 검증
  if ($fileError === 0 && $fileSize < 10 * 1024 * 1024) {
    $uploadDir = 'uploads/';
    $destPath = $uploadDir . uniqid() . '_' . basename($fileName);
    
    if (move_uploaded_file($fileTmpPath, $destPath)) {
      echo json_encode(['success' => true, 'filePath' => $destPath]);
    } else {
      echo json_encode(['success' => false, 'error' => '파일 이동 실패']);
    }
  } else {
    echo json_encode(['success' => false, 'error' => '파일 유효성 검증 실패']);
  }
} else {
  echo json_encode(['success' => false, 'error' => '파일 없음']);
}
?>

 

정리: FormData는 웹 애플리케이션의 필수 도구

FormData API는 파일 업로드와 멀티파트 폼 데이터 처리의 거의 모든 실무 상황에서 필요하다. 복잡한 인코딩을 브라우저가 자동으로 처리해주기 때문에, 개발자는 데이터 유효성 검증과 에러 처리에만 집중할 수 있다. 이 글에서 다룬 append(), get(), set(), entries() 등의 메서드와 fetch/XMLHttpRequest 조합만 이해해도 대부분의 파일 업로드 기능을 구현할 수 있다. 특히 진행률 모니터링이 필요하면 XMLHttpRequest의 upload 이벤트를 활용하는 방법을 꼭 기억해두자. FormData의 이런 작은 메서드들이 모여서 견고하고 사용자 친화적인 업로드 경험을 만든다는 점을 잊지 말자. 이 글의 실전 예제 코드를 참고해 바로 자신의 프로젝트에 적용하면, 안정적인 멀티파트 폼 처리 기능을 얻을 수 있을 것이다.