과거에는 사용자가 파일을 탐색해서 업로드할 파일을 선택하고, 파일 입로드가 완료되어 페이지가 갱신될 때까지 오랫동안(진행 상황을 알려주는 표시나 피드백 없이) 기다려야 했는데 이는 굉장히 불편한 부분일 것입니다. 다행히 XHR은 두 가지 문제를 해결해 줍니다. XHR은 백그라운드로 파일을 업로드하면서 동시에 진행 이벤트도 제공하여 사용자에게 실시간 프로그레스바를 보여줄 수 있다. 주요 브라우저는 XHR을 지원합니다.

기존의 XMLHttpRequest API의 send() 함수나 FormData 인스턴스를 이용해 파일을 업로드 할 수 있습니다. FormData 인스턴스는 폼의 컨텐츠를 조작하기 쉬운 인터페이스로 표현한 것입니다. FormData 오브젝트를 직접 만들 수도 있고 오브젝트를 인스턴스화할 때 기존의 form 엘리먼트를 전달해서 만드는 방법도 있습니다.


var formData = new FormData($("form")[0]);

// 폼 데이터를 문자열로 추가 가능합니다.
formData.append("stringkey", "stringData");

// 파일 오브젝트도 추가할 수 있습니다.
formData.append("fileKey", file);


FormData를 완성했으면 XMLHttpRequest를 이용해 서버로 POST할 수 있습니다. jQuery로 Ajax 요청을 이용하는 경우이면 jQuery가 데이터를 직렬화하지 않도록 processData 옵션을 false로 설정해야 합니다. 브라우저가 Content-Type 헤더를 자동으로 multipart/form-data로 설정(multipart/form-data의 컨텐츠는 multipart boundary 로 파트를 구분)하므로 Content-Type 헤더는 설정하지 않습니다.


jQuery.ajax({
  data: formData,
  processData: false,
  url: "http://example.com", 
  type: "POST"
});


FormData 의 대안으로 파일을 직접 XHR 오브젝트의 send() 함수로 전달하는 방법도 있습니다.


var req = new XMLHttpRequest();
req.open("POST", "http://example.com", true); 
req.send(file);


또는 jQuery의 Ajax API로 다음과 같이 파일을 업로드할 수 있습니다.


$.ajax({
  url: "http://example.com",
  type: "POST",
  success: function(){ /* ... */ },
  processData: false,
  data: file
});


위 방법은 기존의 multipart/form-data 방법과는 다르다는 점에 유의해야 합니다. 보통은 파일 이름 등의 정보를 업로드에 포함합니다. 그러나 위 예제는 순수하게 파일 데이터만 업로드합니다. 

파일 정보를 건달하려면 X-File-Name 같은 커스텀 헤더를 설정해야 합니다. 그러면 우리가 만든 서버는 커스텀 헤더를 읽어서 파일 속성을 처리할 수 있습니다.


$.ajax({
  url: "http://example.com",
  type: "POST",
  success: function(){ /* .., */ },
  processData: false,
  contentType: "multipart/form-data",

  beforeSend: function(xhr, settings){
    xhr.setRequestHeader("Cache-Control", "no-cache");
    xhr.setRequestHeader("X-File-Name", file.fileName);
    xhr.setRequestHealer("X-File-Size", file.fileSize);
  },
  data: file
});


불행히도 순수 데이터 업로드는 멀티파트나 URL, 인코딩 형식 파라미터보다 인지도가 떨어지는 방식이므로 많은 서버에서 문제가 발생할 수 있습니다. 위 방법을 사용한다면 요청을 직접 파싱해야 할지도 모릅니다. 따라서 multipart/form-data를 요청하는 방법으로 직렬화된 데이터를 FormData 오브젝트를 이용해 업로드하는 방법을 권장합니다. 



Ajax 진행

XHR 레벨 2 규격 명세는 내려받기와 업로드 요청 모두에 progress 이벤트 지원을 추가했습니다. progress 이벤트를 이용하면 실시간 파일 업로드 프로그레스바로 사용자에게 업로드가 언제쯤 종료될지 등의 정보를 제공할 수 있습니다.

XHR 인스턴스에 리스너를 추가하면 내려받기 요청에서 발생하는 progress 이벤트를 받을 수 있습니다.


var req = new XMLHttpRequest();

req.addEventListener("progress", updateProgress, false);
req.addEventListener("load", transferComplete, false);
req.open();


XHR 인스턴스의 upload 속성에 이벤트 리스너를 추가하면 업로드 요청에서 발생하는 progress 이벤트를 받을 수 있습니다.


var req = new XMLHttpRequest();

req.upload.addEventListener("progress", updateProgress, false);
req.upload.addEventListener("load", transferComplete, false);
req.open();


업로드 요청이 끝난 다음 서버가 응답을 보내기 전에 load 이벤트가 발생합니다. beforeSend 콜백으로 XHR 오브젝트와 설정을 전달하므로 load 이벤트를 jQuery에 추가할 수 있습니다. 

아래는 커스텀 헤더를 포함한 예제입니다.


$.ajax({
  url: "http://example.com",
  type: "POST",
  success: function(){ /* ... */ },
  processData: false,
  dataType: "multipart/form-data",

  beforeSend: function(xhr, settings){
    var upload = xhr.upload;

    if(settings.progress){
      upload.addEventListener("progress", settings.progress, false);
    }

    if(settings.load){
      upload.addEventListener("load", settings.load, false);
    }

    var fd = new FormData;
    for(var key in settings.data){
      fd.append(key, settings.data[key]);
    }
    settings.data = fd;
  },

  data: file
});


progress 이벤트는 업로드의 position(몇 바이트를 업로드 했는지를 가리키는 값)과 total(업로드 요청의 전체 크기를 바이트로 표현한 값)을 포함합니다. 이 두 프로퍼티를 이용해 진행 퍼센트를 계산할 수 있습니다.


var progress = function(event){
  var percentage = Math.round((event.position / event.total) * 100);
  // 프로그레스바 설정
};


이벤트는 타임스탬프를 포함하므로 업로드를 시작한 시간을 기록했다면 예상 완료 시간(ETA, Estimated Time of Arrival)을 만들 수 있습니다.


var startStamp = new Date();
var progress = function(e) {
  var lapsed = startStamp - e.timeStamp;
  var eta = lapsed * e.total / e.position - lapsed;
};


그러나 소량의 데이터를 업로드(파일 크기가 작은 데이터)할 때는 예상 시간이 정확하지 않을 수 있으므로 4분 이상 걸리는 큰 파일 업로드에만 ETA를 보여주는 것도 한 방법입니다. 아니면 업로드가 언제 끝날지를 시각적으로 보여줄 수 있는 퍼센트바(percentage bar)를 이용하는 것도 괜찮은 방법이 될 수 있습니다.


0 댓글