Published on

ES6) Generator & Async/Await

Authors
  • avatar
    Name
    Hoehyeon Jung
    Twitter

들어가며

이전 포스트에서 반복을 다루는 멋진 방법으로 Iterator/Iterable 프로토콜에 대해서 다뤄보았다. 자바스크립트에서는 이러한 반복을 다루는 또다른 방법으로 Generator를 제공하고 있다. 또한 비동기를 동기처럼 다룰 수 있도록 await/async도 다루고 있다. 이러한 Generator와 await/async의 개념과 용법에 대해 알아보자.

Generator

ES6에 들어서면서 자바스크립트는 loop를 처리하는 데 있어 Iterable 프로토콜이라는 훌륭한 해결책을 제시했다. loop를 돌면서 Iterator를 반환하는 Iterable 프로토콜은 루프를 도는데 있어 새로운 해결책을 제시한다. Iterator는 loop를 돌면서 순환된 값을 저장하면서 진행한다. 중단된 부분부터 진행하는 등의 기능은 지연 평가(lazy Evaluation)등이 가능한 점이 있다.

본론으로 돌아와서 Generator는 함수 역시 이러한 Iterable 프로토콜을 만족하도록 기능을 내장(Built-in)시킨 것이다. 아래의 예제를 보며 실제 사용 예제를 확인해보자.

// Iterable Function before Generator
function fibonacciC(n) {
  const infinite = !n && n !== 0;
  let [current, next] = [0, 1];

  return {
    [Symbol.iterator]() { return this; },
    next() {
      while (infinite || n--) {
        const temp = current;
        [current, next] = [next, current + next];
        return {value: temp, done: false};
      }
      return {value: undefined, done: true};
    }
  }
}

function* fibonacci(n) {
  const infinite = !n && n !== 0;
  let [current, next] = [0, 1];

  while (infinite || n--) {
    yield current;
    [current, next] = [next, current + next];
  }
}

const first10 = [...fibonacci(10)];
console.log(first10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Generator의 기본적인 사용방법은 function* () {/*...*/}로 기존의 함수 선언식에 *을 붙이는 것으로 간단하게 구현할 수 있다. 또한 yield를 만나면 함수가 중단(suspended)되고 제어권이 호출자에게로 넘어간다. 기존의 Well-Formed Iterable 프로토콜을 만족하기 위해서는 다음과 같은 조건이 필요하다.

[Symbol.iterator] property를 가져야 하고 Iterator를 반환해야 한다. (상세 링크)

이러한 프로토콜을 자체적으로 내장한 것이 Generator이다. 또한 Iterable 프로토콜을 만족하기 때문에 Spread, Rest, 비구조화 할당 등의 문법을 활용 가능하다. 특이한 점으로 보자면 일반적인 함수의 경후 return에서 코드의 반환이 이루어지기 때문에 return 밑에 코드는 무시되지만, yield 이후에 있는 코드도 실행이 되며 yield를 만나면 중단이 된다.

Async/Await

자바스크립트는 기본적으로 Non-blocking, Single Thread 언어기 때문에 비동기 함수의 순서를 제어하기 위해서는 Callback 함수나 이를 감싼 객체인 Promise를 활용해왔다. 링크

하지만, 비동기를 제어하기 위한 기존의 방법들에 있어서 아쉬운 점은 비동기와 동기적 코드가 조화되기 힘들다는 것이다. 이러한 문제를 해결하기 위해 도입된 async/await 문법은 비동기적인 코드를 동기적으로 보이게 만들어준다.

// return Promise
const getGithubId = function (username) {
  return fetch(`https://api.github.com/users/${username}`)
    .then(res => res.json())
    .then(user => user.id);
}

const getNamesP = function(usernames) {
  const result = [];
  for (const username of usernames) {
    getGithubId('gonewbie')
      .then(name => result.push(name));
  }
  return result;
}

const getNames = async function(usernames) {
  const result = [];
  for (const username of usernames) {
    const name = await getGithubId(username);
    result.push(name);
  }
  console.log(result);
}

getNames(['gonewbie', 'gonewbie']);

async/await 이 도입되면서부터 비동기를 동기적으로 처리할 수 있게 되었다. 또한 동기적으로 처리할 수 있게 되면서 try/catch를 이용하여 에러 처리를 할 수 있게 되었다.

async/await은 또한 기존의 Promise와도 어울릴 수 있다.

const getIds = async usernames => {
  const result = [];
  for (const username of usernames) {
    const id = Promise.resolve('go');
    result.push(id);
  }
  console.log(await Promise.all(result));
}

getIds(['gonewbie', 'gonewbie']);

Promise 객체로 Wrapping된 값이 있더라도 await을 만나면 Promise 객체 내부의 value를 반환하게 된다.

코루틴(Coroutine)

일반적으로 프로그래밍에서 함수란 특정 동작을 수행하는 일련의 코드 부분을 의미한다. 이를 다른 말로 Subroutine이라고 한다. 함수의 기본적인 특징은 caller가 함수를 call하고 callee 함수는 call stack에 쌓여 역할을 수행하여 값을 return하고 소멸된다. Routine, Subroutine 등에 해당되는 함수들은 기본적으로 call이 끝나면 소멸되고 중간에 중단되었다가 추후에 다시 수행하거나 할 수 없고 처음부터 다시 실행해야 한다.

하지만 Generator는 yield를 만나면 함수가 suspend로 된 상태로 제어권을 넘기게 된다. 또한 중간에 중단되었다가 다시 작동할 경우 중단된 부분부터 수행된다. 이러한 특성을 Coroutine이라고 한다.

함수를 수행하면서 특정 부분에서 함수를 중단하고 제어권을 넘겨준다. 제어권을 넘겨준다는 측면에서보면, 비동기 함수를 처리하는 Callback 함수, Promise와 비슷한 동작을 수행한다. 그렇다면 이 두 함수가 제어권을 서로 넘기면 어떤 역할을 수행할 수 있을까?

비동기 처리에서 두 함수를 이용해보면 어떨까?

// 코드 수정 중
const getGithubId = function (username) {
  return fetch(`https://api.github.com/users/${username}`)
    .then(res => res.json())
    .then(user => user.id);
}

const getNamesG = function* (usernames) {
  const result = [];
  for (const username of usernames) {
    const id = yield getGithubId(username);
    console.log(id);
    result.push(id);
  }
  return result;
};

const g = getNamesG(['gonewbie', 'gonewbie']);
g.next().value.then(
  _ => g.next().value.then(
    __ => g.next()
  )
);

위 코드를 자세히 살펴보면 다음과 같은 과정을 거진다.

  1. g.next()에서 Generator가 실행되고 for .. of 내부에서 yield가 수행될 떄까지 Promise를 반환하며 suspend 상태로 Promise에 제어권이 넘어간다.
  2. Promise.then()에서 다시 Generator를 실행하며 Generator에 제어권을 넘겨준다.
  3. id를 저장하고 result에 id를 push한 후 다시 api에서 Promise를 리턴하면서 제어권을 Promise에 넘겨준다.
  4. Promise.then()에서 다시 Generator를 수행하면서 제어권을 넘겨줃다.
  5. Generator에서 loop 내에서 id를 저장하고 해당 id를 result에 다시 push한다. 또한 loop가 끝나면서 result에 저장된 id들을 반환한다.

코루틴의 특성을 이용해 비동기 처리에 있어서 제어권을 Promise와 Generator 사이에서 핑퐁과 같이 제어권을 서로 전달하면서 함수를 수행하는 것이 가능해진다.

마치며

Generator와 Async의 기본적인 함수 사용법과 활용법 그리고 Generator와 Async/Await의 근간을 이루는 Coroutine에 대한 개념과 이를 활용한 방법들을 확인해보았다. 아직 개념이나 활용법에 대해 익숙치 않은 부분도 있지만 충분히 강력하고 효율적인 도구이기 때문에 개념을 익혀서 실무에서 활용하도록 노력해야 겠다.

참고