라이브러리도 좋지만 XMLHttpRequest를 직접 사용해 봅시다.🤓

준영·2021년 3월 31일
1

XMLHttpRequest

XMLHttpRequest(XHR) 객체는 서버와 상호작용하기 위하여 사용됩니다. 전체 페이지의 새로고침없이도 URL 로부터 데이터를 받아올 수 있습니다. 이는 웹 페이지가 사용자가 하고 있는 것을 방해하지 않으면서 페이지의 일부를 업데이트할 수 있도록 해줍니다. XMLHttpRequest 는 AJAX 프로그래밍에 주로 사용됩니다.
출처 MDN

많이 사용하는 통신 라이브러리인 Axios도 XMLHttpRequest를 기반으로 만들어졌다고 합니다. 그래서 조금 더 날 것으로 다루는 방법을 익히기 위해 Axios와 비슷한 것을 만들어 보려고 합니다.👍

기본 구조

class XHR {
  constructor(url) {
    this.url = url;
    this.defaultHeader = {};
  }

  send (config) {
    const xhr = new XMLHttpRequest();
  }

  get() {}

  post() {}
}

먼저 XHR이라는 class를 만들어 줬습니다. 여기에 각 인스턴스마다 url과 defaultHeader를 담을 공간을 만들어줬습니다.
send 메서드는 XMLHttpRequest의 인스턴스를 생성하고 설정에 맞게 요청을 보내는 역할을 합니다.

Core

  send(config) {
    return new Promise((resolve, reject) => {
      try {
        const xhr = new XMLHttpRequest();
        if (!xhr) throw "XMLHttpRequest의 인스턴스를 만들 수 없습니다.";

        const mergedUrl = typeof config.url === "string" && config.url !== "/" ? this.url + config.url : this.url;
        
        xhr.open(config.method, mergedUrl);
        
      } catch (error) {
        reject(error);
      }
    });
  }

👉 비동기 처리를 위해 Promise를 만들어주고 XMLHttpRequest의 인스턴스를 만들어줍니다.
👉 open 메서드로 요청을초기화 합니다.

setRequestHeader

...
    this.defaultHeader = {
      "Content-Type": "application/json",
      "Cache-Control": "no-cache",
      accept: "*",
    };
...

        if (!xhr) throw "XMLHttpRequest의 인스턴스를 만들 수 없습니다.";

	const mergedUrl = typeof config.url === "string" && config.url !== "/" ? this.url + config.url : this.url;

        xhr.open(config.method, mergedUrl);

		//헤더 설정
        Object.entries({ ...this.defaultHeader, ...config.headers }).forEach(
          ([key, value]) => {
            xhr.setRequestHeader(key, value);
          }
        );
...

그다음 헤더를 설정합니다. 저는 constructor 안의 기본 헤더를 위와 같이 설정했습니다. 기본으로 json 형태를, 캐싱을 하지 않음을, accept를 all로 설정했습니다. 참고

setRequestHeader은 open 이후 send 전에 호출해야 합니다.

onreadystatechange

...
this.resolveStatus = [200];
...
  dataParser(xhr) {
    const contentType = xhr.getResponseHeader("Content-Type");
    const data = xhr.response;

    if (contentType && contentType.indexOf("json") !== -1) {
      return JSON.parse(data);
    } else {
      return data;
    }
  }
...
    // 상태 변경 실행 함수
        xhr.onreadystatechange = () => {
          try {
            if (xhr.readyState === xhr.DONE) {
              if (this.resolveStatus.includes(xhr.status)) {
                resolve({
                  status: xhr.status,
                  data: this.dataParser(xhr),
                });
              } else {
                reject({ status: xhr.status, data: xhr.response });
              }
            }
          } catch (error) {
            reject(error);
          }
        };

👉 바로 밑에 요청의 상태가 변할때 마다 실행할 함수를 등록합니다.
👉 constructor, this.resolveStatus에 요청 성공의 status들을 넣어줍니다.
👉 요청이 성공했을 때 데이터를 resolve해줍니다.
dataParser는 응답의 Content-Type이 json이면 Json.parse까지 합니다.🎉

콜백을 try catch로 감싼 이유는, 에러 응답 이 외의 무응답, 네트워크 문제 등의 핸들링 되지 않은 에러들을 잡기 위함입니다.

send

...
  bodyParser(data) {
    if (!(data instanceof FormData) && typeof data === "object") {
      return JSON.stringify(data);
    }

    return data;
  }
...
        xhr.send(this.bodyParser(config.body));
...

👉 다음 send를 해줍니다.
bodyParser는 body의 담긴 데이터를 변환합니다.

get, post

...
  get(url, headers) {
    return this.send({
      method: "GET",
      url,
      headers,
    });
  }

  post(url, body, headers) {
    return this.send({
      method: "POST",
      url,
      body,
      headers,
    });
  }
...

이렇게 만든 send를 이용해 get, post 메서드를 만들어줍니다.

확인

    (async () => {
      const req = new XHR("http://httpbin.org");

      const res1 = await req.get("/get");

      const res2 = await req.post("/post",{ test: "test" });

      console.log(res1, res2);
    })();

console로 확인을 해보면 위와같이 잘 나오는 것을 볼 수 있습니다.👏👏👏

Code

class XHR {
  constructor(url) {
    this.url = url;
    this.defaultHeader = {
      "Content-Type": "application/json",
      "Cache-Control": "no-cache",
      accept: "*",
    };
    this.resolveStatus = [200];
  }

  bodyParser(data) {
    if (!(data instanceof FormData) && typeof data === "object") {
      return JSON.stringify(data);
    }

    return data;
  }

  dataParser(xhr) {
    const contentType = xhr.getResponseHeader("Content-Type");
    const data = xhr.response;

    if (contentType && contentType.indexOf("json") !== -1) {
      return JSON.parse(data);
    } else {
      return data;
    }
  }

  send(config) {
    return new Promise((resolve, reject) => {
      try {
        const xhr = new XMLHttpRequest();
        if (!xhr) throw "XMLHttpRequest의 인스턴스를 만들 수 없습니다.";

        const mergedUrl = typeof config.url === "string" && config.url !== "/" ? this.url + config.url : this.url;
        
        xhr.open(config.method, mergedUrl);

        //헤더 설정
        Object.entries({ ...this.defaultHeader, ...config.headers }).forEach(
          ([key, value]) => {
            xhr.setRequestHeader(key, value);
          }
        );

        // 상태 변경 실행 함수
        xhr.onreadystatechange = () => {
          try {
            if (xhr.readyState === xhr.DONE) {
              if (this.resolveStatus.includes(xhr.status)) {
                resolve({
                  status: xhr.status,
                  data: this.dataParser(xhr),
                });
              } else {
                reject({ status: xhr.status, data: xhr.response });
              }
            }
          } catch (error) {
            reject(error);
          }
        };

        xhr.send(this.bodyParser(config.body));
      } catch (error) {
        reject(error);
      }
    });
  }

  get(url, headers) {
    return this.send({
      method: "GET",
      url,
      headers,
    });
  }

  post(url, body, headers) {
    return this.send({
      method: "POST",
      url,
      body,
      headers,
    });
  }
}

아래 코드는 TypeScript입니다.

interface sendConfig {
  method: "GET" | "get" | "POST" | "post";
  url?: string;
  body?: any;
  headers?: any;
}

class TsReq {
  readonly url: string;
  defaultHeader: any;
  resolveStatus: number[];

  constructor(url: string) {
    this.url = url;
    this.defaultHeader = {
      "Content-Type": "application/json",
      "Cache-Control": "no-cache",
      accept: "*",
    };
    this.resolveStatus = [200];
  }

  bodyParser<T>(data: T): T | string {
    if (!(data instanceof FormData) && typeof data === "object") {
      return JSON.stringify(data);
    }

    return data;
  }

  dataParser(xhr: XMLHttpRequest): any {
    const contentType = xhr.getResponseHeader("Content-Type");
    const data = xhr.response;

    if (contentType && contentType.indexOf("json") !== -1) {
      return JSON.parse(data);
    } else {
      return data;
    }
  }

  send(config: sendConfig): Promise<any> {
    return new Promise((resolve: any, reject: any) => {
      try {
        const xhr = new XMLHttpRequest();
        if (!xhr)
          return reject("XMLHttpRequest의 인스턴스를 만들 수 없습니다.");

        const mergedUrl =
          typeof config.url === "string" && config.url !== "/"
            ? this.url + config.url
            : this.url;

        xhr.open(config.method, mergedUrl);

        //헤더 설정
        Object.entries({ ...this.defaultHeader, ...config.headers }).forEach(
          ([key, value]: [string, any]) => {
            xhr.setRequestHeader(key, value);
          }
        );

        // 상태 변경 실행 함수
        xhr.onreadystatechange = (): void => {
          try {
            if (xhr.readyState === xhr.DONE) {
              if (this.resolveStatus.includes(xhr.status)) {
                resolve({
                  status: xhr.status,
                  data: this.dataParser(xhr),
                });
              } else {
                reject({ status: xhr.status, data: xhr.response });
              }
            }
          } catch (error) {
            reject(error);
          }
        };

        xhr.send(this.bodyParser(config.body));
      } catch (error) {
        reject(error);
      }
    });
  }

  get(url?: string, headers?: any) {
    return this.send({
      method: "GET",
      url,
      headers,
    });
  }

  post(url?: string, body?: any, headers?: any) {
    return this.send({
      method: "POST",
      url,
      body,
      headers,
    });
  }
}
export default TsReq;

다음번엔 이렇게 만든 라이브러리를 npm에 배포하는 방법을 올려야겠어요.🥰
전체 코드는 여기 Git 에서 확인하실 수 있습니다.
이 코드는 Gitnpm에서 업데이트됩니다. 🔍

onreadystatechange를 open전에 호출하면 readyStatus 0과 1이 반환되지 않습니다.

profile
욕심 많은 개발자🚀 코딩과 청주🥃 를 좋아합니다.

1개의 댓글

comment-user-thumbnail
2022년 2월 22일

감사합니다. 많은 도움이 되었습니다.

답글 달기