참고: 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을 사용합니다:
- Progressive enhancement: 어떤 이유로라도 자바스크립트가 실패해도 HTML form은 계속 작동합니다.
- 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로 파일을 업로드하는 최소한의 작업입니다. 요약하면 다음과 같습니다:
- <input>의 type=file을 사용해 사용자의 파일시스템에 접근
- Fetch(또는 XMLHttpRequest) API를 사용하여 HTTP request 구성
- request method를 POST로 설정
- request body에 파일 추가
- 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가 제대로 작동할 때나 실패할 때나 파일 업로드는 동일하게 작동합니다.
관련 글
'dev > javascript' 카테고리의 다른 글
[javascript] 자바스크립트 console.table() (7) | 2023.05.12 |
---|---|
[javascript] 자바스크립트 실행 시간 계산 (console.time) (7) | 2023.05.12 |
[javascript] HTML 웹 접근성 (Accessibility) (7) | 2023.05.11 |
[javascript] 자바스크립트 getBoundingClientRect (size, position) (6) | 2023.05.10 |
[javascript] 자바스크립트 요소 삽입 insertBefore (18) | 2023.05.09 |
댓글