Click or drag to resize

조각화된 파일 업로드

사용자에 의해 선택된 다수의 파일을 비동기 방식으로 업로드 합니다. 각 파일 데이터는 지정된 크기로 조각화되어 다수의 HTTP 요청 형식으로 서버로 전송됩니다. 서버에서는 조각으로 받은 데이터를 합쳐서 완전한 하나의 파일을 생성합니다.

Note Note

안드로이드 기본 브라우저의 경우 AJAX으로 blob 데이터를 전송하는데 문제가 있으므로 정상 동작하지 않습니다. 안드로이드 Chrome 브라우저는 정상 동작합니다.

이전 업로드 방식의 문제점

FileReader를 사용해 전체 파일을 읽을 후 AJAX로 업로드 하는 방식은 단순한 구조로 인해 구현이 쉬운 이점이 있는 반면 대용량 파일을 전송하는데 있어 아래와 같은 문제점이 존재합니다.

  • 웹 브라우저는 전체 파일 데이터를 메모리로 로딩한 후 서버로 전송합니다. 파일 크기가 작을 경우는 문제가 없지만 대용량 파일의 경우 메모리 문제를 발생시킬 수 있습니다.

  • 일반적으로 웹 서버는 DOS 공격 등을 차단하기 위해 HTTP 요청의 길이를 제한하고 있습니다. 대용량 파일을 받기 위해 이 제한을 해제해야 합니다.

  • 웹 서버는 최대 2GB까지 데이터를 받을 수 있으므로 전송할 수 있는 파일 크기는 2GB로 제한됩니다.

조각화된 업로드 방식은 위 문제점을 모두 해결합니다. 조각 크기만큼만 브라우저 메모리를 사용하고 HTTP 요청 크기도 조각 크기로 제한되므로 HTTP 요청 길이 제한에 부합됩니다. 매우 큰 파일도 조각으로 나뉘어 전송되므로 용량 제한 없는 업로드가 가능합니다.

조각화 업로드를 진행하는 Uploader 클래스

startUpload는 업로드를 시작시키는 메서드로 FileReader를 사용해 파일 내용 중 일부를 읽은 후 AJAX을 사용해 조각 데이터를 서버로 전송합니다. 바이너리 데이터 업로드 방식이므로 파일 이름은 file-name 헤더에 기입합니다. 조각화된 HTTP 요청을 서버에서 하나의 파일로 합쳐야 하므로 각각의 조각이 하나의 파일임을 나타낼 수 있도록 유일한 키 값을 함께 전송해야 합니다. UUID 형식의 키 값을 생성한 후 file-uid 헤더에 기입합니다. 그리고 조각에 전체 파일에서 어떤 부분인지를 나타내는 Content-Range 헤더를 구성합니다. 업로드 페이지에서는 이 두 값을 참조해 조각 데이터를 합쳐서 파일을 완성하게 됩니다.

form_multiple_chunk.asp JavaScript
// 하나의 파일을 업로드하는 클래스
function Uploader(file) {
    var self = this;
    this._file = file;
    this._fileUid = createUUID();
    this._serverFileKey = "";        // 업로드 성공시 서버에서 전달된 고유 값을 저장(이 예제에서는 파일 이름이 사용됨)

    this._fileSize = file.size;
    this._chunkSize = 10 * (1024 * 1024); // 전송할 조각 크기 10MB
    this._rangeStart = 0;
    this._rangeEnd = this._chunkSize;
    // 파일 내용 중 일부를 읽은 메서드 이름이 브라우저 마다 다름
    if ('mozSlice' in file) {
        this._sliceMethod = 'mozSlice';
    }
    else if ('webkitSlice' in file) {
        this._sliceMethod = 'webkitSlice';
    }
    else {
        this._sliceMethod = 'slice';
    }
    this._aborted = false;

    this._xhr = new XMLHttpRequest();
    this._xhr.addEventListener("load", chunkTransferComplete);
    this._xhr.upload.addEventListener("progress", updateProgress);
    this._xhr.upload.addEventListener("error", transferFailed);

    // uploadList에 업로드 아이템을 하나 추가한다.
    var li = document.createElement("li");
    var fileNameSpan = document.createElement("span");
    var blankSpan = document.createElement("span");
    var progressSpan = document.createElement("span");
    var cancelLink = document.createElement("a");
    fileNameSpan.innerHTML = file.name;
    blankSpan.innerHTML = " ";
    cancelLink.setAttribute("href","#");
    cancelLink.innerHTML = "[X]";
    li.appendChild(fileNameSpan);
    li.appendChild(blankSpan);
    li.appendChild(progressSpan);
    li.appendChild(cancelLink);
    var uploadList = document.getElementById("uploadList");
    uploadList.appendChild(li);

    // 취소 버튼 클릭 이벤트
    cancelLink.onclick = function () {
        self._aborted = true;
        self._xhr.abort();            // 업로드 취소
        self._removeUploadItem();    // 업로드 파일 목록에서 제거
    };

    // 업로드를 시작시킨다.
    this.startUpload = function () {
        var uploader = self, chunk;
        setTimeout(function() {
            // 범위 오버플로어 차단
            if (uploader._rangeEnd > uploader._fileSize) {
                uploader._rangeEnd = uploader._fileSize;
            }

            // 파일 데이터에서 일부를 읽어 서버로 전송한다.
            chunk = uploader._file[uploader._sliceMethod](uploader._rangeStart, uploader._rangeEnd);     
            uploader._xhr.open('POST', "upload_chunk.asp", true);
            // 파일 이름을 명시한다.
            uploader._xhr.setRequestHeader("file-name", encodeURIComponent(uploader._file.name));
            // 서버에서 파일 조각을 구분할 수 있도록 고유한 ID를 전송한다.
            uploader._xhr.setRequestHeader("file-uid", uploader._fileUid);
            // 파일 조각의 범위를 명시한다.
            uploader._xhr.setRequestHeader('Content-Range', 'bytes ' + self._rangeStart + '-' + self._rangeEnd + '/' + self._fileSize);
            uploader._xhr.send(chunk);
        }, 20);
    }

    this._transferComplete = function (response) {
        progressSpan.innerHTML = "";
        self._serverFileKey = response;
        uploadFileList.add(self._serverFileKey);
    }

    this._removeUploadItem = function () {
        uploadList.removeChild(li);
        uploadFileList.remove(self._serverFileKey);
    }

    // AJAX 데이터가 전송되는 동안 수시로 발생하는 이벤트로 진행 과정을 출력한다.
    function updateProgress(evt) {
        if (evt.lengthComputable) {
            var percentComplete = Math.ceil((self._rangeStart + evt.loaded) / self._fileSize * 100);
            progressSpan.innerHTML = percentComplete + "%";
        }
    }

    // 하나의 조각 업로드가 완료되면 호출되는 이벤트로 성공시 다음 조각 업로드를 진행한다.
    function chunkTransferComplete() {
        if (this.status == 200) {
            if (self._rangeEnd === self._fileSize) {
                // 마지막 조각 업로드가 완료되어 한 파일 업로드가 끝났다.
                self._transferComplete(this.responseText);
                return;
            }         
            // range 값 갱신 후 계속 업로드
            self._rangeStart = self._rangeEnd;
            self._rangeEnd = self._rangeStart + self._chunkSize;
            if (!self._aborted) self.startUpload();
        } else {
            alert("ERROR : " + this.responseText);
            self._removeUploadItem();
        }
    }        

    // 파일 업로드 도중 오류 발생
    function transferFailed(evt) {
        self._removeUploadItem();
        console.log("An error occurred while transferring the file.");
    }
}

업로드 처리 ASP 페이지

조각화된 업로드를 처리하기 위해서는 UploadSingle 오브젝트의 Start 메서드 대신 StartChunked 메서드를 사용해야 합니다. 이 메서드는 file-uid 헤더와 Content-Range 헤더를 참조해 여러 조각을 하나의 파일로 합치는 작업을 수행합니다.

ChunkedUploadCompleted 프로퍼티는 마지막 조각 데이터를 수신해 완전한 파일이 완성되었는지 여부를 반환합니다. 완전한 파일이 완성되었다면 Save 또는 SaveAs 메서드를 호출해 원하는 위치로 파일을 옮겨 저장합니다.

업로드 한 파일은 TempFileManager에 넣어 향후 자동 삭제 될 수 있도록 처리합니다.

upload_chunk.asp
<%@ CodePage=65001 Language=VBScript %>
<%
UploadPath = "C:\Temp\Upload"

Set upload = Server.CreateObject("TABSUpload4.UploadSingle")
upload.FileNameHeader = "file-name"
'하나의 파일에 대한 청크 바이너리 업로드 데이터를 처리한다.
upload.StartChunked "C:\Temp"

'마지막 청크가 도착해 업로드가 완료되었는지 검사한다.
If upload.ChunkedUploadCompleted Then
    ' 업로드가 완료되면 실제 파일로 저장한다.
    upload.Save UploadPath, False
    Response.Write upload.ShortSaveName

    'AJAX으로 업로드된 파일은 가비지 파일이 될 수 있으므로 TempFileManager에서 일정 시간(5시간) 동안만 유지 시킨다.
    Set tmgr = Server.CreateObject("TABSUpload4.TempFileManager")
    tmgr.AddFile upload.SaveName, 60 * 5
End If
%>
Caution note Caution

조각화된 업로드 방식은 하나의 파일을 다수의 HTTP 요청으로 나누어 전송하는 방식이므로 사용자 취소, 브라우저 종료, 네트워크 오류 등으로 업로드가 중단된 것을 명확하게 알 수 있는 방법이 없습니다. 따라서 업로드 임시 파일(.TMP)을 TempFileManager를 통해 자체적으로 관리해 서버에 가비지 파일이 남아 있지 않도록 만듭니다. TempFileManager가 올바르게 동작하기 위해서는 TABS Upload 5 Utility Service가 실행되고 있어야 합니다.