[JS] 비동기 자바스크립트 - Callback, Promise, Async/Await

29855 단어 jsjs

비동기 작업은 동기 작업과 달리 순서가 보장되지 않는다. 하지만 비동기를 써야 하는데 순서대로 실행하는 것이 필요한 경우도 있을 수 있다. 예를 들어 네트워크에 요청을 보낸 후 받은 값으로 또 다른 요청을 한다고 하면 첫 번째 요청이 먼저 실행된 후에 두 번째 요청이 실행되어야 한다. 이런 상황을 자바스크립트에서는 콜백, 프로미스, async/await으로 다룰 수 있다.

cf 비동기 - 자바스크립트에서는 어떻게 비동기 작업을 처리할까?

Callback

  • 콜백 함수란 다른 함수에 인자로 전달되어 그 함수의 내부에서 특정 시점에 호출되는 함수를 말한다.
  • 자바스크립트 함수는 일급객체이기 때문에 함수를 인자로 받을 수 있다.
  • 콜백은 방식에 따라 동기적/비동기적 콜백으로 나눌 수 있다.
function func1(func) {
  console.log('1');
  func();
}
function func2(func) {
  setTimeout(function () {
    console.log('2');
  }, 0); // Asynchronous Callback
  func();
}
function func3() {
  console.log('3');
}
func1(func3);
// '1'
// '3'
// undefined
func2(func3); 
// '3'
// undefined
// '2'

// setTimeout 안에 있는 콜백함수는 비동기적으로 처리된다.
// 따라서 func2의 실행이 다 끝나고 call stack이 빈 다음에야 
// event loop에 의해 다시 call stack으로 돌아와 처리되기 때문에 
// console창에 가장 나중에 찍힌 것이다.

Callback Hell

  • 비동기 처리를 위한 콜백이 처리 순서를 보장하기 위해 중첩되면서 복잡도가 높아지는 현상. 가독성이 떨어지고 오류를 찾기 어려워진다.
  • ES6부터 Promise가 도입되어 콜백을 중첩하는 대신 then을 체이닝하여 순차적으로 비동기 처리가 가능해졌다. ES7에서는 여기에 async/await이 추가되어 비동기 작업을 마치 동기인 것처럼 코드를 작성하여 Promise를 더욱 직관적으로 사용할 수 있게 되었다.

Promise

  • 프로미스는 이후 시점 언젠가에 완료되거나 실패할 비동기 작업을 대리 표현하는 객체이다.
  • 동기도 가능하지만 비동기에 특화되어 있다.
  • 프로미스는 pending, fulfilled, rejected 세 가지 중 하나의 상태를 가진다.
  • Promise 객체를 new 키워드로 인스턴스화 하여 사용한다.
  • 프로미스 실행 함수는 인자로 resolve, reject 함수를 받는다.
	new Promise((resolve ,reject) => {
          if (err) {
              reject(err)
          } else {
              resolve(result)
          }
    	})
  • 프로미스가 이행되면 이행된 값은 then의 콜백함수의 첫번째 인자로, 거부되면 거부된 이유를 then의 콜백함수의 두번째 인자 또는 catch의 콜백함수의 첫번째 인자로 받을 수 있다.
  • 프로미스의 끝에 가급적 catch를 붙여 발생할 수 있는 에러를 처리한다.

Await/Async

  • ES7에서 도입
  • Promise 위에서 syntactic sugar로 작용하며 비동기 작업을 마치 동기 작업인 것 처럼 작성할 수 있도록 해 준다.
  • 비동기 작업을 핸들링하는 함수는 반드시 async 키워드로 표시해야 한다. async 함수는 항상 Promise를 반환한다.
  • awaitasync 표시 된 함수 안에서만 사용할 수 있다.
  • await 키워드를 만나면 Promise가 resolve되거나 reject될 때까지 함수 실행이 멈춘다.
  • await은 주의해서 사용해야 하며 비동기 작업이 아주 많이 필요한 경우 Promise.all()이 더 적당하다.

Promise.all()

  • 프로미스들을 담은 배열을 인자로 받는다. 프로미스를 반환하며 반환된 프로미스는 인자로 받은 배열의 모든 프로미스가 이행되었거나, 혹은 프로미스가 주어지지 않았을 때 이행된다. 주어진 프로미스들 중 하나라도 거부되면 거부된다.
  • 반환된 프로미스의 이행 값은 배열 형태며 인자로 받은 배열의 프로미스들의 이행 값이 차례로 담겨 있다.
  • 비동기 작업 A가 끝나고 그 값을 받아 B를 진행해야하는 경우 체이닝, A, B가 순서 상관없이 동시에 처리되는 경우 Promise.all()을 사용한다. 반드시 그래야 하는 것은 아니고 상황에 따라 적절하게 사용한다.

AJAX

  • AJAX (Asynchronous Javascript And XML)
  • 정보 교환 기법의 하나로, 페이지 로드가 끝난 후 경우에 따라 필요한 특정 데이터만 가져와 변경하기 위해 비동기적으로 정보를 요청하기 위해 만들어졌다.
  • 만들어질 당시와 달리 현재는 XML보다 JSON이 더 많이 사용되지만 원래 이름대로 AJAJ가 아닌 AJAX라고 불린다.

XMLHttpRequest (by Callback)

  • ES6 이전의 오래된 AJAX 구현 방법
  • Promise를 지원하지 않기 때문에 콜백만 사용할 수 있다.

Fetch (by Promise)

  • ES6부터 도입된 방법으로 AJAX를 쉽게 하용하게 해주는 방법
  • fetch는 프로미스를 반환하며 이 프로미스가 resolve된 다음 연결된 then에서 resolve된 값을 다룰 수 있다. (reject되면 catch에서 reject된 이유를 다룰 수 있다.)

Async/Await (by Promise)

  • ES7에서 도입
  • Promise 위에서 syntactic sugar로 작용하며 비동기 작업을 마치 동기 작업인 것 처럼 작성할 수 있도록 해 준다.
  • 비동기 작업을 핸들링하는 함수는 반드시 async 키워드로 표시해야 한다. async 함수는 항상 Promise를 반환한다.
  • awaitasync 표시 된 함수 안에서만 사용할 수 있다.
  • await 키워드를 만나면 프로미스가 resolve되거나 reject될 때까지 함수 실행이 멈춘다.

AJAX 구현 방법 비교

구현하고자 하는 것 :
  1. swapi에 등장인물에 대한 정보를 랜덤하게 요청
  2. 받은 등장인물의 이름을 콘솔창에 출력
  3. swapi에 받은 등장인물이 등장하는 영화들 중 하나에 대한 정보를 요청
  4. 받은 영화의 제목을 콘솔창에 출력

첫 번째 요청이 완료되어야 두 번째 요청이 발생할 수 있다.

XMLHttpRequest로 구현
// 랜덤한 등장인물에 대한 정보를 요청할 URL 구하기
const randNum = Math.floor(Math.random() * 82) + 1;
const peopleURL = `https://swapi.dev/api/people/${randNum}`;
// 랜덤한 등장인물에 대한 정보 요청
const peopleReq = new XMLHttpRequest();
peopleReq.addEventListener("load", function () {
  console.log("First request loaded");
  const data = JSON.parse(this.responseText);
  // 받은 등장인물의 이름 콘솔창에 출력
  console.log("Character Name : " + data.name);
  const filmURL = data.films[0];
  // 받은 등장인물이 등장하는 영화들 중 하나에 대한 정보 요청
  const filmReq = new XMLHttpRequest();
  filmReq.addEventListener("load", function () {
    console.log("Second request loaded");
    const data = JSON.parse(this.responseText);
    // 받은 영화의 제목 콘솔창에 출력
    console.log("Appearing In : " + data.title);
  });
  // 영화 정보 요청 오류 처리 
  filmReq.onerror = function (err) {
    console.log("Error : ", err);
  };
  // 영화 정보 요청 보내기
  filmReq.open("GET", filmURL);
  filmReq.send();
});
// 등장인물 정보 요청 오류 처리
peopleReq.onerror = function (err) {
  console.log("Error : ", err);
};
// 등장인물 정보 요청 보내기
peopleReq.open("GET", peopleURL);
peopleReq.send();
Fetch로 구현
// 랜덤한 등장인물에 대한 정보를 요청할 URL 구하기
const randNum = Math.floor(Math.random() * 82) + 1;
const peopleURL = `https://swapi.dev/api/people/${randNum}`;
// 랜덤한 등장인물에 대한 정보 요청
fetch(peopleURL)
  .then((response) => response.json())
  .then((data) => {
    console.log("First request loaded");
  	// 받은 등장인물의 이름 콘솔창에 출력
    console.log("Character Name : " + data.name);
    const filmURL = data.films[0];
  	// 받은 등장인물이 등장하는 영화들 중 하나에 대한 정보 요청
    return fetch(filmURL)
      .then((response) => response.json())
      .then((data) => {
        console.log("Second request loaded");
      	// 받은 영화의 제목 콘솔창에 출력
        console.log("Appearing In : " + data.title);
      });
  })
	// 요청 오류 처리
  .catch((err) => {
    console.log("Error : ", err);
  });
Async/Await으로 구현
// 비동기 함수 getCharacterAndFilm 선언
async function getCharacterAndFilm() {
  // 랜덤한 등장인물에 대한 정보를 요청할 URL 구하기
  const randNum = Math.floor(Math.random() * 82) + 1;
  const peopleURL = `https://swapi.dev/api/people/${randNum}`;
  // 랜덤한 등장인물에 대한 정보 요청
  const filmURL = await fetch(peopleURL)
    .then((response) => response.json())
    .then((data) => {
      console.log("First request loaded");
      // 받은 등장인물의 이름 콘솔창에 출력
      console.log("Character Name : " + data.name);
      return data.films[0];
    })
  	// 요청 오류 처리
    .catch((err) => {
      console.log("Error : ", err);
    });
  
  // 받은 등장인물이 등장하는 영화들 중 하나에 대한 정보 요청
  fetch(filmURL)
    .then((response) => response.json())
    .then((data) => {
      console.log("Second request loaded");
    // 받은 영화의 제목 콘솔창에 출력
      console.log("Appearing In : " + data.title);
    })
  	// 요청 오류 처리
    .catch((err) => {
      console.log("Error : ", err);
    });
}
// getCharacterAndFilm 호출
getCharacterAndFilm();

좋은 웹페이지 즐겨찾기