XMLHttpRequest2의 새로운 유용한 정보

소개

HTML5 세계에서 숨겨진 영웅 중 하나는 XMLHttpRequest입니다. 엄밀히 말해 XHR2는 HTML5가 아닙니다. 하지만 이는 브라우저 공급업체가 핵심 플랫폼을 점진적으로 개선하는 과정에서 이루어진 조치입니다. XHR2는 오늘날의 복잡한 웹 앱에서 매우 중요한 역할을 하기 때문에 새로운 도구 모음에 포함했습니다.

오래된 친구가 대대적인 개편을 거쳤지만 많은 사용자가 새로운 기능을 모르고 있습니다. XMLHttpRequest Level 2는 교차 출처 요청, 진행률 이벤트 업로드, 바이너리 데이터 업로드/다운로드 지원과 같은 웹 앱의 복잡한 해킹을 종식시키는 다양한 새로운 기능을 도입합니다. 이를 통해 AJAX는 File System API, Web Audio API, WebGL과 같은 최신 HTML5 API와 함께 작동할 수 있습니다.

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

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

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에서 실제로 반환되는 것은 바이너리 블롭이 아닙니다. 이미지 파일을 나타내는 바이너리 문자열입니다. 서버를 속여 처리되지 않은 데이터를 다시 전달하도록 합니다. 이 작은 보석은 작동하지만 블랙 매직이라고 부르고 사용하지 않는 것이 좋습니다. 데이터를 원하는 형식으로 강제 변환하기 위해 문자 코드 해킹 및 문자열 조작에 의존하는 경우는 언제나 문제가 됩니다.

응답 형식 지정

이전 예에서는 서버의 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는 바이너리 데이터를 위한 일반 고정 길이 컨테이너입니다. 원시 데이터의 일반화된 버퍼가 필요한 경우 매우 유용하지만, 이러한 배열의 진정한 강점은 JavaScript 유형 배열을 사용하여 기본 데이터의 '뷰'를 만들 수 있다는 것입니다. 실제로 단일 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();

Blob는 이 예와 같이 indexedDB에 저장하거나, 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>를 동적으로 만들고 append 메서드를 호출하여 <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.com에서 www.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);

})();

서버에서 파일을 재구성하는 코드는 여기에 표시되지 않습니다.

참조