JavaScript의 Fetch API와 유사한 XMLHttpRequest 래퍼 만들기

이 기사는 원래 게시되었습니다here

JS 표준 라이브러리의 fetch API 함수를 사용할 때 매번 응답을 처리하려고 할 때마다 짜증이 납니다. 그래서 XMLHttpRequest 프로토타입에 대한 래퍼를 만들기로 결정했습니다. 그러면 응답을 더 쉽게 처리할 수 있고 Fetch API와 유사한 인터페이스를 갖게 됩니다(기본적으로 XMLHttpRequest 위에 있는 Fetch API의 대안).

시작하기


XMLHttpRequest는 특정 이벤트에 응답하고 응답에서 데이터를 제공하는 콜백 인터페이스를 지향하지만 HTTP 요청을 처리하기 위한 매우 간단한 API를 제공합니다.
httpRequest API 함수의 첫 번째 버전부터 시작하겠습니다.

let httpRequest = function(method, url, { headers, body, options } = {}) {
  method = method.toUpperCase()

  let xhr = new XMLHttpRequest()
  xhr.withCredentials = true;
  xhr.open(method, url)

  xhr.setRequestHeader("Content-Type", "application/json")
  for (const key in headers) {
    if (Object.hasOwnProperty.call(headers, key)) {
      xhr.setRequestHeader(key, headers[key])
    }
  }

  xhr.send(body)

  return new Promise((resolve, reject) => {
    xhr.onload = function() {
      resolve(new HttpResponse(xhr))
    }

    xhr.onerror = function() {
      reject(new HttpError(xhr))
    }
  })
}


여기에서 볼 수 있듯이 함수는 HTTP 메서드와 URL을 필수 매개변수로 받습니다. 작업에 필요한 기본 개체를 만든 후 요청을 보냅니다. 함수는 xhr 요청 객체에 대한 이벤트 콜백을 래핑하는 약속을 반환합니다. 특정 이벤트가 트리거되면 Promise 리졸버가 HttpResponseHttpError 의 래핑된 값을 전송합니다.

참고로 여기에서도 withCredentialstrue 값으로 설정하여 CORS를 활성화했습니다. 이는 요청을 올바르게 실행하기 위해 서버에서도 활성화되어야 함을 의미합니다.

이제 HttpResponse 프로토타입을 정의합니다.

let HttpResponse = function(xhr) {
  this.body = xhr.response
  this.status = xhr.status
  this.headers = xhr.getAllResponseHeaders().split("\r\n").reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  })
  this.parser = new DOMParser();
}

HttpResponse.prototype.json = function() {
  return JSON.parse(this.body)
}

HttpResponse.prototype.getAsDOM = function() {
  return this.parser.parseFromString(this.body, "text/html")
}


그것이 하는 유일한 일은 XMLHttpRequest 객체를 취하고 HTTP 응답을 처리할 때 가장 관심을 나타내는 특정 필드만 분해합니다: status , bodyheaders . parser 필드는 getAsDOM 메서드에서 사용하도록 정의됩니다. 그 특정 메서드는 text/html 콘텐츠를 구문 분석하고 이를 DOM 개체로 변환합니다.
json 메서드는 매우 간단합니다. 본문에서 JSON을 구문 분석합니다.

이제 HttpError 프로토타입을 살펴보겠습니다.

let HttpError = function(xhr) {
  this.body = xhr.response
  this.status = xhr.status
  this.headers = xhr.getAllResponseHeaders().split("\r\n").reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  })
}

HttpError.prototype.toString = function() {
  let json = JSON.parse(this.body)
  return "["+ this.status + "] Error: " + json.error || json.errors.map(e => e.message).join(", ")
}


이것은 HttpResponse 프로토타입과 매우 유사하지만 JSON 오류 메시지에 대한 특정 규칙에 따라 오류 메시지를 래핑 해제하는 기능만 제공합니다.

작동 방식을 확인해 보겠습니다.

let response = await httpRequest("GET", "https://api.your-domain.com/resource/1")
console.log(response.json())


그러면 응답의 JSON 본문이 반환됩니다.

업로드 진행 상황 추적


Fetch API에 없는 또 다른 기능은 업로드 진행률 추적입니다. 입력 객체의 options 필드에 대한 콜백으로 추가할 수도 있습니다. 또한 요청 중에 오류가 발생했는지 추적하여 오류를 수신해야 합니다.

두 번째 버전은 이러한 모든 변경 사항을 다룹니다.

let httpRequest = function(method, url, { headers, body, options } = {}) {
  method = method.toUpperCase()

  let xhr = new XMLHttpRequest()
  xhr.withCredentials = true;
  xhr.open(method, url, true)

  xhr.setRequestHeader("Content-Type", "application/json")
  for (const key in headers) {
    if (Object.hasOwnProperty.call(headers, key)) {
      xhr.setRequestHeader(key, headers[key])
    }
  }

  if (options && options.hasOwnProperty("checkProgress")) {
    xhr.upload.onprogress = options.checkProgress
  }
  xhr.send(body)

  return new Promise((resolve, reject) => {
    xhr.onload = function() {
      resolve(new HttpResponse(xhr))
    }

    xhr.onerror = function() {
      reject(new HttpError(xhr))
    }

    xhr.onabort = function() {
      reject(new HttpError(xhr))
    }
  })
}

POST 요청을 찾는 방법을 살펴보겠습니다.

let response = await httpRequest("POST", "https://api.your-domain.com/resource", {
  body: JSON.stringify({"subject":"TEST!"}),
  options: {
    checkProgress: function(e) {
      console.log('e:', e)
    }
  }
})
console.log(response.status)
console.log(response.json())


전체 구현을 한 번 더 살펴보겠습니다.


let HttpResponse = function(xhr) {
  this.body = xhr.response
  this.status = xhr.status
  this.headers = xhr.getAllResponseHeaders().split("\r\n").reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  })
  this.parser = new DOMParser();
}

HttpResponse.prototype.json = function() {
  return JSON.parse(this.body)
}

HttpResponse.prototype.getAsDOM = function() {
  return this.parser.parseFromString(this.body, "text/html")
}


let HttpError = function(xhr) {
  this.body = xhr.response
  this.status = xhr.status
  this.headers = xhr.getAllResponseHeaders().split("\r\n").reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  })
}

HttpError.prototype.toString = function() {
  let json = JSON.parse(this.body)
  return "["+ this.status + "] Error: " + json.error || json.errors.join(", ")
}

let httpRequest = function(method, url, { headers, body, options } = {}) {
  method = method.toUpperCase()

  let xhr = new XMLHttpRequest()
  xhr.withCredentials = true;
  xhr.open(method, url, true)

  xhr.setRequestHeader("Content-Type", "application/json")
  for (const key in headers) {
    if (Object.hasOwnProperty.call(headers, key)) {
      xhr.setRequestHeader(key, headers[key])
    }
  }

  if (options && options.hasOwnProperty("checkProgress")) {
    xhr.upload.onprogress = options.checkProgress
  }
  xhr.send(body)

  return new Promise((resolve, reject) => {
    xhr.onload = function() {
      resolve(new HttpResponse(xhr))
    }

    xhr.onerror = function() {
      reject(new HttpError(xhr))
    }

    xhr.onabort = function() {
      reject(new HttpError(xhr))
    }
  })
}


이 작은 코드 조각은 XMLHttpRequest 라이브러리를 활용하며 여전히 유사한 API를 가지고 있습니다. 물론 개선의 여지가 많으니 가능하시다면 댓글로 여러분의 아이디어를 공유해주세요.

좋은 웹페이지 즐겨찾기