회사에서 크기가 GB 단위의 파일을 다운로드 받을 때 화면상에서 변화가 없어서 불편하다는 피드백을 받았다. 가장 좋은것은 progress로 표시하는것이고, 적어도 loading spinner라도 표시하도록 해야겠다고 생각했다.
파일 다운로드 progress는 한번도 구현해본적이 없었는데, 구글링 하다가 axios에서 onDownloadProgress라는 event handler를 지원한다는 것을 알게 되었고, 파일 다운로드시 progress를 표시하는 기능을 구현할 수 있었다.
const response = await axios.get(fileUrl, {
responseType: "blob",
// 다운로드 취소
signal: controller.signal,
// 다운로드 progress 업데이트
onDownloadProgress: (progressEvent) => {
// 작업량이 계산 가능하면
if (progressEvent.lengthComputable) {
// progressEvent.loaded => 현재 load된 작업량
// progressEvent.total => 전체 작업량
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
progressBar.value = percent;
progressText.textContent = `${percent}%`;
}
},
});
사용법은 어렵지 않다. progress 표시는 XMLHttpRequest에서 제공하는 기능인데, axios는 XMLHttpRequest 기반의 라이브러리라서 그런가 공식문서에 onDownloadProgress를 config 설정에 포함은 되어 있지만 자세하게 소개하지는 않고 있다.
그리고 추가적으로 궁금해서 찾아봤는데 fetch에서는 progress 표시를 위한 기능을 제공하지 않는다고 한다. 필요시 stream 형태의 response body를 읽어들여서 직접 구현을 해야된다고 한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Axios File Download with Progress</title>
</head>
<body>
<h2>파일 다운로드</h2>
<button id="downloadBtn">다운로드</button>
<button id="cancelBtn" disabled>취소</button>
<div id="progressWrapper" style="margin-top: 20px; display: none">
<progress
id="progressBar"
value="0"
max="100"
style="width: 300px"
></progress>
<span id="progressText">0%</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const downloadBtn = document.getElementById("downloadBtn");
const cancelBtn = document.getElementById("cancelBtn");
const progressBar = document.getElementById("progressBar");
const progressText = document.getElementById("progressText");
let controller = null;
downloadBtn.onclick = async () => {
const fileUrl =
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4";
controller = new AbortController();
cancelBtn.disabled = false;
downloadBtn.disabled = true;
progressBar.parentElement.style.display = "block";
progressBar.value = 0;
progressText.textContent = "0%";
try {
const response = await axios.get(fileUrl, {
responseType: "blob",
// 다운로드 취소
signal: controller.signal,
// 다운로드 progress 업데이트
onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
progressBar.value = percent;
progressText.textContent = `${percent}%`;
}
},
});
// 다운로드 완료 후 a 태그 강제 클릭으로 파일 저장
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "downloaded_file.pdf");
document.body.appendChild(link);
link.click();
link.remove();
progressBar.parentElement.style.display = "none";
progressBar.value = 0;
progressText.textContent = "0%";
} catch (err) {
if (axios.isCancel(err) || err.name === "CanceledError") {
alert("다운로드가 취소되었습니다.");
} else {
alert("다운로드 중 오류 발생!");
console.error(err);
}
} finally {
controller = null;
cancelBtn.disabled = true;
downloadBtn.disabled = false;
}
};
cancelBtn.onclick = () => {
if (controller) {
controller.abort(); // 요청 취소
progressBar.parentElement.style.display = "none";
progressBar.value = 0;
progressText.textContent = "0%";
}
};
</script>
</body>
</html>
오오!!
항상 궁금했던건데 사용해 볼 기회는 없었고
나중에 찾아봐야지 하고서 잊고 지내던 기능이네요!!
fetch가 더 나중에 등장한 걸로 알고 있는데
XMLHttpRequest에서는 제공하는데 fetch에서는 제공하지 않는게 의외네요!
나중에 사용할 일이 있으면 고민하지 말고, 저는 axios를 사용해야겠습니다! ㅋ.ㅋ