Asynchronous : callback / Promise / Async & Await

Jiwoo Joy Kim (zuzokim)·2021년 5월 2일
1

앞선 글에서 Ajax가 클라이언트-서버간의 통신을 비동기적으로 처리하는 웹 개발 기법이라고 정리했는데, 이번엔 본격 비동기 처리가 뭔지에 대해서 작성해보려고 한다.

Acynchronous 비동기 처리

비동기식 처리 모델(Asynchronous processing model 또는 Non-Blocking processing model)병렬적으로 태스크를 수행한다. 즉, 태스크가 종료되지 않은 상태라 하더라도 대기하지 않고 즉시 다음 태스크를 실행한다. 서버에 데이터를 요청한 이후 서버의 응답을 받을 때까지 대기하지 않고(Non-Blocking) 즉시 다음 태스크를 수행한다. 이후 서버로부터 응답이 오면 이벤트가 발생하고 이벤트 핸들러가 데이터를 가지고 수행할 태스크를 계속해 수행한다. 자바스크립트의 대부분의 DOM 이벤트와 Timer 함수(setTimeout, setInterval), Ajax 요청은 비동기식 처리 모델로 동작한다.
*블로킹(Blocking)은 작업중단을 의미한다.
출처 - (https://poiemaweb.com/es6-promise)


이런 비동기 처리를 위해 이용하는

0. callback 함수 패턴

쉽게말해, 콜백함수는 나중에 호출할 함수다.

콜백 함수의 동작 방식은 일종의 식당 자리 예약과 같습니다. 일반적으로 맛집을 가면 사람이 많아 자리가 없습니다. 그래서 대기자 명단에 이름을 쓴 다음에 자리가 날 때까지 주변 식당을 돌아다니죠. 만약 식당에서 자리가 생기면 전화로 자리가 났다고 연락이 옵니다. 그 전화를 받는 시점이 여기서의 콜백 함수가 호출되는 시점과 같습니다. 손님 입장에서는 자리가 날 때까지 식당에서 기다리지 않고 근처 가게에서 잠깐 쇼핑을 할 수도 있고 아니면 다른 식당 자리를 알아볼 수도 있습니다.
자리가 났을 때만 연락이 오기 때문에 미리 가서 기다릴 필요도 없고, 직접 식당 안에 들어가서 자리가 비어 있는지 확인할 필요도 없습니다. 자리가 준비된 시점, 즉 데이터가 준비된 시점에서만 저희가 원하는 동작(자리에 앉는다, 특정 값을 출력한다 등)을 수행할 수 있습니다.
출처: 캡틴판교 - 자바스크립트 비동기 처리와 콜백 함수

callback hell

비동기 함수는 위에서 말했다시피, 요청에 대한 응답이 오고 나서 그 다음 태스크를 차례대로 처리하지 않기 때문에 작업의 순서가 보장되지 않는다. 응답이 언제 올지, 처리가 언제 끝날지 모른다는 것이다. 그런데 이런 비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해 활용해야 하는 경우가 있다.

이 때 콜백함수 안에 콜백함수를 중첩(nesting)하여 호출해 사용하게 되는데, 아래의 그림처럼 비동기적 구현(ex. 클라이언트의 입력, 클릭..)이 많이 필요한 상황에서는 그에따라 콜백의 깊이도 깊어지게 된다. 이걸 callback hell, 말그대로 지옥 pyramid of doom ㅋㅋㅜ이라고 한다. 이런 코드는 일단 가독성이 떨어지고, 결과 또한 예측하기 어렵다. 또, 물려있는 로직을 변경하거나 에러를 잡아내는 일도 어려워진다.

아래는 이런 콜백헬을 해결하는 방법이다.

1. Promise

Promise 객체는 ES6문법에서 도입된 비동기 처리 패턴이다.
Promise의 세 가지 상태에는 pending(대기), fulfilled(이행), rejected(거부)가 있다.

// <Producer> Promise 객체의 생성 
// Promise 객체가 생성되면 자동으로 executor가 실행된다!
const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행하는 코드 (ex. setTimeout)

  if (/* 비동기 작업 수행 성공 */) {
    resolve('result');
  }
  else { /* 비동기 작업 수행 실패 */
    reject(new Error('failure reason'));
  }
});

// <Consumer> .then .catch .finally (후속처리메소드)
promise
.then(value => { //then() 메소드는 기본적으로 Promise를 반환한다.
	console.log(value); //resolve 실행시 호출
})
.catch(error => {
   	console.log(error); //reject 실행시 호출: 에러핸들링이 편리해진다.
})
.finally( () => {
	console.log('finally'); //작업 성공실패여부와 상관없이 무조건 호출
});

프로미스는 후속 처리 메소드.then() , .catch() 를 체이닝(chainning)하여 값을 리턴하는 것 뿐만 아니라 여러 개의 프로미스 객체를 연결하여 전달할 수도 있다. 이를 Promise Chaining 프로미스 체이닝 이라고 하고, 이로써 콜백 헬을 해결한다. 단, 콜백헬처럼 프로미스헬이 만들어질 수도 있으니 주의.

.catch()는 마지막 위치에서 에러를 핸들링할 수 있도록 실행시킬수 있지만(그리고 이것이 편리한 장점이기도 하지만), 체이닝 단계에서 중간 단계에서 실행시킬 수도 있다. 이를 통해 초기나 중간에 에러가 생겨도 그 다음 수행을 이어갈 수 있도록 대안(?)을 마련해 둘 수도 있다.

Promise.all

Promise.all은 프로미스가 담겨 있는 배열 등의 iterable을 인자로 전달 받는다. 그리고 전달받은 모든 프로미스를 병렬로 처리하고 모든 프로미스의 처리가 성공하면 각각의 프로미스가 resolve한 처리 결과를 배열에 담아 resolve하는 새로운 프로미스를 반환한다.

이때 첫번째 프로미스가 가장 나중에 처리되어도 Promise.all 메소드가 반환하는 프로미스는 첫번째 프로미스가 resolve한 처리 결과부터 차례대로 배열에 담아 그 배열을 resolve하는 새로운 프로미스를 반환한다. 즉, 처리 순서가 보장된다.

만약 입력된 프로미스 배열이 하나라도 reject 되면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

따라서 여러개의 프로미스가 모두 resolve 된 후, 다음 로직을 실행해야하는 경우에 유용하다!

2. async & await

Promise를 조금 더 간단하게 표현한 것이라고 보면 된다.

async function 함수명() {
  await 비동기_처리_메서드_명();
}

//예시
function fetchItems() {
  return new Promise(function(resolve, reject) {
    var items = [1,2,3];
    resolve(items)
  });
}

async function logItems() {
  var resultItems = await fetchItems();
  console.log(resultItems); // [1,2,3]
}

함수 앞에 async라는 예약어를 붙이고, Promise 객체를 반환하는 비동기 처리 함수 앞에 await를 붙인다. 이로써, .then() 등을 사용하지 않고 동기적인 함수'처럼' 사용할 수 있다.

async & await 에러 핸들링 (예외 처리)

Promise의 .catch() 처럼 try catch 문법으로 에러 핸들링이 가능하다.

//예시
async function logTodoTitle() {
  try {
    var user = await fetchUser();
    if (user.id === 1) {
      var todo = await fetchTodo();
      console.log(todo.title); 
    }
  } catch (error) {
    console.log(error);
  }
}

Additional - Event loop

다른 포스트로 정리 예정

profile
- I make something! ✍🏽👩🏻‍💻🎬🎨💖🪑🔨🔜

0개의 댓글