[JS] 콜백지옥 해결방안 (기명함수, promise, generator, async/await)

동기와 비동기

동기적 (synchronous)

  • 하나의 일이 끝날 때까지 다음 일이 기다리고 있음

비동기적 (asynchronous)

  • 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 방식
  • ex. setTimeout, addEventListener 등
  • 별도의 요청, 실행대기, 보류 등과 관련된 코드

 

콜백지옥 예시와 해결방안

예시

setTimeout(
  function (name) {
    var 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,
  "에스프레소"
);

 

해결방안1 기명함수로 변환하기

let coffeList = "";

const addEspresso = function(name) {
    coffeeList = name;
    console.log(coffeeList);
    setTimeout(addAmericano, 500, "아메리카노");
};

const addAmericano = function(name) {
    coffeeList += ", " + name;
    console.log(coffeeList);
    setTimeout(addMocha, 500, "카페모카");
};

const addMocha = function(name) {
    coffeeList += ", " + name;
    console.log(coffeeList);
    setTimeout(addLatte, 500, "카페라떼");
};

const addLatte = function(name) {
    coffeeList += ", " + name;
    console.log(coffeeList);
};

setTimeout(addEspresso, 500, "에스프레소");

가독성은 좋아졌지만 한번만 쓸 익명함수를 하나하나 기명함수로 만들었다는 점에서 근본적인 해결방안이 되지 않음!

 

해결방안2 Promise

비동기작업의 특징은 순서를 보장하지 않는 것이다. 필요하지만 그 안에서도 순서가 필요한 경우가 있다. 그래서 비동기 작업들을 동기적으로 표현하는 것이 필요한데, 이 방법 중 하나가 Promise다!

 

 

Promise

1) 비동기 처리에 대해 처리가 끝나면 알려달라는 약속

  • resolve: 비동기 작업이 성공한 경우
  • reject: 비동기 작업이 실패한 경우

2) resolve 혹은 reject 가 실행되기 전까지는 다음 작업을 설정한 then, 오류 발생시의 작업을 설정한 catch로 넘어가지 않음!

 

생성 방법

1) new Promise()를 통해 인스턴스화 된다.

2) new Promise()는 비동기 작업을 수행할 콜백함수를 인자로 받으며, 해당 콜백함수는 resolve, reject를 인자로 전달받는다.

 

promise의 상태

1) pending: 비동기 처리가 아직 수행되지 않아 resolve or reject가 호출되지 않은 상태

2) fulfilled: 비동기 처리가 수행되고 성공적으로 결과를 반환한 상태, resolve()가 호출된 상태

3) rejected: 비동기 처리가 수행되었지만 실패하거나 오류가 발생하여 reject()가 호출된 상태

4) settled: 비동기 처리가 수행되어 resolve() or reject()가 호출된 상태

 

then / catch

비동기 작업 수행 후 resolve() or reject()가 반환된 후에 수행할 동작을 정의한다.

1) then

  • 두개의 콜백함수를 인자로 전달받는다.
  • 첫번째 콜백함수는 성공할 때 호출되며, 두번째 콜백함수는 실패할 때 호출된다.
  • then 메소드 또한 완료 후 promise를 반환한다.

2) catch

  • 예외 발생시 호출된다.
  • catch 메소드 또한 완료 후 promise를 반환한다.

 

new Promise(function (resolve) {
	setTimeout(function () {
		var name = '에스프레소';
		console.log(name);
		resolve(name);
	}, 500);
}).then(function (prevName) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			var name = prevName + ', 아메리카노';
			console.log(name);
			resolve(name);
		}, 500);
	});
}).then(function (prevName) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			var name = prevName + ', 카페모카';
			console.log(name);
			resolve(name);
		}, 500);
	});
}).then(function (prevName) {
	return new Promise(function (resolve) {
		setTimeout(function () {
			var name = prevName + ', 카페라떼';
			console.log(name);
			resolve(name);
		}, 500);
	});
});

반복되는 부분을 리팩토링 해보자

 

 const addCoffee = function (name) {
    return function (prevName) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                let newName = prevName ? `${prevName}, ${name}` : name;
                console.log(newName);
                resolve(newName);
            }, 100);
        });
    };
};

addCoffee("에스프레소")()
    .then(addCoffee("아메리카노"))
    .then(addCoffee("카페모카"))
    .then(addCoffee("카페라떼"));

resolve가 호출되는 시점이 잘 이해가되지 않아 매개변수로 이름과 함께 setTimeout의 delay도 함께 전달해봤다.

 

const addCoffee = function (name, time) {
    return function (prevName) {
        return new Promise(function (resolve) {
            setTimeout(function () {
                let newName = prevName ? `${prevName}, ${name}` : name;
                console.log(newName);
                resolve(newName);
            }, time);
        });
    };
};

addCoffee("에스프레소", 1000)()
    .then(addCoffee("아메리카노", 100))
    .then(addCoffee("카페모카", 500))
    .then(addCoffee("카페라떼", 200));

먼저 호출됐더라도 각 커피에게 주어진 시간만큼 delay 후 출력 -> 그 다음 커피가 해당 과정을 반복한다.

 

더보기

동작이 잘 이해되지 않아서 정리해본 내용..


1) addCoffee는 함수 -> name과 time을 인자로 갖고, function (prevName) { ... } 을 반환한다.

2) function (prevName) -> promise를 반환함. 즉, 어떤 비동기적 동작이 끝나면 신호를 반환!

3) promise가 확인할 비동기 작업 -> function(resolve) { setTimeout( ... ) }

4) time만큼 시간이 흐른 후 setTimeout의 콜백함수가 실행되면서 resolve(newName) 호출

5) resove()가 호출됐으니 .then() 실행 가능

6) .then() 에서는 이전 promise resolve()에서 매개변수로 전달된 newName이 .then의 콜백함수로 전달됨

-> 즉, addCoffee가 반환한 function(prevName)에 전달

7) 앞의 과정을 반복

 

 

Generator

생성

function*을 통해 생성한다! 제너레이터 함수를 호출할 경우 실행을 처리할 제너레이터 객체가 반환된다. 이 제너레이터 객체는 .next()로 다음 동작을 수행할 수 있는 iterator 객체이다!

function* generateSequence() {
    ...
}

 

.next()

제너레이터 객체의 .next()를 호출하면 가장 가까운 yield <value> 문을 만날 때까지 실행이 지속된다.

그러다 yield를 만나면 실행을 멈추고 <value>가 바깥 코드로 반환된다.

 

반환값

두 프로퍼티를 가진 객체를 반환한다.

1) value: 산출 값

2) done: 함수 실행이 끝난 경우 true, 아닌 경우 false -> 즉, 제너레이터 함수 내 모든 동작을 끝냈다면 true

 

const addCoffee = function(prevName, name) {
    setTimeout(function() {
        coffeeMaker.next(prevName ? prevName + ", " + name : name);
    }, 500);
};

const coffeeGenerator = function* () {  
    const espresso = yield addCoffee("", "에스프레소");
    console.log(espresso);
    
    const americano = yield addCoffee(espresso, "아메리카노");
    console.log(americano);
    
    const mocha = yield addCoffee(americano, "카페모카");
    console.log(mocha);
    
    const latte = yield addCoffee(mocha, "카페라떼");
    console.log(latte);
};

const coffeeMaker = coffeeGenerator();  // coffeeMaker는 generator 객체 (iterator)
coffeeMaker.next();

yield 를 만나면 yield 이후 구문이 모두 실행될 때까지 기다린 후 다음 실행

 

 

async / await

const addCoffee = function (name) {
	return new Promise(function (resolve) {
		setTimeout(function(){
			resolve(name);
		}, 500);
	});
};

const coffeeMaker = async function () {
	let coffeeList = '';
    
	const _addCoffee = async function (name) {
		coffeeList += (coffeeList ? ', ' : '') + await addCoffee(name);
	};
    
	await _addCoffee('에스프레소');
	console.log(coffeeList);
	
    await _addCoffee('아메리카노');
	console.log(coffeeList);
	
    await _addCoffee('카페모카');
	console.log(coffeeList);
	
    await _addCoffee('카페라떼');
	console.log(coffeeList);
};

coffeeMaker();

 

promise의 .then과 같은 효과를 얻을 수 있음

 

async 함수는 항상 Promise를 반환한다.

 

 

참고자료

MDN, Promise.prototype.then()

 

Promise.prototype.then() - JavaScript | MDN

then() 메서드는 Promise를 리턴하고 두 개의 콜백 함수를 인수로 받습니다. 하나는 Promise가 이행했을 때, 다른 하나는 거부했을 때를 위한 콜백 함수입니다.

developer.mozilla.org

 

코어 자바스크립트, 제너레이터와 비동기 이터레이션

 

제너레이터

 

ko.javascript.info