[JS] Ajax란? + XMLHttpRequest, fetch API, axios

Ajax란? + XMLHttpRequest, fetch API, axios 개념과 사용

image

AJAX

AJAX(Asynchronous JavaScript And XML)

AJAX는 자바스크립트를 사용해 비동기적으로 데이터를 받아와 동적으로 DOM을 갱신 및 조작하는 기법을 의미한다. 비동기적으로 동작하기 때문에 서버와 통신할 때 페이지를 새로고침(리로드)하지 않고도 데이터 변경, 페이지 업데이트가 가능하다.약자에 XML이 있는 이유는 예전에 데이터 포맷으로 XML을 많이 사용했기 때문이다.

XMLHttpRequest(XHR) 객체를 이용해 서버와 데이터를 주고 받는다. XML 뿐만 아니라 JSON, HTML, 텍스트 파일 등 다양한 포맷의 파일을 주고 받을 수 있다. HTTP 이외의 프로토콜도 지원한다.(file, ftp 등)

XMLHttpRequest 말고 fetch API를 사용할 수도 있다. (IE에선 지원하지 않는다.) fetch API는 ES2015(ES6)에서 표준이 되었고 Promise를 리턴한다.

XMLHttpRequest API

XMLHttpRequest을 통해 XML뿐만 아니라 JSON 데이터도 주고 받을 수 있다. (+ 이벤트를 기반으로 동작한다.) 코드를 통해 XMLHttpRequest API를 사용해보자.

HTTP 통신을 테스트하기 위해 https://reqres.in/ 에서 제공하는 API를 사용할 것이다. 여러가지 API 명세 중에 사용해볼 것은 다음 두 가지 이다.

  • GET: LIST USERS

  • POST: REGISTER - SUCCESSFUL & UNSUCCESSFUL

UNSUCCESSFUL한 경우는 에러처리를 할 때 테스트 해볼 것이다.

<!-- 📁 index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>http request & javascript</title>
    <link rel="stylesheet" href="" />
    <script src="xhr.js" defer></script>
  </head>
  <body>
    <section id="control-center">
      <button id="get-btn">GET Data</button>
      <button id="post-btn">POST Data</button>
    </section>
  </body>
</html>
  • index.html 실행 결과

// 📁 xhr.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

// GET Data 버튼을 누르면 처리되는 getData() 함수
const getData = () => {
  const xhr = new XMLHttpRequest(); // XMLHttpRequest 객체 생성
  xhr.open("GET", "https://reqres.in/api/users"); // 요청 보낼 준비 (*prepare* request)

  xhr.responseType = "json";

  // use response
  xhr.onload = () => {
    // console.log(xhr.response); // console에 response가 출력됨

    // // convert data from json to real js data
    // const data = JSON.parse(xhr.response);
    // console.log(data);

    // xhr.responseType='json'을 설정해주면 위처럼 JSON.parse를 사용해 변환할 필요가 없다.
    const data = xhr.response;
    console.log(data);
  };

  xhr.send(); // 서버로 요청을 보냄(send request to server)
};

// POST Data 버튼을 클릭하면 호출되는 sendData() 함수
const sendData = () => {
  // ...
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

먼저 GET 요청을 처리하는 getData() 함수를 작성했다. 실행 결과, 요청이 성공적으로 처리된다.

브라우저에서 GET Data 버튼을 누르면 요청이 이루어지고 개발자 도구(F12)의 Network 탭에서 request(req 헤더 내용 포함)와 response(Preview 탭에서 확인 가능) 내용을 확인할 수 있다.

이제 POST 요청을 처리하는 sendData() 함수를 만들어볼건데, getData() 함수와 상당 부분 내용이 중복되므로 재사용할 수 있는 함수를 만들 것이다. 그리고 비동기 작업을 직관적으로 처리하기 위해 비동기 코드 부분을 Promise로 래핑해준다.

// 📁 xhr.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const sendHttpRequest = (method, url, data) => {
  // ➕ POST 요청도 처리하기 위해 인자에 data를 추가한다.
  const promise = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);

    xhr.responseType = "json";

    // ➕ 보내는 데이터가 json이라고 헤더에 명시해줘야 한다.(post 요청 고려)
    if (data) {
      xhr.setRequestHeader("Content-Type", "application/json");
    }

    xhr.onload = () => {
      resolve(xhr.response);
    };

    xhr.send(JSON.stringify(data));
  });
  return promise;
};

const getData = () => {
  sendHttpRequest("GET", "https://reqres.in/api/users").then((responseData) =>
    console.log(responseData)
  );
};

const sendData = () => {
  sendHttpRequest("POST", "https://reqres.in/api/register", {
    email: "eve.holt@reqres.in",
    password: "pistol",
  }).then((responseData) => console.log(responseData));
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

POST 요청도 잘 실행된다. (유의: reqres.in 홈페이지의 api/register API에 있는 email, password값을 넣어야 정상작동한다.)

에러 처리도 해주자.

// 📁 xhr.js
const getBtn = document.getElementById('get-btn');
const postBtn = document.getElementById('post-btn');

const sendHttpRequest = (method, url, data) => {
  const promise = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);

    xhr.responseType = 'json';

    if (data) {
      xhr.setRequestHeader('Content-Type', 'application/json');
    }

    xhr.onload = () => {
      resolve(xhr.response);
    };

    // ➕ 에러 핸들링
    xhr.onerror = () => {
      reject('Something went wrong!');
    };

    xhr.send(JSON.stringify(data));
  });
  return promise;
};

const getData = () => {
  sendHttpRequest('GET', 'https://reqres.in/api/users')
    .then((responseData) => console.log(responseData))
    .catch((err) => console.error(err));
    );
};

const sendData = () => {
  sendHttpRequest('POST', 'https://reqres.in/api/register', {
    email: 'eve.holt@reqres.in',
    //   password: 'pistol' // ➕ 에러 핸들링을 테스트해보기 위해 주석처리
  })
  .then(responseData => console.log(responseData))
    .catch(err => console.error(err));
};

getBtn.addEventListener('click', getData);
postBtn.addEventListener('click', sendData);

그런데 이 상태에서 POST 요청을 잘못 보내면 (예를 들어 email, password를 모두 body에 넣어야 하는데 password를 빼먹은 경우) 에러가 sendData()함수의 catch가 아닌 (첫번째) then에서 잡혀버린다.

이렇게 요청의 body가 잘못 됐거나 데이터 요청에서 오류가 난 경우를 xhr.onload 내에서 처리해줘야한다. 그래야 catch에서 에러가 잡힌다. (xhr.error 라인은 네트워크 통신 실패 등의 에러를 캐치함) 다음과 같이 코드를 수정한다.

// 📁 xhr.js
const getBtn = document.getElementById('get-btn');
const postBtn = document.getElementById('post-btn');

const sendHttpRequest = (method, url, data) => {
  const promise = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);

    xhr.responseType = 'json';

    if (data) {
      xhr.setRequestHeader('Content-Type', 'application/json');
    }

    xhr.onload = () => {
        // ➕ 이렇게 수정
        if(xhr.status >= 400) reject(xhr.response);
        else resolve(xhr.response);
    };

    xhr.onerror = () => {
      reject('Something went wrong!');
    };

    xhr.send(JSON.stringify(data));
  });
  return promise;
};

const getData = () => {
  sendHttpRequest('GET', 'https://reqres.in/api/users')
    .then((responseData) => console.log(responseData))
    .catch((err) => console.error(err));
    );
};

const sendData = () => {
  sendHttpRequest('POST', 'https://reqres.in/api/register', {
    email: 'eve.holt@reqres.in',
    //   password: 'pistol' // ➕ 에러 핸들링을 테스트해보기 위해 주석처리
  })
  .then(responseData => console.log(responseData))
    .catch(err => console.error(err));
};

getBtn.addEventListener('click', getData);
postBtn.addEventListener('click', sendData);

실행 결과이다. console 옆에 찍힌 라인번호를 확인해보자. 발생한 에러가 catch에서 잘 잡히는 것을 볼 수 있다.

fetch API

  • AJAX를 구현할 수 있는 방법 중 최신 방법(XHR을 보완)
  • Promise를 기반으로 하기 때문에 로직을 추가하고 처리하기 쉽다. fetch API의 리턴 타입은 Promise 객체이다.
  • async/await을 사용하여 비동기 코드를 간결하게 작성할 수도 있다.
  • 기본적으로 쿠키를 전송하는 XHR와 달리, 쿠키를 전송하지 않는다. 하지만 자격증명(credentials) 옵션을 통해 전달할 수 있다.
  • 404: Page Not Found, 500: Internal Server Error HTTP 에러를 Promise.reject로 잡아낼 수 없다. 네트워크 장애나 요청이 완료되지 못한 경우, CORS가 잘못 설정된 경우 같은 에러만 reject로 잡아낼 수 있다.
  • IE나 구형 브라우저에서 지원하지 않는다.

간단한 fetch 예시

fetch("http://example.com/movies.json")
  .then((response) => response.json())
  .then((data) => console.log(data));

위 코드는 JSON 파일을 네트워크로부터 받아와 콘솔에 출력하는 코드이다. fetch의 첫번째 인자에 자원의 경로를 주면 된다. fetch는 response를 담은 Promise 객체를 반환한다.

사실 response는 HTTP response이고 그 자체로 JSON은 아니다. 따라서 Response.json() 메소드를 통해 response의 body에서 JSON을 추출한다.

request 옵션을 적용한 fetch 예제도 봐보자.

// Example POST method implementation:
async function postData(url = "", data = {}) {
  // Default options are marked with *
  const response = await fetch(url, {
    method: "POST", // *GET, POST, PUT, DELETE, etc.
    mode: "cors", // no-cors, *cors, same-origin
    cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
    credentials: "same-origin", // include, *same-origin, omit
    headers: {
      "Content-Type": "application/json",
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: "follow", // manual, *follow, error
    referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data), // body data type must match "Content-Type" header
  });
  return response.json(); // parses JSON response into native JavaScript objects
}

postData("https://example.com/answer", { answer: 42 }).then((data) => {
  console.log(data); // JSON data parsed by `data.json()` call
});

위에서 언급했듯이 fetch()는 상태 코드 404 에러로 요청에 실패한 경우에도 Response 객체를 반환한다. 즉 요청이 실패하는 경우를 catch 메소드만으로 처리할 수 없기 때문에 아래 방법을 쓴다.

fetch()가 성공적으로 이루어졌는지 체크하기 위해 promise가 resolve 됐는지 체크하고 Response.ok 속성이 true인지 체크한다. 아래 예제를 봐보자.

fetch("flowers.jpg")
  .then((response) => {
    if (!response.ok) {
      throw new Error("Network response was not ok");
    }
    return response.blob();
  })
  .then((myBlob) => {
    myImage.src = URL.createObjectURL(myBlob);
  })
  .catch((error) => {
    console.error("There has been a problem with your fetch operation:", error);
  });
fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then((data) => {
    if (!data.ok) {
      throw new Error(data.status);
    }
    return data.json();
  })
  .then((post) => {
    console.log(post.title);
  })
  .catch((error) => {
    console.log(error);
  });

Response 객체

  • Response 객체는 우리가 보통 필요로 하는 body(본문, 데이터)외에도 상태 코드, 헤더 등 다양한 정보를 가지고 있다. 따라서 response.json()과 같이 우리가 원하는 형태로 Response 객체를 변환하여 사용한다.
  • Response 객체는 fetch()의 promise가 resolve될 때 반환된다.

  • Response.status: HTTP 상태 (응답) 코드를 포함한 정수(default: 200)
  • Response.statusText: HTTP 상태 코드 메시지를 나타내는 문자열(default: “”)
  • Response.ok: 200 ~ 299를 포함하는 상태코드인지 확인하여 Boolean을 반환한다.

Body

Request, Response 모두 body 데이터를 포함할 수 있다. body 객체에는 다음과 같은 타입들이 담길 수 있다.

  • ArrayBuffer
  • ArrayBufferView
  • Blob/File
  • String
  • URLSearchParams
  • FormData

또한 Request, Response는 body로부터 데이터를 추출하는 인터페이스를 제공한다. 아래 인터페이스들은 모두 프로미스를 반환한다.

  • Request.arrayBuffer() / Response.arrayBuffer()
  • Request.blob() / Response.blob()
  • Request.formData() / Response.formData()
  • Request.json() / Response.json()
  • Request.text() / Response.text()

fetch 예제

XMLHttpRequest API를 공부할 때 작성했던 예제와 같은 기능을 구현할 것이다. index.html의 내용은 같고 API 테스트를 위해 https://reqres.in 사이트를 이용하는 것도 동일하다.

<!-- 📁 index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>http request & javascript</title>
    <link rel="stylesheet" href="" />
    ript> -->
    <script src="fetch.js" defer></script>
  </head>
  <body>
    <section id="control-center">
      <button id="get-btn">GET Data</button>
      <button id="post-btn">POST Data</button>
    </section>
  </body>
</html>
// 📁 fetch.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const getData = () => {
  fetch("https://reqres.in/api/users").then((response) =>
    console.log(response)
  );

  // 이렇게 옵션이 없으면 기본적으로 GET 요청을 보낸다.
  // fetch는 response를 담은 Promise 객체를 리턴한다.
  // (body: ReadableStream) 그런데 이렇게만 해서는 우리가 원하는 데이터 스냅샷을 볼 수 없다.(response 객체에 들어있는 것이 모두 출력됨)
};

const sendData = () => {
  // ...
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

  • Response.json() 사용
// 📁 fetch.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const getData = () => {
  fetch("https://reqres.in/api/users") // 1) get data
    .then((response) => {
      // 2) extract data
      return response.json(); // Response 객체의 data stream body을 > snapshot > json > javascript data type 으로 변환해준다. // Response.json() 도 Promise를 반환한다.
    })
    .then((responseData) => {
      console.log(responseData); // 3) use it
    });
};

const sendData = () => {
  // ...
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

Response 객체에 들어있던 (우리가 원하는) 데이터가 자바스크립트 데이터 형식으로 잘 출력되는 것을 볼 수 있다.

  • GET, POST 사용을 위한 함수 유틸화
// 📁 fetch.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const sendHttpRequest = (method, url, data) => {
  return fetch(url, {
    method: method,
    body: JSON.stringify(data),
    headers: data ? { "Content-Type": "application/json" } : {}, // data 가 falsy한 값일 경우 그냥 어떤 헤더도 추가하지 않겠다는 뜻
  }).then((response) => {
    return response.json();
  });
};

const getData = () => {
  sendHttpRequest("GET", "https://reqres.in/api/users").then((responseData) => {
    console.log(responseData);
  });
};

const sendData = () => {
  sendHttpRequest("POST", "https://reqres.in/api/register", {
    email: "eve.holt@reqres.in",
    password: "pistol",
  }).then((responseData) => {
    console.log(responseData);
  });
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);
  • 에러 처리 적용하기

지금까지 짠 코드에서 POST 요청을 잘못 보내보자. 에러가 발생하지만 그 에러가 sendData의 첫번째 then이 나타내는 success 블록에서 잡혀버린다. catch를 통해 에러를 처리하자.

// 📁 fetch.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const sendHttpRequest = (method, url, data) => {
  return fetch(url, {
    method: method,
    body: JSON.stringify(data),
    headers: data ? { "Content-Type": "application/json" } : {}, // data 가 falsy한 값일 경우 그냥 어떤 헤더도 추가하지 않겠다는 뜻
  }).then((response) => {
    return response.json();
  });
};

const getData = () => {
  sendHttpRequest("GET", "https://reqres.in/api/users").then((responseData) => {
    console.log(responseData);
  });
};

const sendData = () => {
  sendHttpRequest("POST", "https://reqres.in/api/register", {
    email: "eve.holt@reqres.in",
    // password: 'pistol',
  })
    .then((responseData) => {
      console.log(responseData);
    })
    .catch((err) => {
      console.error(err);
    });
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

하지만 catch 코드를 작성해도 여전히 에러가 then의 success 블록에서 잡힌다.

이는 네트워크 통신 장애나 테크니컬한 에러가 아니면 상태 코드(status code)가 40x, 50x인 에러들은 fetch가 캐치를 못하고 정상적으로 응답을 반환하기 때문이다. sendHttpRequest()내에 응답을 처음 받아오는 부분에서 이를 해결해보자.

// 📁 fetch.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const sendHttpRequest = (method, url, data) => {
  return fetch(url, {
    method: method,
    body: JSON.stringify(data),
    headers: data ? { "Content-Type": "application/json" } : {},
  }).then((response) => {
    // ✏️ 40x, 50x 에러 처리를 위한 if문
    if (response.status >= 400) {
      // !response.ok를 사용해도 된다.
      // throw new Error('Something went wrong!');// 여기서 에러를 던지게 되면 reject되는 것이고 다음 promise chain에서 catch로 가게될 것이다.

      // ⭐
      // 그런데, 이게 무슨 에러인지 커스텀 에러에 Response의 에러 내용을 담고 싶다면 어떻게 해야할까? 여기선 중첩 promise를 사용해 해결했다.
      return response.json().then((errResData) => {
        // 에러가 담긴 Response 객체를 스냅샷으로 변환(.json())
        const error = new Error("Something went wrong!"); // 커스텀 에러
        error.data = errResData;
        throw error;
      });
    }
    return response.json();
  });
};

const getData = () => {
  sendHttpRequest("GET", "https://reqres.in/api/users").then((responseData) => {
    console.log(responseData);
  });
};

const sendData = () => {
  sendHttpRequest("POST", "https://reqres.in/api/register", {
    email: "eve.holt@reqres.in",
    // password: 'pistol' // ✏️ 에러 테스트를 위해 주석 처리
  })
    .then((responseData) => {
      console.log(responseData);
    })
    .catch((err) => {
      console.error(err, err.data); // ✏️ 에러의 data 속성도 출력해준다.
    });
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

에러가 catch문에서 잘 잡히고 Response로 부터 가져온 에러 내용도 잘 출력되는 것을 볼 수 있다.

axios

  • axios는 자바스크립트 서드 파티(Third Party) 라이브러리이다.
  • Promise API를 지원한다.
  • 브라우저로부터 XMLHttpRequests 를 만든다. > 구형 브라우저에서도 사용 가능하다.
  • Node.js로부터 http request를 만든다.
  • 사용하기 쉽다.

서드 파티(Thrid Party) 라이브러리

제작사에서 만든 것이 아니라 개인 개발자, 프로젝트 팀 등이 만든 플러그인이나 라이브러리를 의미한다.

node.js 환경이 아니므로 우린 CDN을 이용해 axios를 사용해보자.

<!-- 📁 index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>http request & javascript</title>
    <link rel="stylesheet" href="" />
    <!-- axios 사용을 위해 CDN 넣음 -->
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="axios.js" defer></script>
  </head>
  <body>
    <section id="control-center">
      <button id="get-btn">GET Data</button>
      <button id="post-btn">POST Data</button>
    </section>
  </body>
</html>
// 📁 axios.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const getData = () => {
  // index.html에서 CDN으로 axios를 가져왔음. global하기 때문에 이렇게 axios 사용 가능함
  axios.get("https://reqres.in/api/users").then((response) => {
    console.log(response);
  });
};

const sendData = () => {
  // ...
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

GET 요청을 먼저 작성해봤다. response를 출력해보면 fetch API를 사용했을 때와 달리 우리가 원하는 데이터가 stream이 아닌 이미 스냅샷(javascript 객체)으로 담겨있는 것을 볼 수 있다. fetch API에서는 데이터를 javascript 객체로 변환하기 위해 Response.json()을 썼지만 axios를 쓰면 자동으로 변환되기 때문에 별도의 변환 작업이 필요없다.

// 📁 axios.js
const getBtn = document.getElementById("get-btn");
const postBtn = document.getElementById("post-btn");

const getData = () => {
  axios.get("https://reqres.in/api/users").then((response) => {
    console.log(response);
  });
};

const sendData = () => {
  axios
    .post("https://reqres.in/api/register", {
      email: "eve.holt@reqres.in",
      password: "pistol", // ✏️
    })
    .then((response) => {
      console.log(response);
    })
    .catch((err) => {
      console.error(err, err.response);
    });
};

getBtn.addEventListener("click", getData);
postBtn.addEventListener("click", sendData);

POST 요청을 할 때는 post() 메소드의 두번째 인자에 body에 들어갈 데이터를 전달한다. 이 데이터는 요청을 하면서 자동적으로 (javascript 객체가) json으로 변환되기 때문에 fetchAPI에서 썼던 JSON.stringify 같은 작업을 할 필요가 없다.

요청을 보내보면 axios에 의해 요청에 content-type: application/json도 자동적으로 추가된 것을 볼 수 있다. (개발자 도구의 network 탭에서 확인하기)

또한 fetch API 예제 때와 달리, 40x, 50x 이런 status 에러가 catch에서 자동으로 잘 잡히는 것을 볼 수 있다.(fetchAPI와의 차이점)

(에러를 테스트할 때는 ✏️표시된 라인의 코드를 주석처리하고 실행해보면 된다.)

References