📚 What is TIL?

AJAX가 없던 시절

과거에는 사용자와 상호작용하는 웹 페이지를 만들기 위해서는
HTML과 함께 폼을 이용해 데이터를 서버에 전송하고,
서버에서 처리된 결과를 다시 HTML로 받아와 화면에 출력하는 방식을 사용했다

🔎 따라서, 페이지 내용이 변경될 때 클라이언트에서 서버에 새로운 정보를 요청하고
 응답할 때마다 HTTP 프로토콜이 재설정되면서 페이지 전체가 갱신(새로고침)되었다
🔎 하지만 이 방식은 페이지가 다시 로드되어야 하기 때문에 사용자 경험과 성능면에서 한계가 있었다


<form action = "www.naver.com" type="get">
<button type="submit">

🔎 과거 클라이언트가 데이터를 서버에 요청하는 방법은
 브라우저 주소창에 특정 URL을 입력하거나,
 HTML 요소인 <a> 혹은 <form> 을 이용하는것이었다



AJAX

Asynchronous JavaScript And XML

서버와의 비동기 통신을 가능하게 하는 여러 기능들을 가진
XMLHttpRequest 객체를 활용한 AJAX 개념이 주목받으면서
더이상 페이지의 전체 갱신이 아닌 변경되는 부분의 데이터만
AJAX를 통해서 받아 갱신하면서 그 문제점을 해결할 수 있게 되었다

AJAX는 자바스크립트를 이용하여 웹 페이지 내에서 동적으로 데이터를 요청하고 받아오는 방식으로
웹 페이지에서 자바스크립트를 이용해 서버로 요청을 보내고,
서버는 요청에 대한 데이터를 JSON이나 XML 등의 형식으로 응답한다

💡 이렇게 자바스크립트는 비동기적으로 서버에 요청을 보낸 뒤 계속해서 다른 작업을 가능할 수 있게 되었다


XMLHttpRequest

function xhrRequest() {

// 1. XMLHttpRequest 객체 생성
const requestObj = new XMLHttpRequest();

// 3. 서버로 보낼 Ajax 요청의 형식을 설정
requestObj.open('GET', 'url'); // 요청을 초기화합니다. 통신방법과 요청을 발신할 대상의 주소를 전달합니다.

// 2. onreadystatechange 등록
requestObj.onreadystatechange = () => { 
  // readystate 속성 값이 변경될 때마다 호출되는 이벤트리스너 입니다.
  // 서버로 부터 응답이 오게 되어 XMLHttpRequest 객체의 값이 변하게 되면 이를 감지해 자동으로 호출되는 함수를 설정한다
  // readystate : 요청을 보내는 클라이언트의 상태를 의미합니다.
  // readystate의 종류
  // 0 (UNSENT) - XHR 객체가 생성되었지만 아직 초기화되지 않았습니다.
  // 1 (OPENED) - open()함수가 호출되어 요청이 초기화되었습니다.
  // 2 (HEADERS_RECEIVED) - send()함수가 호출되었습니다.
  // 3 (LOADING) - 데이터를 다운받는 중 입니다.
  // 4 (DONE) - 통신이 완료되었습니다.

  // XMLXttpRequest 객체의 현재 상태와 서버 상의 문서 존재 유무를 확인
  if (requestObj.readyState == 4 && requestObj.status == "200") {
  // status 속성은 요청에 대한 HTTP 응답 코드를 나타냅니다.

    // 서버에 요청하여 응답으로 받은 데이터를 문자열로 반환
    const result = requestObj.responseText;
    // responseText 프로퍼티 : 서버에 요청하여 응답으로 받은 데이터를 문자열로 반환
    // responseXML 프로퍼티 : 서버에 요청하여 응답으로 받은 데이터를 XML DOM 객체로 반환
  }
};

// 4. 작성된 Ajax 요청을 서버로 전달
requestObj.send(); // 서버로 요청을 보냅니다. send 메소드가 실행되어야만 우리가 위에서 설정한 내용들이 의미를 가지게 됩니다.
// send() - GET 방식
// send(문자열) - POST 방식

}

xhrRequest();

🔎 XMLHttpRequest 요청 보내는 순서를 간단하게 살펴보자

 1) XMLHttpRequest 객체 생성
 2) onreadystatechange 등록
 3) 서버로 보낼 Ajax 요청의 형식을 설정
 4) 작성된 Ajax 요청을 서버로 전달


function xhrRequest(callback) {
  const requestObj = new XMLHttpRequest();
  requestObj.open('GET', 'message.txt');
  
  requestObj.onreadystatechange = () => {
    if (requestObj.readyState == 4 && requestObj.status == "200") {
      const result = requestObj.responseText;
      // message.txt에서 가져온 내용을 이용하여 또 다른 데이터를 가공하는 과정
      anotherFunction(result, (data) => {
        // 또 다른 가공된 데이터를 이용하여 또 다른 작업을 수행하는 과정
        yetAnotherFunction(data, (finalResult) => {
          // 최종적으로 가공된 결과를 반환하는 함수
          callback(finalResult);
        });
      });
    }
  };
  
  requestObj.send();
}

// xhrRequest 함수가 콜백 함수를 받아서 사용하는 예시
xhrRequest((finalResult) => {
  console.log(finalResult);
});

⚠️ 비동기 처리의 문제점

 자바스크립트는 AJAX를 통해 API 서버를 호출할 때,
 서버로 요청을 보낸 후 응답을 기다리지 않고 다음 코드를 진행하기 때문에,
 응답이 아직 도착하지 않았는데도 다음 코드가 실행될 수 있다

🔎 이러한 문제를 해결하기 위해 비동기적 처리의 실행 순서를
 제어하기 위한 방법으로 콜백함수를 사용했다
 따라서, AJAX 요청에 대한 응답이 도착했을 때
 콜백함수가 실행되어 처리할 수 있도록 하여 원하는 기능을 수행할 수 있다

🔎 데이터를 받아와서 데이터를 재가공하는 과정에서
 여러가지 함수를 호출시켜야 하는 상황이 벌어질 수 있는데,
 콜백 함수를 중첩해서 사용하다보면 콜백 지옥(Callback Hell)이 펼쳐진다
 즉, 코드의 가독성과 유지보수성이 떨어지는 문제가 발생한다



Promise

이러한 콜백지옥과 같은 문제를 해결하기위해 Promise 라는 자바스크립트의 내장 객체가 등장했다
Promise는 비동기 처리를 실행하고 그 처리가 끝난 후 다음 처리를 실행하기 위한 기능을 제공한다

Promise는 생성자 함수를 호출할 때 함수 인자로 콜백함수를 선언할 수 있다
콜백 함수는 resolvereject 두 개의 함수를 인자로 받는데
resolve 메서드는 Promise 객체가 성공 상태가 될 때 호출되며,
reject 메서드는 Promise 객체가 실패 상태가 될 때 호출된다

cosnt a = new Promise(function(resolve, reject) {

});

🔎 프로미스를 생성하고 종료될 때 까지 총 3가지 상태를 갖는다

Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
Fulfiled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

💡 그럼 어느시점에서 어떤상태를 나타낼까?

 new Promise() 메서드를 호출하면 대기(pending) 상태가 된다
 Promise의 결과는 then 메서드와 catch 메소드로 받을 수 있는데
 Fulfilled 가 되었을 때 then()을 이용하면 처리 결과 값을 받을 수 있고
 Rejected가 되면 실패한 이유를 catch()를 통해서 받을 수 있다

 resolve 메서드가 호출되면, Promise 객체는 이행(Fulfilled) 상태가 되고 then 메서드가 자동으로 호출된다
 reject 메서드가 호출되거나 통신에 문제가 발생하면, Promise 객체는 실패(Rejected) 상태가 되고 catch 메서드가 자동으로 호출된다


function xhrRequest() {
  const requestObj = new XMLHttpRequest();
  requestObj.open('GET', 'message.txt');
  
  return new Promise((resolve, reject) => {
    requestObj.onreadystatechange = () => {
      if (requestObj.readyState == 4 && requestObj.status == "200") {
        const result = requestObj.responseText;
        // message.txt에서 가져온 내용을 이용하여 또 다른 데이터를 가공하는 과정
        anotherFunction(result)
          .then(data => yetAnotherFunction(data))
          .then(finalResult => resolve(finalResult))
          .catch(error => reject(error));
      }
    };
    
    requestObj.send();
  });
}

xhrRequest()
  .then(finalResult => console.log(finalResult))
  .catch(error => console.log(error));

🔎 위와 같이 then 과 catch 가 메서드 체이닝을 이룬것을 볼 수 있다
 then 메서드와 catch 메서드는 다른 Promise 객체를 반환하기 때문에
 메서드 체이닝을 통해 여러 개의 비동기 작업을 순차적으로 처리할 수 있다
 즉, 작업이 순차적으로 실행되는 것이 보장되기때문에 콜백지옥의 문제가 해결된다

🔎 하지만 여전히 XMLHttpRequest 는 사용방법이 복잡하고 가독성이 좋지않다는 단점이 존재했다
 또한 CORS(Cross-Origin Resource Sharing)를 처리하는 것도 복잡했다

💡 CORS(Cross-Origin Resource Sharing)란?

 CORS는 웹 브라우저에서 실행되는 JavaScript 코드가
 다른 출처(origin)의 리소스에 접근할 때 적용되는 보안 정책으로
 출처는 도메인, 프로토콜, 포트번호를 모두 포함한 것을 말한다

 예를 들어, https://www.example.com 도메인에서는
 Ajax 요청을 보내서 https://www.other.com 도메인의 데이터를 가져오려고 한다면,
 보안상의 이유로 브라우저는 이 요청을 차단한다
 이 때, CORS를 이용하여 https://www.other.com 도메인에서 Ajax 요청을 허용하도록 설정하면,
 https://www.example.com 도메인에서 데이터를 가져올 수 있다



fetch API

이에 대한 해결책으로 Promise 기반의 Web API인 fetch API가 등장했다
fetch API는 Promise를 기반으로 하여 HTTP 요청을 처리할 수 있도록 설계되어있어서
비동기 처리를 더욱 간단하고 직관적으로 작성할 수 있었고, 코드의 가독성 또한 더욱 높아질 수 있었다

뿐만아니라, fetch API는 JSON, Blob 등 다양한 데이터 타입을 처리할 수 있도록 지원했고
CORS 처리가 자동으로 이루어지기 때문에, 비교적 간단하게 다른 도메인 간에 요청을 보낼 수 있다

fetch(url[, options])
  .then(response => {
      .then(res => res.json())
      .then(json => console.log(json));
  })
  .catch(error => {
    // 오류 처리
  });

🔎 url은 요청을 보낼 URL 주소를 의미하며, options는 요청에 대한 옵션을 설정하는 객체로
 options 객체를 생략하면 GET 요청을 보내게 된다

🔎 fetch 함수는 응답 객체(Promise object Response)를 반환하며,
 then과 catch 메소드를 이용해서 비동기적으로 응답을 처리한다
 즉, 인자로 전달한 url에 접근해 데이터를 다운로드 받게 되고 다운로드가 완료되면 fulfilled 상태의 프로미스가 반환되어
then 메서드를 통해 처리 결과 값을 받을 수 있다

🔎 XMLHttpRequest 에서 responseXML 속성을 이용해서 데이터를 파싱하는 것과 같이
 fetch 에서는 json 메서드나 text 메서드를 이용해서 데이터를 파싱할 수 있다

💡 파싱을 해야 하는 이유?

 서버로부터 받아온 데이터를 Response 객체로 반환하는데
 파싱을 하지 않고 데이터를 그대로 사용하면, 데이터가 문자열 형태로 반환되어 데이터 사용이 어려울 수 있다
 Response 객체는 대부분 JSON 형식으로 된 데이터를 포함하고 있을텐데
 이 데이터를 사용하려면 자바스크립트 객체로 변환해야 사용할 수 있다


function fetchRequest() {
  return fetch('message.txt')
    .then(response => {
      if (response.ok) {
        return response.json();
      }
      throw new Error('Network response was not ok.');
    })
    .then(result => {
      return anotherFunction(result);
    })
    .then(data => {
      return yetAnotherFunction(data);
    })
    .catch(error => {
      console.error('There was a problem with the fetch operation:', error);
    });
}

fetchRequest()
  .then(finalResult => console.log(finalResult))
  .catch(error => console.log(error));

💡 콜백함수 보다 깔끔하지만 이역시도 코드가 복잡해지면 코드가 중첩되고 가독성이 떨어짐에따라 유지보수가 어렵다



async / await

이러한 문제를 해결하기 위해 Promise 로직을 더 쉽고 간결하게 사용할 수 있게 해주는 async / await 가 등장한다
비동기 함수를 동기식 코드처럼 작성할 수 있도록 해주는 문법적인 기능을 한다

⚠️ 여기서 알아둘 것은 async/await가 Promise를 대체하기 위한 기능이 아니라는 것이다
 Promise를 더 편리하게 사용하기 위한 기능으로 내부적으로 Promise와 동일한 방식으로 동작한다
 즉, 코드 작성 부분을 프로그래머가 유지보수하게 편하게 보이는 문법만 다르게 해줄 뿐이다

async function fetchRequest() {
  try {
    const response = await fetch('message.txt');
    if (response.ok) {
      const result = await response.json();
      const data = await anotherFunction(result);
      const finalResult = await yetAnotherFunction(data);
      return finalResult;
    } else {
      throw new Error('Network response was not ok.');
    }
  } catch (error) {
    console.error('There was a problem with the fetch operation:', error);
    throw error;
  }
}

fetchRequest()
  .then(finalResult => console.log(finalResult))
  .catch(error => console.log(error));

🔎 function 키워드 앞에 async 만 붙여주면 되고, 비동기로 처리되는 부분 앞에 await 만 붙여주면 된다
🔎 async 리턴값은 Promise 객체이고 Promise 객체는 then 메서드를 사용한 것과 동일한 결과를 반환한다
🔎 Promise 체인 대신 try-catch 구문으로 간단하게 작성할 수 있다


Reference

댓글남기기