[책정리] CoreJavaScript 4-5 콜백 지옥과 비동기 제어

이진규·2022년 8월 20일
0
post-thumbnail

Q. 콜백 지옥이 뭐에요?

콜백 지옥은 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기가 계속되는 현상으로 주로 비동기적인 작업을 수행할 때 자주 등장하는데, 가독성도 떨어지고 코드를 수정하기도 어렵습니다.

setTimeout( function(name) {
  let coffeeList = name;
  console.log(coffeeList);
  setTimeout( function(name) {
  	coffeeList += ', ' + name;
  	console.log(coffeeList);
  	setTimeout( function(name) {
      coffeeList += ', ' + name;
  	  console.log(coffeeList);
	  setTimeout( function(name) {
        coffeeList += ', ' + name;
        console.log(coffeeList);
      }, 500, '카페라떼');
    }, 500, '카페모카');
  }, 500, '아메리카노');
}, 500, '에스프레소');

위 코드는 0.5초마다 커피 목록을 수집하고 출력합니다. 각 콜백 함수는 커피 이름을 전달하고 목록에 이름을 추가합니다. 작동하는 데에는 문제가 없지만 들여쓰기가 과도하게 깊어지고 값이 전달되는 순서가 아래에서 위로(에스프레소 -> 카페라떼) 향하기 때문에 어색하고 이해하기 어렵습니다.

Q. 그럼 어떻게 해야돼요?

콜백 지옥을 해결하는 간단한 방법은 익명의 콜백 함수를 기명 함수로 전환하는 것입니다.

let coffeeList = '';
const addEspresso = function(name){
  coffeeList = name;
  console.log(coffeeList);
  setTimeout(addAmericano, 500, '아메리카노');
};
const addAmericano = function(name){
  coffeeList += ', ' + name;
  console.log(coffeeList);
  setTimeout(addMoca, 500, '카페모카');
};
const addMoca = function(name){
  coffeeList += ', ' + name;
  console.log(coffeeList);
  setTimeout(addLatte, 500, '카페라떼');
};
const addLatte = function(name){
  coffeeList += ', ' + name;
  console.log(coffeeList);
};
setTimeout(addEspresso, 500, '에스프레소');

이러면 들여쓰기도 깊어지지 않고 위에서부터 아래로 순서대로 실행되기 때문에 간단하게 해결되었습니다.
하지만, 일회성 함수를 일일이 이름을 지어서 변수에 할당하기 불편하기도 하고 코드명을 계속 따라가야되므로 오히려 헷갈릴 소지가 있습니다.
그래서 이러한 비동기적 코드를 동기적으로, 또는 동기적으로 보이게라도 하기 위해 도입된 새로운 장치들이 있습니다. ES6에서 도입된 Promise, Generator가 있고 ES2017에서 도입된 async/await가 있습니다.

Q. Promise?

Promise는 ES6에서 도입된 새로운 객체입니다. Promise는 생성할 때 인자로 함수를 넘겨주는데 인자로 넘겨주는 함수의 인자로 resolve, reject 변수를 넣어서 넘겨줍니다.

01 new Promise(function(resolve){
02   setTimeout(function(){
03     let name = '에스프레소';
04     console.log(name);
05     resolve(name);
06   }, 500);
07 }).then(function(prevname) {
08   return new Promise(function (resolve) {
09     setTimeout(function() {
10       let name = prevName + ', 아메리카노';
11       console.log(name);
12       resolve(name);
13     }, 500);
14   });
15 }).then(function (prevName) {
16   return new Promise(function (resolve) {
17     setTimeout(function (){
18       let name = prevName + ', 카페모카';
19       console.log(name);
20       resolve(name);
21     }, 500);
22   });
23 }).then(function (prevName) {
24   return new Promise(function (resolve) {
25     setTimeout(function () {
26       let name = prevName + ', 카페라떼';
27       console.log(name);
28       resolve(name);
29     }, 500);
30   });
31 });

위 코드는 앞의 커피리스트를 출력하는 코드를 Promise-then을 사용하여 바꾼 코드입니다.

01 new Promise(function(resolve){

Promise는 ES6에서 새롭게 추가된 클래스이고 생성될 때 위처럼 resolve라는 변수를 담은 함수를 콜백함수를 인자로 넘겨줍니다.

02   setTimeout(function(){
03     let name = '에스프레소';
04     console.log(name);
05     resolve(name);

이 때 resolve라는 변수는 함수이고 resolve가 호출되면 then으로 넘어가게 됩니다.

07 }).then(function(prevname) {

resolve에는 인자를 담아서 보낼 수 있는데, 위에서 resolve에 담겨진 name은 아래 then에서 넘겨진 콜백함수의 prevName이 되어 전달됩니다.
이런식으로 Promise 객체 안의 콜백함수는 Promise객체가 호출될 때 바로 실행되지만, 콜백함수 내부에서 resolve를 만나기 전까지는 .then()이 실행되지 않습니다. 위의 코드는setTimeout을 통해 Promise객체가 실행되고 0.5초 이후에 .then()이 실행되도록 실행시간을 조정했습니다.

Q. Generator?

Generator도 ES6에서 도입된 개념인데, *를 붙여서 Generator라는 특별한 함수로 지정하게 됩니다.

01 const addCoffee = function(prevName, name){
02   setTimeout(function() {
03     coffeeMaker.next(
04       prevName ? prevName + ', ' + name : name);
05   }, 500);
06 };
07 const coffeeGenerator = function* () {
08   const espresso = yield addCoffee('', '에스프레소');
09   console.log(espresso);
10   const americano = yield addCoffee(espresso, '아메리카노');
11  console.log(americano);
12   const mocha = yield addCoffee(americano, '카페모카');
13   console.log(mocha);
14   const latte = yield addCoffee(mocha, '카페라떼');
15   console.log(latte);
16 };
17 const coffeeMaker = coffeeGenerator();
18 coffeeMaker.next();

7번째 줄의 *가 붙은 함수가 Generator 함수인데, Generator함수를 실행하면 Iterator가 반환되고 Iterator는 next라는 메소드를 갖고 있습니다. 그래서 이 next 메소드를 호출하면 Generator 함수 내부에서 가장 먼저 등장하는 yield 까지만 실행하고 실행을 멈추고 yield 뒤의 표현식을 리턴합니다. 그리고 나중에 next 메소드를 다시 호출하면 멈췄던 부분부터 시작해서 그 다음 등장하는 yield 까지만 실행하고 또 실행을 멈추고 yield 뒤의 표현식을 리턴합니다.

17 const coffeeMaker = coffeeGenerator();
18 coffeeMaker.next();

17번 코드에서 Generator함수를 호출합니다. 호출한 결과로 Iterator가 반환되며 18번 코드에서 next메소드를 통해 다음 yield까지 실행합니다.

07 const coffeeGenerator = function* () {
08   const espresso = yield addCoffee('', '에스프레소');

첫 yield인 8번 코드까지 실행시키며 8번 코드는 addCoffee를 호출합니다.

01 const addCoffee = function(prevName, name){
02   setTimeout(function() {
03     coffeeMaker.next(
04       prevName ? prevName + ', ' + name : name);
05   }, 500);
06 };

addCoffee함수를 호출하면서 setTimeout을 만나 0.5초 뒤에 다시 next메소드를 호출하게 됩니다. 이 때 prevName이 ''이므로 next 메소드의 인자값에는 '에스프레소'가 담기게 됩니다.

08   const espresso = yield addCoffee('', '에스프레소');
09   console.log(espresso);
10   const americano = yield addCoffee(espresso, '아메리카노');

그럼 전에 8번 코드까지 실행됐었으므로 9번코드부터 다음 yield인 10번 코드까지 실행합니다.

이런 방식으로 addCoffee()함수를 통해 Generator 함수인 coffeeMaker()의 끝까지 실행되게 됩니다.

Q. await/async?

await/async는 ES2017에서 추가된 키워드인데, 가독성도 좋고 작성법도 간단합니다.
비동기 작업을 수행하려는 함수 앞에 async를 써주고 async를 써준 함수 내부에서 실제 비동기 작업이 필요한 위치마다 await을 표기하면 뒤의 내용을 자동으로 Promise로 전환시켜주고 해당 내용이 resolve된 이후에 다음으로 진행하게 됩니다. Promise의 then과 흡사한 효과지만 사용법이 간단합니다.

01 const addCoffee = function (name) {
02   return new Promise(function (resolve) {
03     setTimeout(function () {
04       resolve(name);
05     }, 500);
06   });
07 };
08 const coffeeMaker = async function () {
09   let coffeeList = '';
10   let _addCoffee = async function (name) {
11     coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
12   };
13   await _addCoffee('에스프레소');
14   console.log(coffeeList);
15   await _addCoffee('아메리카노');
16   console.log(coffeeList);  
17   await _addCoffee('카페모카');
18   console.log(coffeeList);
19   await _addCoffee('카페라떼');
20   console.log(coffeeList);
21 };
22 coffeeMaker();

위의 코드는 계속 사용했던 커피 리스트를 받아오고 출력하는 코드를 async/await를 사용하여 수정한 코드입니다.
22번 코드에서 coffeeMaker()를 호출합니다.

08 const coffeeMaker = async function () {
09   let coffeeList = '';
10   let _addCoffee = async function (name) {
11     coffeeList += (coffeeList ? ',' : '') + await addCoffee(name);
12   };
13   await _addCoffee('에스프레소');

coffeeMaker함수는 비동기 작업을 수행해야하므로 async를 function() 앞에 붙여줍니다.
_addCoffee함수는 name으로 받은 커피이름을 0.5초 뒤에 coffeeList에 추가해주는 함수입니다.
13번 줄에서 _addCoffee함수를 호출하는데 그러면 11번 코드에서 addCoffee함수를 호출합니다. addCoffee함수는 0.5초뒤에 받은 name을 되돌려주는 함수입니다. 그래서 0.5초 뒤에 coffeeList에 '에스프레소'가 추가됩니다.

14   console.log(coffeeList);
15   await _addCoffee('아메리카노');

그럼 13번 줄을 완료하고 14번 줄로 와서 coffeeList를 출력해주고 다시 _addCoffee함수를 호출하고 전과 같이 0.5초 뒤에 coffeeList에 '아메리카노'를 추가해줍니다.
이런 방식으로 카페모카, 카페라떼까지 추가하고 출력해주게 됩니다.

여기까지가 콜백 지옥을 해결하기 위한 익명함수 기명함수로 바꿔주기, Promise-then, Generator함수, await/async 였습니다. 감사합니다.

참고

profile
개발자

0개의 댓글