await은 어떻게 함수를 멈추나?

In9_9yu·2023년 5월 14일
21

요약

await 이 앞에 붙으면 어떻게 함수의 실행을 중지시킬 수 있을까?

제너레이터 함수로 변환되어 해당 작업이 끝날 때 까지 멈출 수 있다.

❓ 의문점

MDN에 async function 문서에는 다음과 같이 써있다.

async 함수에는 await식이 포함될 수 있습니다. 이 식은 async 함수의 실행을 일시 중지하고 전달 된 Promise의 해결을 기다린 다음 async 함수의 실행을 다시 시작하고 완료후 값을 반환합니다.

  • 어떻게 함수의 실행을 멈추었다가 다시 시작할 수 있을까?
    • 다시 실행한다는 이야기는 중지된 부분부터 다시 실행되는 것일까? (결론: 그건 아니다)
    • 만약 아주 오랜 시간이 걸리는 작업이라면, 다른 작업에 영향을 끼치는게 아닐까? (microtask queue에 들어가 있어서 다른 작업을 방해하지는 않는다)

📒 기본적인 내용

1. async 함수는 기본적으로 Promise를 리턴한다.

	const asyncFn = async() => {}
    asyncFn() // Promise {<fulfilled>: undefined}

2. 이행된 Promise 값은 '일반적'으로 바깥으로 꺼내오지 못한다.

	const result = Promise.resolve(3).then(val => val)
    console.log(result) // Promise {<fulfilled>: 3}

👻 3. Promise.resolve(arg)는 arg의 값에 따라 다른 값을 리턴한다.

  • arg가 promise가 아닌 경우 : arg를 promise로 감싼 값이 된다 + 즉시 fullfilled 상태가 된다.
	const result = Promise.resolve(3) 
    console.log(result) // Promise {<fulfilled>: 3}
  • arg가 promise인 경우 : 해당 promise를 그대로 return 한다.
	// 1. pending 
	const result = Promise.resolve(new Promise((resolve) => setTimeout(resolve,5000)))
    
    console.log(result) // Promise {<pending>}

	// 5초 뒤
	
	console.log(result) // Promise {<fulfilled>: undefined}

🧪 직접 해보기

bable을 사용해서 async함수를 generator로 바꿔보자.

1. 필요한 패키지 설치

npm i @babel/cli @babel/core @babel/plugin-transform-async-to-generator -D

일반적으로 @babel/preset-env를 주로 사용하는데, 이 async함수를 _regeneratorRuntime으로 변환해서, 이번 실험에서는 사용하지 않았다.

2. .babelrc 세팅

{
  "plugins": [
      "@babel/plugin-transform-async-to-generator"
  ]
}

3. 코드 작성

const getValue = async () => {
  const value = await new Promise((resolve)=> setTimeout(resolve,5000)).then(()=> 100)
  return await Promise.resolve(value).then(val => val + 100);
};

4. 코드 변환

npx babel await.js -o output.js

5. 변환 결과

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      _next(undefined);
    });
  };
}

const getValue = /*#__PURE__*/ (function () {
  var _ref = _asyncToGenerator(function* () {
    const value = yield new Promise((resolve) =>
      setTimeout(resolve, 5000)
    ).then(() => 100);
    return Promise.resolve(value).then((val) => val + 100);
  });
  
  return function getValue() {
    return _ref.apply(this, arguments);
  };
})();

getValue();

🍊 하나씩 까보자

getValues

const getValue = /*#__PURE__*/ (function () {
  var _ref = _asyncToGenerator(function* () {
    const value = yield new Promise((resolve) =>
      setTimeout(resolve, 5000)
    ).then(() => 100);
    return yield Promise.resolve(value).then((val) => val + 100);
  });
  
  return function getValue() {
    return _ref.apply(this, arguments);
  };
})();

getValue는 _ref 를 실행한 결과를 반환한다.

_asyncToGenerator

function _asyncToGenerator(fn) {
  return 
  /* 아래 함수를 반환한다 */
  function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      _next(undefined); 
    });
  };
  
}

_ref는 _asyncToGenerator의 결과이므로, Promise를 반환하는 함수이다.

따라서 getValue를 호출하는 것_ref를 호출하는 것이고, 즉 Promise를 반환하는 함수를 호출하는 것이다.

asyncGeneratorStep

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

🛑 잠깐!

gen[key](args) 에서 key는 nextthrow 이다. 그럼 gen 자체에 next와 throw가 존재할까?

function* gen() { 
  yield 1;
  yield 2;
}

const g1 = gen();
console.log(g1)

/*

[[GeneratorLocation]]: ...
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: f* gen()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]

*/

g1을 콘솔로 찍어보면 알 수 있듯이, g1 자체에는 next , throw 키워드는 존재하지 않는다.
해당 key들을 찾기 위해 Prototype 탭을 열어보자.

[[Prototype]]: Generator
	> [[Prototype]]: Generator
   		> constructor: ... 
    	> next: ... // 찾았다!
        > return: ...
        > throw: ... // 여기도 있다!
        Symbol: "Generator" ...
        > [[Prototype]]: Object

그렇다. prototype chaining을 통해 next와 throw를 호출하는 방식이다.

🌊 흐름 정리

이해를 위해 큰 부분만 흐름을 정리해보았다.

1. getValue 호출

2. _next(undefined) 호출

3. 첫 번째 yield 도착

	var info = gen[key](arg); // 여기서 실제로 next가 수행된다 -> gen.next(undefined) 
	console.log(info) // {done:false, value: Promise {<pending>}
    var value = info.value; // Promise{<pending>}

아직 제너레이터 함수의 첫 번재 yield 이기 때문에 else 구문으로 진행한다.
아래 부분에서 Promise.resolve 가 즉시 fullfilled 된 promise를 반환하는 걸로 알고 있어서 이해가 불가능했었다.
하지만 윗 부분에 작성했듯이, Promise.resolve 의 인자가 promise인 경우, 해당 promise를 그대로 리턴한다.

	Promise.resolve(value).then(_next,_throw)

	// 첫 번째 yield 구문과 합쳐보자.
	Promise.resolve(new Promise((resolve)=>setTimeout(resolve,5000)).then(()=>100)).then(_next,_throw)

5초가 지나기 전 까지 pending 상태로 있다가, 5초 후에 resolve를 호출하게 될 것이고, 뒤에 붙은 then 구문까지 처리되어 다음과 같이 변환될 것이다.

	Promise.resolve(new Promise((resolve)=>setTimeout(resolve,5000)).then(()=>100)).then(_next,_throw)
	
	// 코드가 아래 처럼 되는 건 아닌데 (나의) 이해를 돕기 위해 다음과 같이 작성해보았다.
	Promise.resolve(Promise {<fulfilled>: 100}).then(_next,_throw)

위에서 설명 했듯이, Promise.resolve 의 인자가 promise인 경우 해당 promise를 그대로 리턴한다.

그러면, fullfilled된 상태의 promise에 then 메서드가 붙었으므로, _next 를 호출 하면서, _next의 인자로 fullfilled 된 값을 같이 넘겨주게 된다.

	Promise.resolve(Promise {<fulfilled>: 100}).then(_next,_throw)
	
	// 위 코드가 결국 다음 코드를 실행시킨다.
	_next(100)

어라? 제너레이터 인스턴스의 next를 호출 할 때 인자와 같이 전달하면, 해당 인자가 제너레이터 내부로 전달된다는 사실을 알고 있다.

	function* () {
    	const value = yield new Promise((resolve) =>setTimeout(resolve, 5000)).then(() => 100); 
    	return yield Promise.resolve(value).then((val) => val + 100);
  	}

아! 그래서 value에 100이라는 값을 가질 수 있구나! (제일 궁금했던 내용인 듯 😂)
그럼 또 다시 next를 호출 했으니, 위와 같은 방식을 한 번 더 반복하게 된다.
굳이 다른 점을 꼽자면, next를 한번 더 호출할 때는 info.done 값이 true인 정도?

결론

사실 코드 자체는 길지 않은데, Promise가 어떻게 동작하는지 정확하게 알고있지 못했던 것이 시간을 많이 잡아먹는데 큰 원인이 되었다.
비록 시간은 많이 걸렸지만, 처음 async~await을 사용할 때부터 궁금했던 점이라 아주 뿌듯뽀듯 하다. (+ Promise에 대한 이해)
사실 중간에 생략된 내용도 많다. babel/preset-env 에서는 generator를 한단계 더 변환을 하는데, 그 부분은 양이 많아서 차마 엄두가 나지 않는다. 언젠간 도전해봐야지.

참고한 자료

async/await 동작원리 (feat.제너레이터) 정말 도움을 많이 받았다. 이 글을 이해하려고 이 게시물을 썼다고 해도 무방.

MDN Promise

MDN Using Promise

profile
FE 임니다

2개의 댓글

comment-user-thumbnail
2023년 5월 23일

충격적인 썸네일 문구에 이끌려 들어와서 잘 읽고 갑니다. 👍👍

1개의 답글