본문 바로가기
dev/javascript

[javascript] 자바스크립트 파일 업로드 (form post)

by 최연탄 2023. 5. 12.
728x90
반응형

참고: https://austingil.com/upload-files-with-javascript/

이 포스트에서는 JavaScript를 사용해 파일 업로드 요청을 만들어 보겠습니다. 또한 파일에 접근하는 방법, HTTP 요청을 생성하는 방법, 이를 재사용 가능하게 하는 방법을 알아보겠습니다.

이벤트 핸들러 설정

<form action="/api" method="post" enctype="multipart/form-data">
  <label for="file">File</label>
  <input id="file" name="file" type="file" />
  <button>Upload</button>
</form>

사용자의 파일에 접근하려면 <input> 요소에 type 속성을 file로 지정해야합니다. 파일 업로드를 위해 HTTP request를 만들려면 <form> 요소를 사용해야 합니다.

JavaScript는 항상 그렇지만 파일에 접근하려면 사용자의 파일 입력이 있어야합니다. 하지만 HTTP 요청은 브라우저가 form 없이 Fetch API를 사용하여 만들어낼 수 있습니다.

하지만 다음과 같은 이유로 여전히 form을 사용합니다:

  1. Progressive enhancement: 어떤 이유로라도 자바스크립트가 실패해도 HTML form은 계속 작동합니다.
  2. I'm lazy: 나중에 언급하겠지만 form은 실제로 작업을 더 쉽게 만들어줍니다.

위의 이유로 JavaScript가 이 form을 제출할 수 있도록 "submit" 이벤트 핸들러를 설정하겠습니다.

const form = document.querySelector('form');
form.addEventListener('submit', handleSubmit);

function handleSubmit(event) {
  // The rest of the logic will go here.
}

다음 파트에서 이벤트 핸들러 함수인 handleSubmit 내의 로직을 살펴보겠습니다.

HTTP request 준비

이 핸들러에서 가장 먼저 해야 할 일은 이벤트의 preventDefault() 메소드를 호출하여 브라우저가 form을 제출하고 페이지를 다시 로드하지 않도록 하는 것 입니다. 이를 함수 실행 중 exception이 발생하면 preventDefault()가 호출되지 않고 브라우저의 기본 동작을 실행하도록 이벤트 핸들러의 마지막 부분에 배치하는 것을 선호합니다.

function handleSubmit(event) {
  // Any JS that could fail goes here
  event.preventDefault();
}

다음으로 Fetch API를 사용하여 HTTP 요청을 구성하겠습니다. Fetch API의 첫 번째 인수는 URL 이고 두 번째 인수는 옵셔널 객체 입니다.

form의 action 속성에서 URL을 얻을 수 있습니다. 이는 이벤트의 currentTarget 속성을 사용하여 접근할 수 있는 모든 form DOM node에서 사용할 수 있습니다. action이 HTML에 정의되어 있지 않다면 기본 값으로 현재 브라우저의 URL을 사용합니다.

function handleSubmit(event) {
  const form = event.currentTarget;
  const url = new URL(form.action);

  fetch(url);

  event.preventDefault();
}

URL 정의를 HTML에 의존하면 URL을 더 명확하게 하고 이벤트 핸들러를 재사용 가능하게 하며 JavaScript 번들을 더 작게 만듭니다. 또한 JavaScript 실행이 실패해도 기능을 유지합니다.

기본적으로 Fetch는 GET method를 사용하여 HTTP 요청을 보내지만 파일을 업로드 하려면 POST method를 사용해야 합니다. Fetch의 두 번째 인수를 사용하여 method를 변경할 수 있습니다. 이제 method 속성을 설정할 객체를 만들고 여기에 HTML form의 method 속성을 할당하겠습니다.

const url = new URL(form.action);

const fetchOptions = {
  method: form.method,
};

fetch(url, fetchOptions);

이제 남은 부분은 실제로 리퀘스트 바디에 파일을 포함하는 것 입니다.

request body 추가

이전에 Fetch 요청을 생성해본 적이 있다면 body를 JSON 문자열 또는 URLSearchParams 객체로 넣은 적이 있을 것 입니다. 하지만 파일을 다룰 때는 바이너리 파일 내용에 대한 접근 권한이 없기 때문에 둘 다 사용할 수 없습니다.

다행히도 FormData API가 있습니다. 이는 form DOM node에서 요청 바디를 구성하는 데 사용할 수 있습니다. 편리하게도 이렇게 하면 요청의 Content-type 헤더가 multipart/form-data로 설정됩니다. 이는 바이너리 데이터를 전송하는 데 필요한 단계입니다.

const url = new URL(form.action);
const formData = new FormData(form);

const fetchOptions = {
  method: form.method,
  body: formData,
};

fetch(url, fetchOptions);

여기 까지가 JavaScript로 파일을 업로드하는 최소한의 작업입니다. 요약하면 다음과 같습니다:

  1. <input>의 type=file을 사용해 사용자의 파일시스템에 접근
  2. Fetch(또는 XMLHttpRequest) API를 사용하여 HTTP request 구성
  3. request method를 POST로 설정
  4. request body에 파일 추가
  5. HTTP 헤더 Content-type을 multipart/form-data로 설정

이번 포스트에서는 submit 이벤트 핸들러가 있는 HTML form 요소를 사용하고, request body에 FormData 객체를 사용하는 편리한 방법을 알아봤습니다. 현재 handleSubmit 함수는 다음과 같아야 합니다:

function handleSubmit(event) {
  const url = new URL(form.action);
  const formData = new FormData(form);

  const fetchOptions = {
    method: form.method,
    body: formData,
  };

  fetch(url, fetchOptions);

  event.preventDefault();
}

아쉽게도 현재 만들어진 submit 핸들러는 재사용이 쉽지 않습니다. 모든 요청에는 FormData 객체로 설정된 body와 multipary/form-data로 설정된 Content-type 헤더가 포함됩니다. 이 코드는 꽤 불안정합니다. body는 GET 요청에서는 허용되지 않으며 다른 POST 요청을 할 때 다른 content type을 사용해야 할 수도 있기 때문입니다.

재사용 가능하게 만들기

위의 코드를 GET 및 POST 요청을 처리하고 적절한 Content-type 헤더를 보낼 수 있도록 보다 강력하게 만들 수 있습니다. FormData 외에 URLSearchParams 객체를 만들고 request method가 POST 또는 GET 인지 여부에 따라 일부 로직을 변경할 수 있습니다.

  • request가 POST를 사용하는가?
    • 네: form의 enctype 속성이 multipary/form-data 인가?
      • 네: request body를 FormData 객체로 설정하고 브라우저가 자동으로 Content-type 헤더를 multipary/form-data로 설정
      • 아니오: request body를 URLSearchParams 객체로 설정하면 브라우저가 자동으로 Content-type 헤더를 application/x-www-form-urlencoded로 설정 해 줌
    • 아니오: 이 요청을 GET으로 생각하고 URL에 쿼리 파라미터로 데이터 추가

위의 요구사항을 리팩토링한 솔루션은 다음과 같습니다:

function handleSubmit(event) {
  const form = event.currentTarget;
  const url = new URL(form.action);
  const formData = new FormData(form);
  const searchParams = new URLSearchParams(formData);

  const fetchOptions = {
    method: form.method,
  };

  if (form.method.toLowerCase() === 'post') {
    if (form.enctype === 'multipart/form-data') {
      fetchOptions.body = formData;
    } else {
      fetchOptions.body = searchParams;
    }
  } else {
    url.search = searchParams;
  }

  fetch(url, fetchOptions);

  event.preventDefault();
}

다음의 여러 가지 이유로 이 솔루션을 좋아합니다:

  • 어떤 form에서도 사용할 수 있습니다.
  • 기본 HTML 구성을 선언적 요소로 사용 가능합니다.
  • HTTP request는 HTML form과 동일하게 작동합니다. 이는 점진적 향상의 원칙을 따르므로 JavaScript가 제대로 작동할 때나 실패할 때나 파일 업로드는 동일하게 작동합니다.

관련 글

자바스크립트 fetch로 JSON POST

자바스크립트 fetch로 FormData POST

자바스크립트 addEventListener 사용 방법

반응형

댓글