async await 그리고 Promise

하머·2022년 12월 9일
0

async await 패턴이전에는 제너레이터를 사용해 비동기 처리를 동기처럼 처리하였음

const runGenerator = (generatorFunction) => {...}

(runGenerator(function* fetchData() {
	const url = "https://test.com";
	
	const response = yield fetch(url);

	const data = yield response.json();

	console.log(data);
})();

// function*은 Generator 함수 키워드이며 하나 이상의 yield 문을 포함한다.
// Generator는 Iterable을 생성하는 함수

es8에서 async await 등장 이후 (구형 브라우저에서 사용안됨)

async function fetchData() {
	const url = "https://test.com";
	
	const response = await fetch(url);

	const data = await response.json();

	console.log(data);
}

fetchData()

async await의 동작 원리 (babel의 es5 방식 구현)

// ES8
async function foo() {
    await bar();
}

// ES5 complied
let foo = (() => {
   var _ref = _asyncToGenerator(function*() {
       yield bar();
   });
    return function foo() {
       return _ref.apply(this, arguments);
    };
})();
function _asyncToGenerator(fn) {
    return function() {
        var gen = fn.apply(this, arguments);
        return new Promise(function(resolve, reject) {
            function step(key, arg) {
                try {
                    var info = gen[key](arg);
                    var value = info.value;
                } catch (error) {
                    reject(error);
                    return;
                }
                if (info.done) {
                    resolve(value);
                } else {
                    return Promise.resolve(value).then(
                        function(value) {
                            step("next", value);
                        },
                        function(err) {
                            step("throw", err);
                        }
                    );
                }
            }
            return step("next");
        });
    };
}
  • async 함수인 foo함수는 즉시실행함수로 바뀌었고, foo함수 안에서 _asyncToGenerator의 동작이 이루어짐

    • _asyncToGenerator새로운 Promise 인스턴스를 리턴 ⇒ async 함수는 암묵적으로 내용이 없더라도 promise를 리턴시킴

    • Promise에서는 fn.apply를 실행하여 인자로 넘어온 Generator를 실행하여 iterator객체를 클로저로 저장해둔다.

    • 나머지는 클로저에 저장한 iterator를 실행시키고, 반환된 Promise객체를 재귀함수를 통해 반복실행(next와 else문)한다.

      (각각의 await이 각 iterator의 next가 되는 것이다.) 그리고 info.done에 따라 Generator 반복이 끝나면 값을 resolve처리 한다.

  • 정리하자면 async await 패턴은 Generatoryield를 통해 동기적인 모습으로 바꾸어주고, PromiseGenerator 로 만든 iterator 를 재귀를 통해 반복해서 실행시켜준다.

    • await에 사용되는 함수가 항상 Promise 를 반환해야하는 이유
  • await 키워드는 Promisesettled 상태(비동기 처리가 수행된 상태)가 될 때까지 대기한 후 resolve한 처리 결과를 반환하는 표현식이다.

// 순차적인 비동기 통신을 위한 형태
async function bar(n) {
  const a = await new Promise(resolve => setTimeout(() => resolve(n), 3000));
  // 두 번째 비동기 처리를 위해 첫 번째 비동기 처리 결과가 필요하다.
  const b = await new Promise(resolve => setTimeout(() => resolve(a + 1), 3000));
  // 세 번째 비동기 처리를 위해 두 번째 비동기 처리 결과가 필요하다.
  const c = await new Promise(resolve => setTimeout(() => resolve(b + 1), 3000));
  console.log([a, b, c]);
}

bar(1) // 6초 소요

// 순차적인 순서가 필요없을 때의 형태
async function foo() {
  const res = await Promise.all([
  	new Promise(resolve => setTimeout(() => resolve(1), 3000)),
    new Promise(resolve => setTimeout(() => resolve(2), 3000)),
    new Promise(resolve => setTimeout(() => resolve(3), 3000))
  ]);
  console.log(res); // [1, 2, 3]
}

foo() //3초 소요

Promise

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.

  • mdn Promise 페이지

  • 보통 우리가 알고있는 Promise를 사용하는 이유 중 하나는 콜백 지옥을 해결한다는 것이다. 어떻게 해결한다는 것일까

제어의 역전

콜백 지옥의 문제점은 가독성적인 문제도 있지만, 여기서 짚어볼 문제점은 제어의 역전이다. 믿음직하지 못한 어떤 비동기 프로그램에게 우리 프로그램의 호출 제어권을 넘겨주어 어떤 일이 발생할 수 있다는 것이다.

제어권이 콜백에게 있는 상태에서 저 함수에서 어떤 일이 잘못된다면..? 어떻게 동작할지, 어떠한 값들이 나올지 모르게 된다.

그래서 이 제어권 을 돌려받기 위해 콜백을 이벤트 방식처럼 사용하는 것이 바로 Promise 이다.

  • 이벤트 방식
    function foo(x) {
      // 어떤 일을 실행하고 이벤트를 반환
      return listner;
    }
    
    var evt = foo(42);
    
    evt.on('completion', bar());
    evt.on('failure', barErr());
  • Promise 방식
    function foo(x) {
      // 어떤 일을 실행하고 프로미스를 반환
    
      return new Promise(function(resolve, reject) {});
    }
    
    var p = foo(42);
    
    bar(p);
    
    function bar(fooPromise) {
      fooPromise.then(
        function() {}, // resolve 시 작업
        function() {} // reject 시 작업
      );
    }

Thenable Duck Typing

In Promises-land, an important detail is how to know for sure if some value is a genuine Promise or not. Or more directly, is it a value that will behave like a Promise?

  • You Don’t Know JS: Async & Performance

하지만 그런 신뢰도 때문에 Promise 를 만들었지만, 그 Promise 가 진짜 Promise 인줄 어떻게 알까요..?

만약에 Promise 가 아닌 then을 품고 그 안에 fulfilledrejected를 담고있는 객체 라면?

var p = {
  then: function(foo, err) {
    foo(42);
    err('하이');
  }
};

p.then(
  function fulfilled(val) {
    console.log(val);
  },
  function rejected(err) {
    console.log(err);
  }
);

자바스크립트는 이를 덕 타이핑 이라는 특징으로 이 신뢰를 해결합니다.

덕 타이핑 : 오리처럼 생긴 동물이 오리처럼 운다면, 그것은 오리로 생각하겠다는 의미입니다. 코드적으로 풀어보면 instanceof 같은 키워드로 관계를 조사하는 대신에, 객체가 갖추고 있어야 할 일정한 조건을 만족하면 그것을 대상 객체처럼 사용하겠다는 뜻입니다.

이 특성은 Promise 에서는 then() 메소드를 갖고있는 Thenable 한 객체라면 모두 Promise 로 간주하고 동작한다는 의미로 쓰입니다.

안전하게 Promise를 사용하는 방법Promise.resolve를 사용해 감싸자

// foo가 실제 Promise인지, 아니면 then을 갖춘 가짜 Promise 객체인지 알 수 없다.
foo( 42 )
.then( function(v){
console.log( v );
} );

// Promise.resolve는 foo가 Thenable한 객체라면 Promise로 간주하고 사용함으로써
// 이것이 Promise라는 신뢰를 얻을 수 있다.
Promise.resolve( foo( 42 ) )
.then( function(v){
console.log( v );
} );

foo라는 객체가 진짜 Promise 인지 가짜 Promise 인지는 모르지만 최소한 Thenable 한 것은 알고있으니 이런식으로 사용해야 Promise 의 의미인 신뢰를 살려 사용할 수 있다.

0개의 댓글