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의 인스턴스를 생성하고 설정에 맞게 요청을 보내는 역할을 합니다.
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 메서드로 요청을초기화 합니다.
...
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 전에 호출해야 합니다.
...
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로 감싼 이유는, 에러 응답 이 외의 무응답, 네트워크 문제 등의 핸들링 되지 않은 에러들을 잡기 위함입니다.
...
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(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로 확인을 해보면 위와같이 잘 나오는 것을 볼 수 있습니다.👏👏👏
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 에서 확인하실 수 있습니다.
이 코드는 Git과 npm에서 업데이트됩니다. 🔍
onreadystatechange를 open전에 호출하면 readyStatus 0과 1이 반환되지 않습니다.
감사합니다. 많은 도움이 되었습니다.