728x90

자바스크립트 비동기 프로그래밍(Asynchronous Programming)

들어가며

  • 자바스크립트의 비동기 프로그래밍(Asynchronous Programming)에 대해 공부했던 내용을 정리해본다.

 

비동기 프로그래밍(Asynchronous Programming)

개념

  • 자바스크립트에서 서버와 통신을 하다 보면 어떤 자료를 요청하고 받는지에 따라, 또는 네트워크 속도에 따라 조금씩 처리 시간이 달라진다.
  • 그리고 시간 차이가 나는 처리 결과를 받아서 순서대로 처리해야 하는데, 이러한 처리 방식을 '비동기 처리 방식'이라고 한다.
  • 자바스크립트 프로그램은 많은 함수들이 모여서 하나의 기능을 만든다.
  • 그런데 이들 함수의 실행 시간이 서로 다르므로 특정 작업이 끝나면 다른 작업을 하고, 그 작업이 끝나면 이어서 또 다른 작업을 하도록 서로 연결해 주어야 한다.
  • 이때 이전 작업이 끝날 때까지 기다렸다가 다음 작업을 하든지, 또는 이전 작업을 시작해 놓고 다음 작업도 동시에 하는지에 따라 동기(Synchronous)비동기(Asynchronous)로 나뉜다.

 

동기 처리 방식(Synchronous Processing)

개념

  • 기본적으로 자바스크립트 프로그램은 코드가 작성된 순서대로 처리한다. (동기 처리 방식)
  • 동기 처리 방식은 '단일 스레드 방식', '싱글 스레드 방식'이라고 불린다.
  • 동기 처리 방식은 대기줄이 길어지면 처리 시간이 길어진다는 단점이 있다.

 

동기 처리 예

function displayA() {
  console.log("A");
}
function displayB() {
  console.log("B");
}
function displayC() {
  console.log("C");
}

displayA();
displayB();
displayC();

// 콘솔창에 A -> B -> C 순으로 표시된다.

 

  • 자바스크립트는 싱글 스레드를 사용하지만, 시간이 많이 걸리는 작업은 따로 처리해서 싱글 스레드의 단점을 보완한다. 
function displayA() {
  console.log("A");
}
function displayB() {
  setTimeout(() => console.log("B"), 2000); // 2초 뒤에 실행
}
function displayC() {
  console.log("C");
}

displayA();
displayB();
displayC();
// A -> C -> B 순으로 출력된다. (함수의 실행 시간에 따라 오래 걸리는 것은 별도로 처리하고, 실행이 끝났을 때 결과를 반환한다.)
// 자바스크립트는 싱글 스레드를 사용하지만, 시간이 많이 걸리는 작업은 따로 처리해서 싱글 스레드의 단점을 보완한다.

 

비동기 처리 방식(Asynchronous Processing)

개념

  • 프로그램에서는 여러 개의 함수를 작성하는데, 실행 시간이 다른 함수들을 원하는 처리 순서에 맞게 프로그래밍하는 것을 '비동기 처리'라고 한다.
  • 서버에서 자료를 가져와서 화면에 표시한다면 서버에서 자료를 가져올 때 아무리 많은 시간이 걸려도 자료를 가져오는 함수 다음에 화면에 표시하는 함수를 실행해야 한다.

 

비동기 처리 방식

  • 자바스크립트에서 비동기 방식으로 처리하면 다음과 같이 크게 3가지 방법을 사용할 수 있다.
비동기 방식 버전 기능
콜백 함수
(Callback Function)
기존부터 사용 함수 안에 또 다른 함수를 매개변수로 넘겨서 실행 순서 제어
(콜백 함수가 많으면 가독성이 떨어짐.)
프로미스
(Promise)
ECMAScript2015(ES6)부터 @Promise@ 객체와 콜백 함수를 사용해서 실행 순서 제어
async, await ECMAScript2017(ES8)부터 @async@ 함수와 @await@ 예약어를 사용해서 실행 순서 제어

 

 

비동기 처리 예 ① : 콜백 함수(Callback Function)

  • 콜백 함수(Callback Function)다른 함수의 매개변수로 사용하는 함수를 의미한다.
  • 자바스크립트는 오래 전부터 콜백 함수를 사용해서 비동기 처리를 구현해왔다.
function displayA() {
  console.log("A");
}
function displayB(callback) {
  setTimeout(() => {
    console.log("B");
    callback();
  }, 2000); // 2초 뒤 실행
}
function displayC() {
  console.log("C");
}

displayA();
displayB(displayC);

// A -> B -> C (A를 표시하고 잠시 기다렸다가 B와 C를 표시한다.)

 

  • 다음과 같이 @setTimeout()@을 이용하여 비동기 처리를 구현할 수 있다.
function order(coffee, callback) {
  // 커피 주문 (3초 기다린 후 표시)
  console.log(`${coffee} 주문 접수`);
  setTimeout(() => {
    callback(coffee);
  }, 3000);   // 3초 뒤 실행
}

function display(result) {
  // 커피 완료 표시
  console.log(`${result} 준비 완료`);
}

order("아메리카노", display);

// 아메리카노 주문 접수
// 아메리카노 준비 완료 (3초 후)

 

  • 하지만 다음과 같이 콜백 지옥(Callback Hell)을 맞닥뜨릴 수도 있다.
// A를 표시한 후 1초마다 B -> C -> D -> STOP! 순서로 표시하기
function displayLetter() {
  console.log("A");
  setTimeout(() => {
    console.log("B");
    setTimeout(() => {
      console.log("C");
      setTimeout(() => {
        console.log("D");
        setTimeout(() => {
          console.log("STOP!");
        }, 1000);
      }, 1000);
    }, 1000);
  }, 1000);
}

 

  • 이러한 이유로 ECMAScript2015(ES6)에서 프로미스(Promise)가 등장하였다.

 

비동기 처리 예 ② : 프로미스(Promise)

  • 프로미스(Promise)는 처리에 성공했을 때 실행할 콜백 함수와 성공하지 않았을 때 실행할 콜백 함수를 미리 '약속'하는 것이다.
  • 프로미스를 사용하려면 먼저 @Promise@ 객체를 만들어야 한다.
    • 성공했을 때 실행할 @resolve()@ 콜백 함수와, 실패했을 때 실행할 @reject()@ 콜백 함수를 매개변수로 사용한다.

 

Promise 객체 만들기
  • 아래의 코드는 @Promise@ 객체를 만들기만 할 뿐 실제 이 프로미스를 사용하지는 않는데, 이렇게 @Promise@ 객체를 만드는 코드를 '제작 코드(Producing Code)'라고 한다.
let likePizza = true;
const pizza = new Promise((resolve, reject) => {
  if (likePizza) {
    resolve("피자를 주문합니다.");
  } else {
    reject("피자를 주문하지 않습니다.");
  }
});

 

likePizza가 true이면 '피자를 주문합니다.'를 resolve 함수에, false이면 '피자를 주문하지 않습니다.'를 reject 함수에 넘긴다.

 

 

Promise 객체 사용하기
  • Promise 객체를 사용하는 코드를 '소비 코드(Consuming Code)'라고 한다.
  • 즉, 프로미스는 '객체를 생성하는 부분'과 '프로미스를 사용하는 부분'으로 나뉜다.
  • 프로미스를 실행할 때는 @then()@ 메서드와 @catch()@ 메서드, @finally()@ 메서드를 사용한다.
    • @then()@ 메서드는 프로미스에서 '성공'했다는 결과를 보냈을 때 실행할 함수나 명령을 연결한다.
    • @catch()@ 메서드는 프로미스에서 '실패'했다는 결과를 보냈을 때 실행할 함수나 명령을 연결한다.
    • @finally()@ 메서드는 프로미스에서 '성공'/'실패'했다는 결과와 상관 없이 실행할 함수나 명령을 연결한다.
let likePizza = true;
const pizza = new Promise((resolve, reject) => {
  if (likePizza) {
    resolve("피자를 주문합니다."); // v
  } else {
    reject("피자를 주문하지 않습니다.");
  }
});

pizza
  .then((result) => console.log(result)) // v, 프로미스에서 성공 결과를 보냈을 떄 처리 (세미콜론을 붙이지 않는다.)
  .catch((err) => console.log(err)); // 프로미스에서 실패 결과를 보냈을 때 처리
let likePizza = false;
const pizza = new Promise((resolve, reject) => {
  if (likePizza) {
    resolve("피자를 주문합니다.");
  } else {
    reject("피자를 주문하지 않습니다."); // v
  }
});

pizza
  .then((result) => console.log(result)) // 프로미스에서 성공 결과를 보냈을 떄 처리
  .catch((err) => console.log(err)); // v, 프로미스에서 실패 결과를 보냈을 때 처리
let likePizza = true;
const pizza = new Promise((resolve, reject) => {
  if (likePizza) {
    resolve("피자를 주문합니다."); // v
  } else {
    reject("피자를 주문하지 않습니다.");
  }
});

pizza
  .then((result) => console.log(result)) // v, 프로미스에서 성공 결과를 보냈을 떄 처리
  .catch((err) => console.log(err)) // 프로미스에서 실패 결과를 보냈을 때 처리
  .finally(() => console.log("완료")); // v, 프로미스에서 성공/실패 결과를 보냈을 때와 상관 없이 처리
result와 err의 변수 이름은 임의로 지정해도 된다.

 

 

Promise의 상태
  • 프로미스는 @resolve()@ 함수나 @reject()@ 함수를 매개변수로 받아서 실행하는 객체이다.
  • 프로미스 객체는 자신의 상태를 저장했다가 @resolve()@ 함수나 @reject()@ 함수를 실행하면 상태를 바꾼다.
  • 그리고 다음의 3단계 상태로 진행된다.
상태 설명
pending 처음 프로미스를 만들면 대기 상태(pending)가 된다.
fulfilled 처리에 성공하면 이행 상태(fulfilled)가 된다.
rejected 처리에 실패하면 거부 상태(rejected)가 된다.

 

프로미스 체이닝(Promise Chaining)
  • 콜백 함수로 여러 단계를 연결하면 다음과 같이 코드를 작성할 수 있다.
const step1 = (callback) => {
  setTimeout(() => {
    console.log("피자 도우 준비");
    callback();
  }, 2000);
};

const step2 = (callback) => {
  setTimeout(() => {
    console.log("토핑 완료");
    callback();
  }, 1000);
};

const step3 = (callback) => {
  setTimeout(() => {
    console.log("굽기 완료");
    callback();
  }, 2000);
};

console.log("피자를 주문합니다.");
step1(function () {
  step2(function () {
    step3(function () {
      console.log("피자가 준비되었습니다.");
    });
  });
});

 

  • 프로미스 체이닝(Promise Chaining)을 사용하여 다음과 같이 간단하게 표현할 수 있다.
    • @then()@ 메서드를 사용해서 여러 개의 프로미스를 연결할 수 있다.
const pizza = () => {
  return new Promise((resolve, reject) => {
    resolve("피자를 주문합니다.");
  });
};

const step1 = (message) => {
  console.log(message);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("피자 도우 준비");
    }, 3000);
  });
};

const step2 = (message) => {
  console.log(message);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("토핑 완료");
    }, 1000);
  });
};

const step3 = (message) => {
  console.log(message);
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("굽기 완료");
    }, 2000);
  });
};

pizza()
  .then((result) => step1(result)) // pizza()가 성공하면 step1() 실행
  .then((result) => step2(result)) // step1()이 성공하면 step2() 실행
  .then((result) => step3(result)) // step2()이 성공하면 step3() 실행
  .then((result) => console.log(result)) // step3()이 성공하면 "굽기 완료" 표시
  .then(() => {
    console.log("피자가 준비되었습니다. 🍕");
  });

 

  • 위의 소비 코드를 아래와 같이 축약해서 표현할 수 있다.
pizza()
  .then(step1) // pizza()가 성공하면 step1() 실행
  .then(step2) // step1()이 성공하면 step2() 실행
  .then(step3) // step2()이 성공하면 step3() 실행
  .then(console.log) // step3()이 성공하면 "굽기 완료" 표시
  .then(() => {
    console.log("피자가 준비되었습니다. 🍕");
  });

 

pizza().then((result) => step1(result)); →  pizza().then(step1);

 

 

비동기 처리 예 ③ : async, await

  • 프로미스 체이닝은 프로미스를 계속 연결해서 사용하므로 콜백 지옥처럼 코드가 복잡해질 수도 있다.
  • 이 문제를 줄이기 위해 ECMAScript2017(ES8)부터 @async@ 함수와 @await@ 예약어가 등장했다.

 

async 함수
  • @async@ 예약어를 붙인 함수는 프로미스(Promise) 객체를 반환한다.
async function displayHello() {
  console.log("Hello");
}
displayHello();

// -> 콘솔창에 다음의 코드를 입력하면 displayHello() 함수가 프로미스를 반환하는 것을 확인할 수 있다.
displayHello(); // Promise  {<fulfilled>: undefined}

 

  • @async@ 함수를 사용한 않은 경우와 사용하는 경우를 비교해보면 다음과 같다.
function whatsYourFavorite() {
  let fav = "Javascript";
  return new Promise((resolve, reject) => resolve(fav));
}

function displaySubject(subject) {
  return new Promise((resolve, reject) => resolve(`Hello, ${subject}`));
}

whatsYourFavorite()
  .then(displaySubject) // .then(response => displaySubject(response))
  .then(console.log); // .then (result => console.log(result));
async function whatsYourFavorite() {
  let fav = "Javascript";
  return fav;
}

async function displaySubject(subject) {
  return `Hello ${subject}`;
}

whatsYourFavorite()
  .then(displaySubject) //
  .then(console.log); //

 

await 예약어
  • @await@은 '이 함수가 끝날 때까지 기다려!' 라고 표시하는 것이다.
  • 프로미스를 계속 연결하면서 실행할 경우 연달아 @then@을 사용한다. (프로미스 체이닝이 너무 길어지면 코드를 이해하기 어려워진다.)
  • 이럴 때 @await@ 예약어를 사용하면 이전 프로미스 결과를 받아서 다음 프로미스로 연결해 주는 과정을 좀 더 쉽게 알아볼 수 있다.
  • @await@ 예약어는 자바스크립트에서 비동기 코드를 실행할 때 유용한데, @await@은 @async@ 함수에서만 사용할 수 있다.
  • @await@은 @async@ 함수에서만 사용할 수 있으므로, @async init()@ 함수를 따로 만든 후에는 그 안에서 @await@를 사용해 프로미스의 실행 순서를 지정하면 된다.

 

await은 async 함수에서만 사용할 수 있다.
async function whatsYourFavorite() {
  let fav = "Javascript";
  return fav;
}

async function displaySubject(subject) {
  return `Hello ${subject}`;
}

async function init() {
  const response = await whatsYourFavorite(); // whatsYourFavorite() 함수의 실행이 끝날 때까지 기다린 후, 결과값을 response에 저장
  const result = await displaySubject(response); // response 값을 이용해 displaySubject() 함수를 실행하고, 끝나면 결과값을 result에 저장
  console.log(result); // result를 표시
}

init();

 

 

fetch API

개념

  • 서버에 있는 JSON 파일을 가져올 때 @XMLHttpRequest@ 객체를 사용한다.
  • @XMLHttpRequest@를 통해 자료를 주고 받는 방법은 자바스크립트 초기 버전부터 지금까지 사용하고 있다.
  • 모던 자바스크립트에서 @XMLHttpRequest@를 대신할 fetch API가 등장하였다.

 

특징

  • @fetch@는 AJAX처럼 서버로 요청을 보내거나 자료를 받아오는 방법이지만, 프로미스(Promise)를 반환한다는 게 가장 중요한 차이점이다.
fetch("student1.json"); // Promise {<pending>}  (프로미스 반환)

fetch("student1.json").then(console.log); // (Response 객체 반환)
// ▼ Response {type: 'basic', url: 'http://127.0.0.1:3000/Study/Day6/Practice/student1.json', redirected: false, json: ƒ, text: ƒ, …}
// json: ƒ ()
// text: ƒ ()
// body: (...)
// bodyUsed: false
// headers: Headers {}
// ok: true ✅
// redirected: false
// status: 200 ✅
// statusText: "OK"
// type: "basic"
// url: "http://127.0.0.1:3000/Study/Day6/Practice/student1.json"

// -> Response 객체에는 지정한 파일을 가져오는 데 성공했을 때 반환되는 값이 들어 있다.
// -> 자료를 성공적으로 가져왔는지 여부를 확인하려면 status 값이 200인지 또는 ok값이 true인지 체크한다.

 

예제 코드

@XMLHttpRequest@로 JSON 자료 가져오기
let xhr = new XMLHttpRequest();

xhr.open("GET", "student2.json");
xhr.send();

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    let students = JSON.parse(xhr.responseText);

    renderHTML(students);
  }
};

function renderHTML(contents) {
  let output = "";

  for (let content of contents) {
    output += `
      <h2>${content.name}</h2>
      <ul>
        <li>전공 : ${content.major}</li>
        <li>학년 : ${content.grade}</li>
      </ul>
      <hr>
    `;
  }
  document.getElementById("result").innerHTML = output;
}

 

@fetch@로 JSON 자료 가져오기
  • @fetch@를 사용할 경우 따로 if 문을 쓰지 않아도 된다.
    • @then()@ 함수를 연결하면서 이미 자료를 성공적으로 가져왔다는 전제가 생겼기 떄문이다.
fetch("student2.json") // 1) json 파일을 읽어온다.
  .then((response) => response.json()) // 2) json 파일을 객체로 변환한다.
  .then((json) => {
    // 3) 객체를 출력한다.
    let output = "";

    json.forEach((student) => {
      output += `
      <h2>${student.name}</h2>
      <ul>
        <li>전공 : ${student.major}</li>
        <li>학년 : ${student.grade}</li>
      </ul>
      <hr>
    `;
    });
    document.querySelector("#result").innerHTML = output;
  })
  .catch((error) => console.log(error)); // 4) 에러가 발생하면 에러를 출력한다.

 

Cheat Sheet

 

참고 사이트

 

Asynchronous JavaScript - Learn web development | MDN

In this module, we take a look at asynchronous JavaScript, why it is important, and how it can be used to effectively handle potential blocking operations, such as fetching resources from a server.

developer.mozilla.org

 

Introducing asynchronous JavaScript - Learn web development | MDN

In this article, we'll explain what asynchronous programming is, why we need it, and briefly discuss some of the ways asynchronous functions have historically been implemented in JavaScript.

developer.mozilla.org

 

Fetch API 사용하기 - Web API | MDN

Fetch API는 HTTP 파이프라인을 구성하는 요청과 응답 등의 요소를 JavaScript에서 접근하고 조작할 수 있는 인터페이스를 제공합니다. Fetch API가 제공하는 전역 fetch() 메서드로 네트워크의 리소스를

developer.mozilla.org

 

728x90