비동기 처리를 위한 세 가지 방법

Callback

2를 요소로 가진 길이가 4인 배열이 있다.

let arr = [2, 2, 2, 2];

이 배열에 2를 곱한 값, 즉 각 요소에 2를 곱한 값을 반환할 땐 우리는 배열의 메소드 중 map또는 forEach를 사용할 수 있다.

let squareArr = arr.map((el) => el * 2);
console.log(squareArr); // [4, 4, 4, 4]

map 메소드는 인자로 함수 (el) => el * 2 를 받는다. 이 함수는 el이라는 입력인자를 받아, 배열의 각 요소 el에 2를 곱한 값을 반환한다.

이처럼 고차함수의 인자로 전달되는 함수를 콜백(Callback) 함수라고 부른다. 콜백 함수를 인자로 받은 함수를 Caller 함수라고 하며, map은 각 요소에 2를 곱하는 콜백 함수의 Caller가 된다.

const printString = (string, callback) => {
  setTimeout(() => {
    console.log(string);
    callback();
  }, 1000);
};

printString이라는 함수를 만들고 인자로 stringcallback함수를 받는다. 이 함수는 setTimeout이라는 Web API를 사용해 1000ms (1sec) 후 입력 인자로 받아온 string을 반환하고, callback함수를 실행한다.

const printAll = () => {
  printString('a', () => {
    printString('b', () => {
      printString('c', () => {});
    });
  });
};
printAll();

printAll이라는 함수는 실행하게 되면 printString 함수를 실행하고, callback 함수로 들어온 printString 함수를 실행하고, 그 다음 callback으로 들어온 printString함수를 실행한다.

결과는 string 으로 들어온 a,b,c 가 1초 간격으로 console창에 표기된다.

위 아주 간단한 예시인 callback함수도 마지막 부분에 })가 다수 반복되는데, 실제 현업에서 callback을 사용한다면 어떨까?

아마 다음과 같이 가독성이 매우 떨어지는 이른바 callback hell을 마주할 것같다.

Fig 1. 콜백 지옥

이러한 불편함을 개선하기 위해 Promise를 사용할 수 있다.

Promise

자바스크립트는 비동기 처리를 위해 콜백 함수를 사용하나, 전통적인 콜백 패턴은 콜백 지옥으로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러 처리가 곤란하다. 또한, 여러 개의 비동기 처리를 한 번에 처리하는 데도 한계가 있다.

ES6에선 비동기 처리를 위한 또 다른 패턴으로 Promise를 도입했다.

Promise는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

Promisecallback chain을 핸들링할 수 있는 하나의 Class이다. Promiseresolvereject로 함수를 실행하거나, 에러를 핸들링할 수 있다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(string);
      resolve();
    }, 1000);
  });
};

위 함수는 string을 입력인자로 받고 Promise class를 반환한다. setTimeout은 위와 동일하며, callback을 실행하는 대신 resolve를 실행한다.

const printAll = () => {
  printString('a')
    .then(() => {
      return printString('b');
    })
    .then(() => {
      return printString('c');
    });
};
printAll();

위 함수는 a라는 string을 입력인자로 받고 Promise를 생성한다. 이후 다음 작업으로 .then을 사용하는데, 이 결과로 위와 같이 b를 입력인자로 넣은 Promise를 리턴한다. callback과 동일하게 동작하지만 더 가시성이 있다는 장점이 있다.

Promise.then으로 다음 작업을 이어나갈 수 있으며, 에러 핸들링은 reject를 실행하여 .catch로 관리할 수 있다.

Promise 또한 .then의 연속적인 사용으로 Promise Hell을 유발할 수 있다. 이를 Promise 클래스를 미리 선언하고, 결과에 return 하는 방식으로 가시성을 향상할 수 있다.

Async / Await

Promise를 조금 더 간편하게 사용하기 위해 ES7 부터 새롭게 도입된 Async가 있다. Promise를 리턴하는 함수는 동일하며, 어떤 비동기 함수들을 실행할 때 동기적으로 실행하는 것처럼 보이는 코드를 작성할 수 있다.

비동기 함수를 실행할 때 함수의 선언 전에 async를 앞에 붙이고, .then을 수행하는 대신 앞에 await을 붙여 사용할 수 있다.

const printString = (string) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(string);
      resolve();
    }, 1000);
  });
};
const printAll = async () => {
  await printString('a');
  await printString('b');
  await printString('c');
};

Promise와 유사하지만 await을 사용해 훨씬 가독성이 뛰어난 함수를 만들 수 있다. 중요한 점은 함수의 선언 앞에 async를 꼭 붙여줘야 한다는 점, 그리고 Promise를 실행하는 단계마다 await로 구분해줘야 한다는 것이다.

Reference

  • 모던 자바스크립트 Deep Dive 도서
YUNSU BAE

YUNSU BAE

주니어 웹 개발자 배윤수 입니다!

예술의 영역을 동경하고 있어요. 🧑‍🎨