hmk run dev

Promise와 Generator 그리고 async await 본문

카테고리 없음

Promise와 Generator 그리고 async await

hmk run dev 2023. 12. 18. 22:38

1. 프로미스란?

자바스크립트는 비동기 처리를 위한 하나의 패턴으로 콜백 함수를 사용한다.

하지만 전통적인 콜백 패턴은 콜백 헬로 인해 가독성이 나쁘고 비동기 처리 중 발생한 에러의 처리가 곤란하며 여러 개의 비동기 처리를 한번에 처리하는 데도 한계가 있다.

ES6에서는 비동기 처리를 위한 또 다른 패턴으로 프로미스(Promise)를 도입했다.
프로미스는 전통적인 콜백 패턴이 가진 단점을 보완하며 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.


2. 콜백 패턴의 단점

2.1 콜백 헬

먼저 동기식 처리 모델과 비동기식 처리 모델에 대해 간단히 살펴보자.



동기식 처리 모델

 

동기식 처리 모델(Synchronous processing model)은 직렬적으로 태스크(task)를 수행한다.
즉, 태스크는 순차적으로 실행되며 어떤 작업이 수행 중이면 다음 태스크는 대기하게 된다. 예를 들어 서버에서 데이터를 가져와서 화면에 표시하는 태스크를 수행할 때, 서버에 데이터를 요청하고 데이터가 응답될 때까지 이후의 태스크들은 블로킹된다.

 

 

비동기식 처리 모델

 

비동기식 처리 모델(Asynchronous processing model 또는 Non-Blocking processing model)은 병렬적으로 태스크를 수행한다. 즉, 태스크가 종료되지 않은 상태라 하더라도 대기하지 않고 즉시 다음 태스크를 실행한다. 예를 들어 서버에서 데이터를 가져와서 화면에 표시하는 태스크를 수행할 때, 서버에 데이터를 요청한 이후 서버로부터 데이터가 응답될 때까지 대기하지 않고(Non-Blocking) 즉시 다음 태스크를 수행한다. 이후 서버로부터 데이터가 응답되면 이벤트가 발생하고 이벤트 핸들러가 데이터를 가지고 수행할 태스크를 계속해 수행한다. 자바스크립트의 대부분의 DOM 이벤트와 Timer 함수(setTimeout, setInterval), Ajax 요청은 비동기식 처리 모델로 동작한다.

 


자바스크립트에서 빈번하게 사용되는 비동기식 처리 모델은 요청을 병렬로 처리하여 다른 요청이 블로킹(blocking, 작업 중단)되지 않는 장점이 있다.

비동기는 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 방식이기 때문에, 

 

 

만일 비동기 작업의 결과에 따라 다른 작업을 수행해야 할 때는 전통적으로 콜백 함수를 사용했다. 

 

하지만 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 네스팅(nesting, 중첩)되어 복잡도가 높아지는 콜백 헬(Callback Hell)이 발생하는 단점이 있다. 콜백 헬은 가독성을 나쁘게 하며 실수를 유발하는 원인이 된다. 아래는 콜백 헬이 발생하는 전형적인 사례이다.

 

 

 

 

2.2 에러 처리의 한계

콜백 방식의 비동기 처리가 갖는 문제점 중에서 가장 심각한 것은 에러 처리가 곤란하다는 것이다. 아래의 코드를 살펴보자.

 

try {
  setTimeout(() => { throw new Error('Error!'); }, 1000);
} catch (e) {
  console.log('에러를 캐치하지 못한다..');
  console.log(e);
}

 

 

try 블록 내에서 setTimeout 함수가 실행되면 1초 후에 콜백 함수가 실행되고 이 콜백 함수는 예외를 발생시킨다.
하지만 이 예외는 catch 블록에서 캐치되지 않는다. 그 이유에 대해 알아보자.

 

 

1. setTimeout의 비동기 특성:
setTimeout 함수는 비동기 함수로, 콜백 함수가 실행될 때까지 기다리지 않고 즉시 종료되어 호출 스택에서 제거됩니다.


2. 콜백 함수 호출 시점:
setTimeout으로 예약한 콜백 함수는 1초 후에 실행됩니다.


3. 콜백 함수와 호출 스택:
이때, setTimeout 함수는 이미 호출 스택에서 제거된 상태입니다.

 

4. 예외의 호출자 방향 전파:
예외는 호출자 방향으로 전파되는데, setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout 함수가 아니라서 호출 스택에 setTimeout 함수가 없습니다.


5. catch 블록에서의 문제:
따라서, try 블록 내의 setTimeout 콜백 함수에서 발생한 예외는 호출 스택에 올라가지 않아 catch 블록에서 캐치되지 않습니다.

 

- setTimeout 함수는 비동기로 동작하며, 콜백 함수가 호출되는 것이 이벤트 루프에서 발생 
- try-catch 블록이 이미 완료된 후에 콜백 함수가 실행되므로 catch 블록에서는 에러를 잡을 수 없습니다.

 

이벤트 루프의 비동기 함수 처리 방식

 

 

이러한 문제를 극복하기 위해 Promise가 제안되었다. Promise는 ES6에 정식 채택되어 IE를 제외한 대부분의 브라우저가 지원하고 있다.



위 의 코드를 Promise를 사용하여 개선이 가능하다.

(async () => {
    try {
        await new Promise((resolve, reject) => {
            throw new Error('error with async await');
        });
    } catch (e) {
        console.log('에러를 캐치했다!');
        console.log(e.message); // 에러 메시지만 출력하도록 수정
    }
})();



3. 프로미스의 생성

프로미스는 Promise 생성자 함수를 통해 인스턴스화한다. Promise 생성자 함수는 비동기 작업을 수행할 콜백 함수를 인자로 전달받는데 이 콜백 함수는 resolve와 reject 함수를 인자로 전달받는다.

 

// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.

  if (/* 비동기 작업 수행 성공 */) {
    resolve('result');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('failure reason');
  }
});

 

 

Promise는 비동기 처리가 성공(fulfilled)하였는지 또는 실패(rejected)하였는지 등의 상태(state) 정보를 갖는다.

 

상태 의미  구현
pending 비동기 처리가 아직 수행되지 않은 상태 resolve 또는 reject 함수가 아직 호출되지 않은 상태
fulfilled 비동기 처리가 수행된 상태 (성공) resolve 함수가 호출된 상태
rejected 비동기 처리가 수행된 상태 (실패) reject 함수가 호출된 상태
settled 비동기 처리가 수행된 상태 (성공 또는 실패) resolve 또는 reject 함수가 호출된 상태

 

 

promise lifeCycle

 

 

오후 수집기 예시)

 

상품 수집기의 sendMessage 함수

 

background에 메세지를 보내고 응답값을 리턴하는 함수

export function sendMessage(
  message: ContentMessagePacket,
  timeout = 10000,
): Promise<SendResponseStatus> {
  return new Promise((resolve, reject) => {
    const { type, data } = message;

	// 응답이 10초 이상 지연될 경우 에러로 간주하고 에러 발생
    const timeoutId = setTimeout(() => {
      reject(new Error('sendMessage: Timeout exceeded'));
    }, timeout);

    try {
      chrome.runtime.sendMessage({ type, data }, (response) => {
        
        // 메세지 응답이 오면 타이머 해제
        clearTimeout(timeoutId);
		
        // 백그라운드로 부터 응답 받은 메세지에 대한 처리
        if (chrome.runtime.lastError) {
          reject(new Error(`sendMessage: ${chrome.runtime.lastError.message}`));
        } else {
          resolve(response);
        }
      });
    } catch (error) {
      clearTimeout(timeoutId);
      reject(new Error(`sendMessage: ${error}`));
    }
  });
}

 

 

사용예시

 

keyword(소싱 사이트에서 추출한)를 기반으로 백그라운드에 main keyword를 응답 받는 함수

export async function getMainKeyword(keyword: string): Promise<string> {
  const result = await sendMessage({
    type: 'getMainKeyword',
    data: keyword,
  });

  return result;
}

 

 

4. Promise 정적 메서드

 

Promise.all

Promise.all 메소드는 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다. 그리고 전달받은 모든 프로미스를 병렬로 처리하고 그 처리 결과를 resolve하는 새로운 프로미스를 반환한다. 아래 예제를 살펴보자.

 

Promise.all([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve(3), 1000))  // 3
]).then(console.log) // [ 1, 2, 3 ]
  .catch(console.log);

 

Promise.all 메소드는 3개의 프로미스를 담은 배열을 전달받았다. 각각의 프로미스는 아래와 같이 동작한다.

  • 첫번째 프로미스는 3초 후에 1을 resolve하여 처리 결과를 반환한다.
  • 두번째 프로미스는 2초 후에 2을 resolve하여 처리 결과를 반환한다.
  • 세번째 프로미스는 1초 후에 3을 resolve하여 처리 결과를 반환한다.

 

Promise.all 메소드는 전달받은 모든 프로미스를 병렬로 처리한다. 이때 모든 프로미스의 처리가 종료될 때까지 기다린 후 아래와 모든 처리 결과를 resolve 또는 reject한다.

  • 모든 프로미스의 처리가 성공하면 각각의 프로미스가 resolve한 처리 결과를 배열에 담아 resolve하는 새로운 프로미스를 반환한다. 이때 첫번째 프로미스가 가장 나중에 처리되어도 Promise.all 메소드가 반환하는 프로미스는 첫번째 프로미스가 resolve한 처리 결과부터 차례대로 배열에 담아 그 배열을 resolve하는 새로운 프로미스를 반환한다. 즉, 처리 순서가 보장된다.
  • 프로미스의 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

Promise.all([
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 1!')), 3000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 2!')), 2000)),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('Error 3!')), 1000))
]).then(console.log)
  .catch(console.log); // Error: Error 3!

 

 

위 예제의 경우, 세번째 프로미스가 가장 먼저 실패하므로 세번째 프로미스가 reject한 에러가 catch 메소드로 전달된다.

Promise.all 메소드는 전달 받은 이터러블의 요소가 프로미스가 아닌 경우, Promise.resolve 메소드를 통해 프로미스로 래핑된다.

 

 

Promise.allSettled

 

Promise.all의 경우, 하나의 프로미스라도 실패하면 에러로 처리되었다. 

반면, Promise.allSettled는 여러 프로미스를 병렬적으로 처리하되, 하나의 프로미스가 실패해도 무조건 이행한다.



const promise1 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(1);
  }, 3000);
});

const promise2 = new Promise((resolve) => {
  setTimeout(() => {
    resolve(2);
  }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error('다 무시하고 에러내버려!'));
  }, 2500);
});

Promise.allSettled([promise1, promise2, promise3])
  .then((result) => console.log(result))
  .catch((e) => console.error(e));

 

위 코드의 실행결과

 

 

Promise.race

 

Promise.race 메소드는 Promise.all 메소드와 동일하게 프로미스가 담겨 있는 배열 등의 이터러블을 인자로 전달 받는다. 그리고 Promise.race 메소드는 Promise.all 메소드처럼 모든 프로미스를 병렬 처리하는 것이 아니라 가장 먼저 처리된 프로미스가 처리 결과를 반환한다.

 

Promise.race([
  new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
  new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
  new Promise(resolve => setTimeout(() => resolve('에러가 가장먼저 발생'), 1000))  // 3
]).then(console.log) // 3
  .catch(console.log);


에러가 발생한 경우는 Promise.all 메소드와 동일하게 처리된다. 즉, Promise.race 메소드에 전달된 프로미스 처리가 하나라도 실패하면 가장 먼저 실패한 프로미스가 reject한 에러를 reject하는 새로운 프로미스를 즉시 반환한다.

 

Promise.any

 

Promise.any역시 이터러블한 Promise들을 인자로 받는다. 그리고 Promise.race와 아주 유사하게 동작한다. 하지만 다른점은 Promise.any는 가장 먼저 resolve된 Promise를 반환하며 단락된다.

 

Promise.any([
  new Promise((res, rej) => setTimeout(() => rej(1), 1000)),
  new Promise((res) => setTimeout(() => res(3), 2000)),
  new Promise((res) => setTimeout(() => res(2), 3000)),
]).then(console.log);

 

 

 

5. 제너레이터

 

제너레이터 함수는 일반 함수와는 다른 독특한 동작을 한다.

제너레이터 함수는 일반 함수와 같이 함수의 코드 블록을 한 번에 실행하지 않고,

함수 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재시작할 수 있는 특수한 함수이다.

function* counter() {
  console.log('첫번째 호출');
  yield 1;                  // 첫번째 호출 시에 이 지점까지 실행된다.
  console.log('두번째 호출');
  yield 2;                  // 두번째 호출 시에 이 지점까지 실행된다.
  console.log('세번째 호출');  // 세번째 호출 시에 이 지점까지 실행된다.
}

const generatorObj = counter();

console.log(generatorObj.next()); // 첫번째 호출 {value: 1, done: false}
console.log(generatorObj.next()); // 두번째 호출 {value: 2, done: false}
console.log(generatorObj.next()); // 세번째 호출 {value: undefined, done: true}

 

 

일반 함수를 호출하면 return 문으로 반환값을 리턴하지만 제너레이터 함수를 호출하면 제너레이터를 반환한다. 이 제너레이터는 이터러블(iterable)이면서 동시에 이터레이터(iterator)인 객체이다. 

// 제너레이터 함수 정의
function* counter() {
  console.log('Point 1');
  yield 1;                // 첫번째 next 메소드 호출 시 여기까지 실행된다.
  console.log('Point 2');
  yield 2;                // 두번째 next 메소드 호출 시 여기까지 실행된다.
  console.log('Point 3');
  yield 3;                // 세번째 next 메소드 호출 시 여기까지 실행된다.
  console.log('Point 4'); // 네번째 next 메소드 호출 시 여기까지 실행된다.
}

// 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
// 제너레이터 객체는 이터러블이며 동시에 이터레이터이다.
// 따라서 Symbol.iterator 메소드로 이터레이터를 별도 생성할 필요가 없다
const generatorObj = counter();

// 첫번째 next 메소드 호출: 첫번째 yield 문까지 실행되고 일시 중단된다.
console.log(generatorObj.next());
// Point 1
// {value: 1, done: false}

// 두번째 next 메소드 호출: 두번째 yield 문까지 실행되고 일시 중단된다.
console.log(generatorObj.next());
// Point 2
// {value: 2, done: false}

// 세번째 next 메소드 호출: 세번째 yield 문까지 실행되고 일시 중단된다.
console.log(generatorObj.next());
// Point 3
// {value: 3, done: false}

// 네번째 next 메소드 호출: 제너레이터 함수 내의 모든 yield 문이 실행되면 done 프로퍼티 값은 true가 된다.
console.log(generatorObj.next());
// Point 4
// {value: undefined, done: true}



loop를 이용해서 호출이 가능하다.

for (const v of counter()) {
  console.log(v);
}

 

 

제너레이터를 활용한 비동기처리

 

제너레이터를 사용해 비동기 처리를 동기 처리처럼 구현할 수 있다. 다시 말해 비동기 처리 함수가 처리 결과를 반환하도록 구현할 수 있다.

const fetch = require('node-fetch');

function getUser(genObj, username) {
  fetch(`https://api.github.com/users/${username}`)
    .then(res => res.json())
    // ① 제너레이터 객체에 비동기 처리 결과를 전달한다.
    .then(user => genObj.next(user.name));
}

// 제너레이터 객체 생성
const g = (function* () {
  let user;
  // ② 비동기 처리 함수가 결과를 반환한다.
  // 비동기 처리의 순서가 보장된다.
  user = yield getUser(g, 'jeresig');
  console.log(user); // John Resig

  user = yield getUser(g, 'ahejlsberg');
  console.log(user); // Anders Hejlsberg

  user = yield getUser(g, 'ungmo2');
  console.log(user); // Ungmo Lee
}());

// 제너레이터 함수 시작
g.next();

 

 

1. 비동기 처리가 완료되면 next 메소드를 통해 제너레이터 객체에 비동기 처리 결과를 전달한다.

2. 제너레이터 객체에 전달된 비동기 처리 결과는 user 변수에 할당된다.

제너레이터을 통해 비동기 처리를 동기 처리처럼 구현할 수 있으나 코드는 장황해졌다.

따라서 좀 더 간편하게 비동기 처리를 구현할 수 있는 async/await가 ES7에서 도입되었다.

위 예제를 async/await 구현해 보자.

 

 

6. Async Await

 

async/await는 ES2017에 도입된 문법으로서, Promise 로직을 더 쉽고 간결하게 사용할 수 있게 해준다. 
유의해야 할 점이 async/await가 Promise를 대체하기 위한 기능이 아니라는 것이다. 
내부적으로는 여전히 Promise를 사용해서 비동기를 처리하고, 
단지 코드 작성 부분을 프로그래머가 유지보수하게 편하게 보이는 문법만 다르게 해줄 뿐이라는 것이다. 

제너레이터에 비해 간결해진 코드,

마치 동기적으로 코드를 구성할 수 있어서 프로그래머 입장에선 더 가독성 좋은 코드를 작성할 수 있다.

const fetch = require('node-fetch');

// Promise를 반환하는 함수 정의
function getUser(username) {
  return fetch(`https://api.github.com/users/${username}`)
    .then(res => res.json())
    .then(user => user.name);
}

async function getUserAll() {
  let user;
  user = await getUser('jeresig');
  console.log(user);

  user = await getUser('ahejlsberg');
  console.log(user);

  user = await getUser('ungmo2');
  console.log(user);
}

getUserAll();

 

 

 

제너레이터로 구현한 async/await

 

 

before-transpiling 코드를 Babel을 이용하여 ES6로 트랜스파일링을 하면 after-transpiling 코드가 됩니다.

 

 

 

before-transpiling

const outerFn = async () => {
  const val = await new Promise((resolve) => setTimeout(resolve, 1000)).then(
    () => 123
  );
  return await Promise.resolve(val).then((v) => 3 * v);
};

outerFn().then((val) => console.log(val)); // 369

 



"use strict";

// 제너레이터 함수의 각 단계를 수행하는 helper 함수
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg); // 현재 제너레이터 함수의 key에 해당하는 메소드 호출 (예: next() 또는 throw())
    var value = info.value; // 제너레이터 함수에서 반환된 값
  } catch (error) {
    reject(error); // 에러가 발생하면 reject 호출
    return;
  }
  if (info.done) {
    resolve(value); // 제너레이터 함수가 완료되면 최종 값을 resolve
  } else {
    Promise.resolve(value).then(_next, _throw); // 비동기적으로 다음 단계 호출
  }
}

// 비동기 함수를 Generator 함수로 변환하는 helper 함수
function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      // 비동기 함수를 호출하여 제너레이터 생성
      var gen = fn.apply(self, args);

      // _next와 _throw 함수를 정의하여 각각 다음 단계와 예외 처리를 수행
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }

      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }

      _next(undefined); // 제너레이터 함수의 첫 단계 시작
    });
  };
}

// outerFn을 Generator 함수로 변환
// outerFn은 클로저로 감싸져 있고 내부에는 Generator 함수가 정의 되어 있음
// Generator 함수는 _asyncToGenerator 함수로 래핑되어 있습니다
const outerFn = /*#__PURE__*/ (function () {
  var _ref = _asyncToGenerator(function* () {
    // Generator 함수 내에서 yield를 사용하여 비동기 동작을 표현
    const val = yield new Promise((resolve) => setTimeout(resolve, 1000)).then(
      () => 123
    );
    return yield Promise.resolve(val).then((v) => 3 * v);
  });
  return function outerFn() {
    return _ref.apply(this, arguments);
  };
})();

// outerFn을 호출하고 결과를 출력
outerFn().then((val) => console.log(val)); // 369



return _ref.apply(this, arguments);

더보기

클로저(Closure)는 함수가 선언될 때의 환경(스코프)을 기억하고, 그 함수가 다른 스코프에서 호출될 때에도 해당 환경에 접근할 수 있도록 하는 개념입니다. 클로저는 내부 함수가 외부 함수의 변수에 접근할 수 있게 하므로, 외부 함수의 변수가 사라져도 내부 함수에서는 여전히 사용할 수 있습니다.



아래 코드에 대한 설명

return _ref.apply(this, arguments);

함수 선언문 내부에서의 this는 해당 함수가 호출될 때의 컨텍스트를 가리킵니다.
일반적으로 함수 선언문 내에서의 this는 실행 컨텍스트에 따라 동적으로 결정됩니다.
이때 함수가 메소드로서 호출되면 this는 호출한 객체를 가리키고, 일반 함수로서 호출되면 this는 전역 객체를 가리키게 됩니다.

클로저나 Generator 함수와 같은 경우에는 함수가 정의될 때의 this를 유지하기 위해서 주로 _ref.apply(this, arguments)와 같은 패턴을 사용합니다. 이렇게 함으로써 함수 내에서 비동기적인 작업이나 Generator 함수가 일시 중지되었을 때, 해당 함수가 속한 객체나 컨텍스트에 접근할 수 있습니다.

함수가 선언될 당시의 컨텍스트를 유지하려면 클로저를 활용하거나 함수를 호출할 this 명시적으로 지정하는 방법을 사용할 있습니다. 이를 통해 함수의 동작을 명확하게 제어하고 원하는 대로 컨텍스트를 전달할 있습니다.

 




after-transpiling

 

코드 동작 설명

  1. 38~48번 라인에 걸쳐 정의된 함수를 즉시실행시킴으로써 _asyncToGenerator 함수를 호출시킵니다.
  2. _asyncToGenerator 함수의 인자로 fn을 받는데 위 코드에서 fn은 39~44번 라인의 제너레이터입니다. _asyncToGenerator 함수는 Promise를 반환하는 함수를 반환합니다. 이로 인해 50번 라인의 outerFn()은 Promise가 되며 then으로 등록한 (val) => console.log(val)은 22번 라인의 Promise의 resolve로 등록됩니다. (잘 이해가 안가시면 이전 글의 "Promise를 구현해보자"를 보고 오시면 도움이 될 것 같습니다.)
  3. 33번 라인에서 _next()를 호출하여 asyncGeneratorStep() 함수를 호출합니다. 이 함수의 각 인자는 다음과 같습니다.
  • gen: 23번 라인에서 생성된 제너레이터(39~44번 라인에 정의된 제너레이터 함수)
  • resolve: 50번 라인의 outerFn()의 then으로 등록한 (val) => console.log(val) 함수3
  • _next: 25번 라인에 정의된 _next() 함수
  • key: "next" 또는 "throw". (gen.next() 또는 gen.throw()를 호출하기 위함)
  • arg: 아래에서 설명
  1. 5 라인에서 gen[key](arg) 호출합니다key next 혹은 throw이므로gen.next(arg) 또는 gen.throw(arg) 호출하는 것과 같습니다.(올바르게 동작한다고 가정하여 gen.next(arg) 설명하겠습니다.) 호출로 40 라인에서 yield Promise value, 제너레이터는 아직 끝나지 않았으므로 done false 객체가 info 할당됩니다. 따라서, 14 라인을 수행하게됩니다. 14 라인의 resolve 들어가는 value 1 123 되는 Promise이며_next 위에서 설명한대로 25 라인에 정의된 _next() 함수입니다. 1초뒤에 123 인자로 하여 _next(123) 호출되겠네요.
  2. 4번이 수행완료되어 _next(123) 호출됩니다. 이에 따라 5 라인에서 gen.next(123) 호출됩니다next(args)에서 설명한대로next() 함수에 인자로 123 들어갔으므로 40 라인의 val 123 됩니다. (await처럼 동작한 같지 않나요? 🤔) 그리고 43 라인에 return 있지만 yield 키워드가 먼저 실행되어 value Promise, done false 객체가 info 할당됩니다value 369 되는 Promise 14 라인을 수행하게 됩니다. (43 라인의 then((v) => 3 * v) 인해 3 * 123 수행됩니다.)
  3. 5번이 수행되고 value Promise에서 369 나오면 next(369) 호출되어 gen.next(369) 호출됩니다. 따라서 43 라인의 return문은 return 369 되어{ value: 369, done: true } info 할당됩니다.
  4. info.done true이므로 12 라인의 resolve(369) 호출되어 console.log(369) 호출됩니다. (3 설명에서 resolve (val) => console.log(val)이었습니다.)





yield 키워드를 활용하여 await의 동작이 수행완료될 때까지 멈추는 것, 
next(args)를 이용해 await의 동작이 완료되면 값을 꺼내어 할당해주는 것.
이 async/await의 동작을 제너레이터를 활용하여 훌륭하게 구현되어 있다고 생각하지 않나요?




Reference

https://www.timegambit.com/blog/digging/async-await/01

 

[async/await] 동작원리 (feat. 제너레이터)

자바스크립트의 제너레이터 개념과 사용법을 정리하고, 이를 이용한 async/await의 동작원리를 분석해보았습니다.

www.timegambit.com


https://poiemaweb.com/es6-promise

Comments