XMLHttpRequest2의 새로운 유용한 정보

소개

HTML5 세계에서 잘 알려지지 않은 핵심 기업 중 하나는 XMLHttpRequest입니다. 엄밀히 말하면 XHR2는 HTML5가 아닙니다. 하지만 이는 브라우저 공급업체가 핵심 플랫폼에 제공하는 점진적인 개선의 일부입니다. XHR2는 오늘날의 복잡한 웹 앱에서 매우 중요한 역할을 하기 때문에 새 제품 상자에 포함했습니다.

예전에 친구가 많이 개편되었지만 많은 사람들이 새 기능을 모르고 있었던 것으로 나타났습니다. XMLHttpRequest 수준 2는 교차 출처 요청, 업로드 진행률 이벤트, 바이너리 데이터 업로드/다운로드 지원 등 웹 앱의 복잡한 해킹을 끝낼 수 있는 많은 새로운 기능을 도입합니다. 이를 통해 AJAX는 File System API, Web Audio API, WebGL과 같은 여러 최첨단 HTML5 API와 함께 작동할 수 있습니다.

이 가이드에서는 XMLHttpRequest의 새로운 기능, 특히 파일 작업에 사용할 수 있는 기능을 중점적으로 설명합니다.

데이터를 가져오는 중입니다.

파일을 바이너리 blob으로 가져오는 것은 XHR에서 매우 힘든 작업입니다. 기술적으로는 불가능했습니다. 잘 문서화된 한 가지 요령은 아래와 같이 사용자 정의 문자 집합으로 MIME 유형을 재정의하는 것입니다.

이미지를 가져오는 이전 방법은 다음과 같습니다.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);

// Hack to pass bytes through unprocessed.
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
  if (this.readyState == 4 && this.status == 200) {
    var binStr = this.responseText;
    for (var i = 0, len = binStr.length; i < len; ++i) {
      var c = binStr.charCodeAt(i);
      //String.fromCharCode(c & 0xff);
      var byte = c & 0xff;  // byte at offset i
    }
  }
};

xhr.send();

이렇게 하면 작동하기는 하지만 실제로 responseText에 반환되는 것은 바이너리 blob이 아닙니다. 이미지 파일을 나타내는 바이너리 문자열입니다. 서버가 처리되지 않은 상태로 데이터를 다시 전달하도록 하고 있습니다. 이 조그만 보석은 효과가 있을지 모르지만, 이것을 검은 마법이라고 부르고 반대하도록 조언하겠습니다. 데이터를 원하는 형식으로 강제 변환하기 위해 문자 코드 해킹과 문자열 조작에 의존할 때마다 문제가 됩니다.

응답 형식 지정

이전 예에서는 서버의 MIME 유형을 재정의하고 응답 텍스트를 바이너리 문자열로 처리하여 이미지를 바이너리 '파일'로 다운로드했습니다. 대신 XMLHttpRequest의 새로운 responseTyperesponse 속성을 활용하여 반환되는 데이터의 형식을 브라우저에 알려 보겠습니다.

xhr.responseType
요청을 전송하기 전에 데이터 요구사항에 따라 xhr.responseType를 'text', 'arraybuffer', 'blob' 또는 'document'로 설정합니다. xhr.responseType = ''를 설정하거나 생략하면 기본적으로 응답이 'text'로 설정됩니다.
xhr.response
요청이 성공하면 xhr의 응답 속성에 요청된 데이터가 DOMString, ArrayBuffer, Blob 또는 Document (responseType에 설정된 항목에 따라 다름)로 포함됩니다.

이 새로운 기능을 통해 이전 예를 재작업할 수 있지만 이번에는 이미지를 문자열 대신 Blob로 가져옵니다.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    // Note: .response instead of .responseText
    var blob = new Blob([this.response], {type: 'image/png'});
    ...
  }
};

xhr.send();

훨씬 좋네요!

ArrayBuffer 응답

ArrayBuffer는 바이너리 데이터의 고정 길이 일반 컨테이너입니다. 일반화된 원시 데이터 버퍼가 필요한 경우 매우 편리합니다. 하지만 가장 큰 장점은 자바스크립트 유형 배열을 사용하여 기본 데이터의 '뷰'를 만들 수 있다는 것입니다. 실제로 단일 ArrayBuffer 소스에서 여러 뷰를 만들 수 있습니다. 예를 들어 동일한 데이터에서 기존 32비트 정수 배열과 동일한 ArrayBuffer를 공유하는 8비트 정수 배열을 만들 수 있습니다. 기본 데이터는 동일하게 유지되고, 우리는 그저 다른 표현을 만들 뿐입니다.

예를 들어 다음은 ArrayBuffer와 동일한 이미지를 가져오지만 이번에는 이 데이터 버퍼에서 부호 없는 8비트 정수 배열을 만듭니다.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'arraybuffer';

xhr.onload = function(e) {
  var uInt8Array = new Uint8Array(this.response); // this.response == uInt8Array.buffer
  // var byte3 = uInt8Array[4]; // byte at offset 4
  ...
};

xhr.send();

blob 응답

Blob를 직접 사용하고자 하거나 파일의 바이트를 조작할 필요가 없는 경우에는 xhr.responseType='blob'를 사용합니다.

window.URL = window.URL || window.webkitURL;  // Take care of vendor prefixes.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;

    var img = document.createElement('img');
    img.onload = function(e) {
      window.URL.revokeObjectURL(img.src); // Clean up after yourself.
    };
    img.src = window.URL.createObjectURL(blob);
    document.body.appendChild(img);
    ...
  }
};

xhr.send();

이 예와 같이 BlobindexedDB에 저장하거나 HTML5 파일 시스템에 쓰거나 Blob URL 만들기 등 다양한 위치에서 사용할 수 있습니다.

데이터 전송

데이터를 다양한 형식으로 다운로드할 수 있다는 것은 바람직한 일이지만, 이러한 리치 형식을 홈 기반 (서버)으로 다시 보낼 수 없다면 소용이 없습니다. XMLHttpRequest로 인해 한동안 DOMString 또는 Document (XML) 데이터를 전송하도록 제한했습니다. 이제 더 이상 그렇지 않습니다. 개선된 send() 메서드가 DOMString, Document, FormData, Blob, File, ArrayBuffer 유형 중 하나를 허용하도록 재정의되었습니다. 이 섹션의 나머지 부분에 있는 예에서는 각 유형을 사용하여 데이터를 전송하는 방법을 보여줍니다.

문자열 데이터 전송 중: xhr.send(DOMString)

function sendText(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.responseText);
    }
  };

  xhr.send(txt);
}

sendText('test string');
function sendTextNew(txt) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.responseType = 'text';
  xhr.onload = function(e) {
    if (this.status == 200) {
      console.log(this.response);
    }
  };
  xhr.send(txt);
}

sendTextNew('test string');

새로운 내용은 없지만 오른쪽 스니펫은 약간 다릅니다. 비교를 위해 responseType='text'를 설정합니다. 이번에도 이 줄을 생략해도 동일한 결과가 나옵니다.

양식 제출: xhr.send(FormData)

대부분의 사용자는 jQuery 플러그인이나 다른 라이브러리를 사용하여 AJAX 양식 제출을 처리하는 데 익숙할 것입니다. 대신 XHR2용으로 고안된 또 다른 새로운 데이터 유형인 FormData를 사용할 수 있습니다. FormData는 JavaScript로 즉시 HTML <form>를 만드는 데 편리합니다. 그런 다음 AJAX를 사용하여 해당 양식을 제출할 수 있습니다.

function sendForm() {
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);
}

기본적으로 <form>를 동적으로 만들고 추가 메서드를 호출하여 <input> 값을 처리합니다.

물론 <form>를 처음부터 만들 필요는 없습니다. FormData 객체는 페이지의 기존 HTMLFormElement에서 초기화할 수 있습니다. 예를 들면 다음과 같습니다.

<form id="myform" name="myform" action="/server">
  <input type="text" name="username" value="johndoe">
  <input type="number" name="id" value="123456">
  <input type="submit" onclick="return sendForm(this.form);">
</form>
function sendForm(form) {
  var formData = new FormData(form);

  formData.append('secret_token', '1234567890'); // Append extra data before send.

  var xhr = new XMLHttpRequest();
  xhr.open('POST', form.action, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);

  return false; // Prevent page from submitting.
}

HTML 양식은 파일 업로드 (예: <input type="file">)를 포함할 수 있으며 FormData도 이를 처리할 수 있습니다. 파일을 추가하기만 하면 send()가 호출될 때 브라우저에서 multipart/form-data 요청을 생성합니다.

function uploadFiles(url, files) {
  var formData = new FormData();

  for (var i = 0, file; file = files[i]; ++i) {
    formData.append(file.name, file);
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', url, true);
  xhr.onload = function(e) { ... };

  xhr.send(formData);  // multipart/form-data
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  uploadFiles('/server', this.files);
}, false);

파일 또는 blob 업로드: xhr.send(Blob)

XHR을 사용하여 File 또는 Blob 데이터를 전송할 수도 있습니다. 모든 FileBlob이므로 여기서도 작동합니다.

이 예시에서는 Blob() 생성자를 사용하여 처음부터 새 텍스트 파일을 만들고 이 Blob를 서버에 업로드합니다. 또한 이 코드는 핸들러를 설정하여 사용자에게 업로드 진행 상황을 알립니다.

<progress min="0" max="100" value="0">0% complete</progress>
function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  // Listen to the upload progress.
  var progressBar = document.querySelector('progress');
  xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) {
      progressBar.value = (e.loaded / e.total) * 100;
      progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
    }
  };

  xhr.send(blobOrFile);
}

upload(new Blob(['hello world'], {type: 'text/plain'}));

바이트 청크 업로드: xhr.send(ArrayBuffer)

마지막으로 ArrayBuffer를 XHR의 페이로드로 전송할 수 있습니다.

function sendArrayBuffer() {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };

  var uInt8Array = new Uint8Array([1, 2, 3]);

  xhr.send(uInt8Array.buffer);
}

교차 출처 리소스 공유 (CORS)

CORS는 한 도메인의 웹 애플리케이션이 다른 도메인에 교차 도메인 AJAX 요청을 수행할 수 있게 해줍니다. 사용 설정하기는 매우 간단하므로 서버에서 하나의 응답 헤더만 전송하면 됩니다.

CORS 요청 사용 설정

애플리케이션이 example.com에 있고 www.example2.com에서 데이터를 가져오려고 한다고 가정해 보겠습니다. 일반적으로 이러한 유형의 AJAX 호출을 시도하면 요청이 실패하고 브라우저에서 원본 불일치 오류가 발생합니다. CORS를 사용하면 www.example2.com에서 헤더를 추가하여 example.com의 요청을 허용하도록 선택할 수 있습니다.

Access-Control-Allow-Origin: http://example.com

Access-Control-Allow-Origin은(는) 사이트 또는 전체 도메인에서 단일 리소스에 추가할 수 있습니다. 모든 도메인에서 나에게 요청을 보낼 수 있도록 허용하려면 다음과 같이 설정하세요.

Access-Control-Allow-Origin: *

실제로 해당 사이트 (html5rocks.com)는 모든 페이지에서 CORS를 사용하도록 설정했습니다. 개발자 도구를 실행하면 응답에 Access-Control-Allow-Origin가 표시됩니다.

html5rocks.com의 Access-Control-Allow-Origin 헤더
html5rocks.com의`Access-Control-Allow-Origin` 헤더

교차 출처 요청은 쉽게 사용 설정할 수 있으므로 데이터가 공개 상태인 경우 CORS를 사용 설정하세요.

교차 도메인 요청 만들기

서버 엔드포인트에서 CORS를 사용 설정한 경우 교차 출처 요청을 실행하는 것은 일반적인 XMLHttpRequest 요청과 다르지 않습니다. 예를 들어 다음은 이제 example.comwww.example2.com에 수행할 수 있는 요청입니다.

var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.example2.com/hello.json');
xhr.onload = function(e) {
  var data = JSON.parse(this.response);
  ...
}
xhr.send();

실제 사례

HTML5 파일 시스템에 파일 다운로드 및 저장

이미지 갤러리가 있고 여러 이미지를 가져온 다음 HTML5 파일 시스템을 사용하여 로컬에 저장한다고 가정해 보겠습니다. 이를 실행하는 한 가지 방법은 이미지를 Blob로 요청하고 FileWriter를 사용하여 작성하는 것입니다.

window.requestFileSystem  = window.requestFileSystem || window.webkitRequestFileSystem;

function onError(e) {
  console.log('Error', e);
}

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
xhr.responseType = 'blob';

xhr.onload = function(e) {

  window.requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
    fs.root.getFile('image.png', {create: true}, function(fileEntry) {
      fileEntry.createWriter(function(writer) {

        writer.onwrite = function(e) { ... };
        writer.onerror = function(e) { ... };

        var blob = new Blob([xhr.response], {type: 'image/png'});

        writer.write(blob);

      }, onError);
    }, onError);
  }, onError);
};

xhr.send();

파일 분할 및 각 부분 업로드

File API를 사용하면 대용량 파일을 업로드하는 작업을 최소화할 수 있습니다. 이 기법은 업로드를 여러 개의 청크로 분할하고, 각 부분에 대해 XHR을 생성하고, 이 파일을 서버에 함께 넣는 것입니다. 이는 Gmail에서 대용량 첨부파일을 이렇게 빠르게 업로드하는 방법과 비슷합니다. 이 기술은 Google App Engine의 32MB http 요청 제한을 우회하는 데 사용할 수도 있습니다.

function upload(blobOrFile) {
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/server', true);
  xhr.onload = function(e) { ... };
  xhr.send(blobOrFile);
}

document.querySelector('input[type="file"]').addEventListener('change', function(e) {
  var blob = this.files[0];

  const BYTES_PER_CHUNK = 1024 * 1024; // 1MB chunk sizes.
  const SIZE = blob.size;

  var start = 0;
  var end = BYTES_PER_CHUNK;

  while(start < SIZE) {
    upload(blob.slice(start, end));

    start = end;
    end = start + BYTES_PER_CHUNK;
  }
}, false);

})();

여기에는 서버에서 파일을 재구성하는 코드가 나와 있지 않습니다.

참조