Frontend/javaScript

[javaScript] 파일 업로드, 다운로드 (파일 깨짐 해결, readAsArrayBuffer, btoa, _arrayBufferToBase64)

dddzr 2023. 4. 25. 17:01

1. 파일 손상문제 발생

자바스크립트 단에서 파일을 DB에 업로드, 다운로드를 진행하다가 파일이 깨지는 문제가 발생했습니다.

아래는 문제가 발생했을 당시 코드입니다.

var reader = new FileReader();

const file = document.getElementById("fileInput").files[0];
const fileName = file.name;
const fileSize = file.size;
const fileType = file.type;
let fileBlob = "";
reader.readAsArrayBuffer(file);
reader.onload = function (evt) {
fileBlob = evt.target.result;

let data = {
    Name: fileName,
    Size: fileSize,
    Type: fileType,
    Blob: fileBlob
}

//download test code (DB에 저장하기 전)
const blob = new Blob([fileBlob], fileName, {type: fileType}); // encoding: "UTF-8",
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${data.name}`;
a.click();
a.remove();
window.URL.revokeObjectURL(url);
};

위의 코드에서는 FileReader 객체를 사용하여 클라이언트 측에서 선택한 파일을 읽어들인 후, 이를 바이너리 문자열로 변환하여 객체에 저장하는 코드입니다.

readAsArrayBuffer를 사용하여 인코딩 했고,

파일을 인코딩 하는 과정에서 문제가 있나 확인하기 위해 인코딩 -> 디코딩 코드를 바로 작성하여 테스트 해보았습니다.

이때 파일이 깨지지 않고 다운로드 되는 것을 확인했습니다.

 

*readAsArrayBuffer() 메서드는 다음과 같은 방식으로 동작합니다.

1. FileReader 객체를 생성합니다.
2. FileReader 객체의 readAsArrayBuffer() 메서드를 호출하면서, 읽어들일 파일을 인자로 전달합니다.
3. 파일을 비동기적으로 읽어들입니다.
4. 파일 읽기가 완료되면, onload 이벤트 핸들러 함수가 호출됩니다.
5. onload 이벤트 핸들러 함수에서는, FileReader 객체의 result 속성을 사용하여, 읽어들인 바이너리 데이터를 ArrayBuffer 객체로 변환합니다.

 

2. base64 인코딩 이용

검색결과 DB에 저장하는 과정에서 인코딩 방식에 따라 손상이 있을 수 있고

통신과정에서 바이너리 데이터의 손실을 막기 위해 Base64로 인코딩으로 파일을 저장해보았습니다.

아래는 수정한 코드입니다.

 

*Base64로 인코딩하면  6bit당 2bit의 Overhead가 발생하여 전송해야 될 데이터의 크기가 약 33% 정도 늘어납니다.

다른 방법이 있을 경우 수정하는게 좋을 것 같습니다.

function upload() {
    var reader = new FileReader();

    const file = document.getElementById("fileInput").files[0];
    const fileName = file.name;
    const fileSize = file.size;
    const fileType = file.name.split(".").reverse()[0];

    reader.readAsArrayBuffer(file);
    reader.onloadend = function(evt) {
    if (evt.target.readyState == FileReader.DONE) {
        let fileByteArray = evt.target.result;
        let array = new Uint8Array(fileByteArray);

        let data = {
        Name: fileName,
        Size: fileSize,
        Type: fileType,
        //Blob: btoa(String.fromCharCode.apply(null, array))
        Blob: _arrayBufferToBase64(array)
        }
        fileModel.saveFile("File", fileComponent, data);
        if (callback !== null && callback !== undefined) callback();
    }
    }
    function _arrayBufferToBase64( buffer ) {
    var binary = '';
    var bytes = new Uint8Array( buffer );
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode( bytes[ i ] );
    }
    return window.btoa( binary );
    }
}

처음 코드를 수정했을 때 btoa(String.fromCharCode.apply(null, array))를 이용했는데 파일의 크기가 커질 경우 에러가 나서 _arrayBufferToBase64로 코드를 최종 수정했습니다.

 

*btoa(String.fromCharCode.apply(null, array))는 배열(Array)을 문자열(String)로 변환하고, 그 문자열을 Base64 인코딩하는 방식으로 ArrayBuffer를 Base64 문자열로 변환하는 방법입니다.

*_arrayBufferToBase64 함수는 ArrayBuffer 객체를 직접 다루며, 이를 Blob 객체로 감싸고 FileReader 객체를 사용하여 읽어들인 뒤, Base64 인코딩하는 방식으로 ArrayBuffer를 Base64 문자열로 변환합니다.

 

3. 디코딩 + 다운로드

Base64를 디코딩할때는 atob를 사용합니다.

function downloadFile() {
    const data = getData(parameter);
    downloadAsFile(data, parameter);
}

function getData(parameter) {
let datas = fileModel.getFileData();
return datas;
}

function downloadAsFile(data) {
    if (data.length  < 3) {
        for (let i = 0; i < data.length; i++) {
            let fileType = "";
            const extension = data[i].Type.toUpperCase();
            if (extension === "TEXT" || extension === "TXT") {
            fileType = 'text/plain';
            } else if (extension === "PPT") {
            fileType = 'application/vnd.ms-powerpoint';
            } else if ( extension === "PPTX") {
            fileType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
            } else if (extension.indexOf("XLX") !== -1 || extension === "XLXS") {
            fileType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
            }else {
            fileType = 'application/octet-stream';
            }

            const blob = new Blob([Uint8Array.from(atob(data[i].Blob), c => c.charCodeAt(0))], {Type: fileType});//encoding: "UTF-8", 
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement("a");
            a.href = url;
            a.download = `${data[i].Name}`;
            a.click();
            a.remove();
            window.URL.revokeObjectURL(url);
        }
    } else {
        let today = new Date();
        let todayStr = today.getFullYear() + today.getMonth() + today.getDate();

        let zip = new JSZip();
        for (let i = 0; i < data.length; i++) {
            const utf8Encode = new TextEncoder();
            let uint8Array = utf8Encode.encode(data[i].Blob);
            zip.file(data[i].Name, uint8Array);
        }

        zip.generateAsync({Type: "blob"}).then(
            function(blob) {
            const zipName = todayStr + ".zip";
            saveToFile_Chrome(zipName, blob);
            }
        )
    }
}

Blob() 생성자의 첫 번째 매개변수로는 Uint8Array 객체를 전달합니다.
두 번째 매개변수로는 다운로드할 파일의 MIME 타입을 전달합니다.

 

 URL.createObjectURL() 메서드를 사용하여 Blob() 객체를 URL로 변환하고, <a> 태그의 href 속성에 할당합니다. download 속성에는 다운로드할 파일의 이름을 설정하고, a.click() 메서드를 사용하여 다운로드를 진행합니다. 마지막으로, URL.revokeObjectURL() 메서드를 사용하여 URL 객체를 해제합니다.

 

파일의 개수가 3개 이상인 경우에는 JSZip 라이브러리를 사용하여 파일들을 압축한 후, 압축 파일을 다운로드합니다. JSZip() 생성자를 사용하여 JSZip 객체를 생성한 후, zip.file() 메서드를 사용하여 파일 데이터를 추가합니다. generateAsync() 메서드를 사용하여 압축 파일을 생성하고, saveToFile_Chrome() 함수를 호출하여 다운로드합니다.