자바스크립트로 파일 다운로드 함수를 만들어서 사용하고 있었다.
요즘 이렇게 만들어뒀던 코드들을 분석하며 되돌아보는 시간을 가지고 있다.
downloadAuthorizedFile()
async function downloadAuthorizedFile(fileName = "file", { downloadURL = "", requestParam, responseType}) {
function checkIncludeExtension(fileName) {
const regex = /\.[0-9a-z]+$/i;
return regex.test(fileName);
}
if (!downloadURL)
throw new Error("Cannot found download URL.");
const response = await Api.get(downloadURL, requestParam, responseType);
if (!response || !response.data)
throw new Error("Fail download file.");
let fullFileName = fileName;
if (!checkIncludeExtension(fileName)) {
const contentType = response.data.type;
const extension = contentType.split("/")[1];
fullFileName = extension ? `${fileName}.${extension}` : fileName;
}
const fileURL = URL.createObjectURL(response.data);
const a = document.createElement("a");
a.href = fileURL;
a.download = fullFileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(fileURL);
document.body.removeChild(a);
}
권한이 필요한 관리자 페이지에서의 다운로드가 기존에 써왔던 방식으로는 작동하지 않아서 해당 함수 로직을 만들어보게 됐고 네이밍이 authorizedFile이 되었다. 기존 방식은 함수 외부에서 다운로드 할 객체를 미리 만들어두고 다운로드 로직을 탔었는데, 지금 방식은 직접 서버에 다운로드 객체를 요청한다.
예전 방식과 지금을 비교하는 포스팅도 글로 쓸 예정이다.
코드를 하나씩 뜯어보자.
매개변수와 필수값 검증
function downloadAuthorizedFile(fileName = "file", {
downloadURL = "",
requestParam,
responseType
}) {
if (!downloadURL)
throw new Error("Cannot found download URL.");
}
매개변수로는 기본 파일 이름인 fileName, 그리고 옵션객체로 downloadURL과 requestParam, responseType을 받는다.
필수 옵션값인 downloadURL이 없을 경우 에러를 발생시켜 유효성을 검사한다.
비동기로 서버에서 다운로드할 객체 요청
const response = await Api.get(downloadURL, requestParam, responseType);
if (!response || !response.data)
throw new Error("Fail download file.");
GET 요청을 통해 파일을 비동기적으로 가져온다. 비동기를 이용했기 때문에 함수 자체에 async를 걸어뒀다.
응답 값이 없거나 데이터가 비어있으면 에러를 발생시킨다.
확장자 유효성 검증
function checkIncludeExtension(fileName) {
const regex = /\.[0-9a-z]+$/i;
return regex.test(fileName);
}
...
let fullFileName = fileName;
if (!checkIncludeExtension(fileName)) {
const contentType = response.data.type;
const extension = contentType.split("/")[1];
fullFileName = extension ? `${fileName}.${extension}` : fileName;
}
자바스크립트 정규식을 통해 확장자가 포함된지 체크하는 내부 함수이다.
원래 이 로직이 없을 때는 확장자가 없는 파일을 다운로드 하려고 할 때 파일이 깨져서 다운로드 됐었다.
파일 이름에 확장자가 없는 경우, 응답 객체의 contentType을 참조하여 MIME 유형 기반의 확장자를 추가한다.
다운로드 로직
const fileURL = URL.createObjectURL(response.data);
const a = document.createElement("a");
a.href = fileURL;
a.download = fullFileName;
document.body.appendChild(a);
a.click();
다운로드한 데이터를 Blob URL로 변환하고 생성한 a 태그에 속성을 채워준 뒤 클릭해서 다운로드 한다.
클린업 수행
window.URL.revokeObjectURL(fileURL);
document.body.removeChild(a);
위에서 생성한 ObjectURL 객체를 revoke 함수를 통해 메모리를 해제하고, 생성한 a 태그도 DOM에서 제거해줬다.
어떻게 개선할 수 있을까?
나름 할 수 있는 선에서는 최선으로 짰다고 생각한 코드다. 하지만 시니어가 없는 우리 회사에서는 봐줄 사람도 없고 주변에도 요청하기 마땅치 않은 상황. 코드 리뷰를 받아보는 마음으로 GPT에게 코드를 던져 주고 개선점에 대해서 요청했다.
몇 가지 문제를 짚어줬다.
API 호출 에러 처리가 부족
try {
const response = await Api.get(downloadURL, requestParam, responseType);
if (!response || !response.data) throw new Error("Failed to download file: No data in response.");
} catch (error) {
console.error("Download error:", error.message);
throw new Error("Failed to download file. Please check the network connection or URL.");
}
try-catch 블록을 사용해서 네트워크 오류나 API 호출 실패 시 구체적인 메시지와 함께 처리하는 방법을 추가했다고 한다.
에러를 단순하게 처리했던 이유는 사실 따로 있다.
Api.get 이 부분은 내가 axios를 래핑해서 API 모듈을 따로 만들어서 사용하고 있는데, 이 모듈엔 에러를 처리하는 공통 핸들러가 따로 있다. 서버에서 에러가 발생하면 서버 에러 메시지를 표시해주기 때문에 굳이 넣지 않았던 것이다.
그래서 이 부분은 따로 추가하지 않아도 될 것 같다.
파일 타입에 의존적인 확장자 처리
if (!checkIncludeExtension(fileName)) {
const defaultExtension = 'txt'; // 기본 확장자 설정
const contentType = response.data.type;
const extension = contentType.split("/")[1] || defaultExtension;
fullFileName = `${fileName}.${extension}`;
}
MIME 타입에 의존하지 않고 기본 확장자를 설정하는 코드이다.
현재 내 코드는 파일 타입에 너무 의존적으로 확장자를 처리하고 있다고 한다. 일부 경우엔 MIME 타입과 실제 확장자가 다를 수 있다고 하는데, 이 부분은 넣어줘도 좋을 것 같다.
중복된 DOM 추가/삭제
const downloadAnchor = document.createElement("a"); // 재사용 가능한 <a> 요소
async function downloadAuthorizedFile(fileName = "file", { downloadURL = "", requestParam, responseType }) {
...
const fileURL = URL.createObjectURL(response.data);
downloadAnchor.href = fileURL;
downloadAnchor.download = fullFileName;
downloadAnchor.click();
window.URL.revokeObjectURL(fileURL);
}
<a> 요소를 매번 동적으로 추가하고 삭제하면서 DOM 변경이 많아진다고 한다.
확실히 이 부분에서 성능이 저하될 가능성이 있어보이긴 했다. 하지만 파일 다운로드 함수는 그렇게 많이 호출되는 부분은 아닐 것 같아서 크게 신경쓰이는 부분은 아니였다.
GPT가 제안한 방법은 재사용 가능한 <a> 요소를 전역으로 선언하는 방법을 제안했다.
파일 다운로드가 아닌 다른 부분에서 성능을 고려하게 된다면 이용해도 좋을 것 같다.
개선점을 반영한 전체 코드
async function downloadAuthorizedFile(fileName = "file", { downloadURL = "", requestParam, responseType}) {
function checkIncludeExtension(fileName) {
const regex = /\.[0-9a-z]+$/i;
return regex.test(fileName);
}
if (!downloadURL)
throw new Error("Cannot found download URL.");
const response = await Api.get(downloadURL, requestParam, responseType);
if (!response || !response.data)
throw new Error("Fail download file.");
let fullFileName = fileName;
if (!checkIncludeExtension(fileName)) {
const defaultExtension = "txt";
const contentType = response.data.type;
const extension = contentType.split("/")[1] || defaultExtension;
fullFileName = `${fileName}.${extension}`;
}
const fileURL = URL.createObjectURL(response.data);
const a = document.createElement("a");
a.href = fileURL;
a.download = fullFileName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(fileURL);
document.body.removeChild(a);
}
다운로드 함수 사용 예시
const handleDownload = () => {
const downloadURL = `/users/${entityId}/files`;
const requestData = {
downloadURL: downloadURL,
responseType: "blob"
}
downloadAuthorizedFile(originEntity.fileName, requestData);
}
확장자가 깨지는 이슈가 없었다면 확장자를 자동으로 추가하는 기능이 없었을 지도 모른다.
모든 개발이 정해진 프로세스대로만 동작하면 최선이지만 사용자가 한 명이라도 있다면 쉬운 일은 아닐 것이다.
여러 가지 다양한 이슈를 겪어보고 해결하는 과정을 통해서 더 나은 코드를 짜는 사람이 되고 싶다.
'Frontend > javascript' 카테고리의 다른 글
FormData 파일 전송 방법: Blob 그리고 JSON.stringify() (1) | 2024.11.14 |
---|---|
jQuery 코드 순수 자바스크립트로 마이그레이션 하기 (0) | 2024.11.12 |
JavaScript로 HTML 코드에서 텍스트만 추출하기 (1) | 2024.11.09 |
JavaScript 법정동 데이터 관리와 동적 선택기 모듈 구현: 동적 셀렉트 박스 구현 (1) | 2024.11.08 |
JavaScript 법정동 데이터 관리와 동적 선택기 모듈 구현: 최신 데이터 가져오기 및 데이터 가공하기 (1) | 2024.11.07 |