JavaScript - Generator & async/await

이소라·2022년 12월 20일
0

JavaScript

목록 보기
15/22

1. 제네레이터(Generator)

  • ES6에서 도입된 함수
  • 코드 블록의 실행을 일시 중시했다가 필요한 시점에 재개할 수 있는 특수한 함수

1.1 제네레이터와 일반 함수의 차이점

일반 함수제네레이터
함수의 제어권함수를 호출하면 제어권이 함수에게 넘어가므로, 함수 호출자가 함수 실행 제어 X제네레이터 함수는 함수 호출자가 함수 실행을 제어 O
함수 호출자와 함수 상태함수가 실행되고 있는 동안에는 함수 외부에서 내부로 값을 전달하여 상태 변경 X제네레이터 함수는 함수 호출자에게 상태를 전달하기 O, 함수 호출자로부터 상태 전달받기 O
반환값함수를 호출하면 함수 코드를 실행하고 값을 반환함제네레이터 함수를 호출하면 제네레이터 객체를 반환함

1.2 제네레이터 함수의 정의

  • 제네레이터 함수는 function*키워드로 선언함
    • *의 위치는 function 키워드와 함수 이름 사이의 어디든지 상관없음
    • 일관성 유지를 위해 function 키워드 바로 뒤에 붙이는 것을 권장함
function* genFunc() {yield 1; } // 권장
function * genFunc() {yield 1; }
function *genFunc() {yield 1; }
function*genFunc() {yield 1; }
  • 제네레이터 함수는 하나 이상의 yield 표현식을 포함함
// 제네레이터 함수 선언문
function* genDecFunc() {
  yield 1;
}

// 제네레이터 함수 표현식
const genExpFunc = function* () => {
  yield 1;
}

// 제네레이터 메서드
const obj = {
  * genObjMethod() {
    yield 1;
  }
}

// 제네레이터 클래스 메서드
class MyClass {
  * genClsMethod() {
    yield 1;
  }
}
  • 제네레이터 함수는 화살표 함수로 정의할 수 없음
const genArrowFunc = * () => {
  yield 1;
}; // SyntaxError : Unexpected token '*'
  • 제네레이터 함수는 new 연산자와 함께 생성자 함수로 호출할 수 없음
    • 제네레이터 함수는 언제나 프로미스를 반환해야하므로, 인스턴스를 반환하는 생성자 함수로 호출할 수 없음
function* genFunc() {
  yield 1;
}

new genFunc(); // TypeError: genFunc is not a constructor

1.3 제네레이터 객체

  • 제네레이터 함수를 호출하면 제네레이터 객체를 생성해 반환함

    • 제네레이터 객체는 이터러블(iterable)이면서 이터레이터(iterator)
      • Symbol.iterator 메서드를 상속받는 이터러블
      • value, done 프로퍼티를 갖는 이터러블 리젝트 객체를 반호나하는 next 메서드를 소유하는 이터레이터
  • 이터러블(iterable)과 이터레이터(iterator)

    • 이터러블(iterable)
      • 이터러블 프로토콜(iterable protocol)을 준수하는 객체
      • for ... of 문으로 순회 가능함
      • 스프레드 문법과 구조 분해 할당의 대상으로 사용 가능함
    • 이터레이터(iterator)
      • 이터레이터 프로토콜(iterator protocol)을 준수하는 객체
      • 이터러블의 요소를 탐색하기 위한 포인터 역할을 함

Note

  • 이터러블 프로토콜(iterable protocol)
    • Symbol.iterator를 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환함
  • 이터레이터 프로토콜(iterator protocol)
    - 이터레이터는 next 메서드를 소유하며 next 메서드를 호출하면 이터러블을 순회하며 value와 done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환함
  • 제너레이터 객체는 next, return, throw 메서드를 가짐
    • next 메서드를 호출할 경우,

      • 제네레이터 함수의 yield 표현식까지 코드 블럭을 실행함
      • yield된 값을 value 프로퍼티 값으로, false를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환함
    • return 메서드를 호출할 경우,

      • 인수로 전달받은 값을 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환함
    • throw 메서드를 호출할 경우,

      • 인수로 전달받은 에러를 발생시킴
      • undefined를 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환함
function* genFunc() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch(e) {
    console.error(e);
  }
}

const generator = genFunc();

console.log(generator.next()); // {value: 1, done: false}
console.log(generator.return('End!')); // {value: 'End!', done: true}
console.log(generator.throw('Error!'); // {value: undefined, done: true}

1.4 제네레이터의 일시 중지와 재개

  • 제네레이터는 yield 키워드와 next 메서드를 통해 실행을 일시 중지했다가 필요한 시점에 다시 재개할 수 있음

    • yield 키워드
      • 제네레이터 함수의 실행을 일시 중지시킴
      • yield 키워드 뒤에 오는 표현식의 평가 결과를 제네레이터 함수 호출자에게 반환함
    • next 메서드
      • next 메서드를 호출하면, yield 표현식까지 실행되고 일시 중지됨
      • value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환함
        • value 프로퍼티 : yield 표현식에서 yield된 값이 할당됨
        • done 프로퍼티 : 제네레이터 함수가 끝까지 실행되었는지를 나타내는 불리언 값이 할당됨
  • next 메서드를 반복 호출하여 제네레이터 함수가 끝까지 실행될 경우, 아래와 같은 값의 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환함

    • value 프로퍼티 : 제네레이터 함수의 반환값이 할당됨
    • done 프로퍼티 : 제네레이터 함수가 끝까지 실행되었음을 나타내는 true가 할당됨
function* genFunc() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = genFunc();

// 처음 next 메서드를 호출하면, 첫 번째 yield 표현식까지 실행되고 일시 중지됨
console.log(generator.next()); // {value: 1, done: false}

// 다시 next 메서드를 호출하면, 두 번째 yield 표현식까지 실행되고 일시 중지됨
console.log(generator.next()); // {value: 2, done: false}

// 다시 next 메서드를 호출하면, 세 번째 yield 표현식까지 실행되고 일시 중지됨
console.log(generator.next()); // {value: 3, done: false}

// 다시 next 메서드를 호출하면, 남은 yield 표현식이 없으므로 제네레이터 함수가 마지막까지 실행됨
console.log(generator.next()); // {value: undefined, done: true}
  • 제네레이터 객체의 next 메서드에는 인수를 전달할 수 있음
    • next 메서드에 전달한 인수는 제네레이터 함수의 yield 표현식을 할당받는 변수에 할당됨
    • yield 표현식을 할당받는 변수에 yield 표현식의 평가 결과가 할당되지 않음
function* genFunc() {
  // 처음 next 메서드를 호출하면, 첫 번째 yield 표현식까지 실행되고 일시 중지됨
  // yield된 1은 이터레이터 리절트 객체의 value 프로퍼티에 할당됨
  // x 변수에는 아직 할당 안됨, x 변수의 값은 next가 두 번째 호출될 때 결정됨
  const x = yield 1;
  
  // 두 번째 next 메서드를 호출할 때 전달된 인수 10은 첫 번째 yield 표현식을 할당받는 x에 할당됨
  // 두 번째 next 메서드를 호출하면, 두 번째 yield 표현식까지 실행되고 일시 중지됨
  // 이 때 yield된 값 x + 10은 이터레이터 객체의 value 프로퍼티에 할당됨
  const y = yield (x + 10);
  
  // 세 번째 next 메서드를 호출할 때 전달된 인수 20은 두 번째 yield 표현식을 할당받는 변수 y에 할당됨
  // 세번 째 next 메서드를 호출하면, 남아있는 yield 표현식이 없으므로 함수 끝까지 실행됨
  // 함수의 반환값 x + y는 이터레이터 객체의 value 프로퍼티에 할당됨
  return x + y;
}

const generator = genFunc();

// 처음 호출하는 next 메서드에는 인수를 전달하지 않음
// 만약 처음 호출하는 next 메서드에 인수를 전달하면 무시됨
// 이터레이터 리절트 객체의 value 프로퍼티에 첫 번째 yield된 값 1이 할당됨
let rest = generator.next();
console.log(rest); // {value: 1, done: false}

// next 메서드에 인수로 전달한 10은 genFunc 함수의 x 변수에 할당됨
// 이터레이터 리절트 객체의 value 프로퍼티에 두 번째 yield된 값 20이 할당됨
let rest = generator.next(10);
console.log(rest); // {value: 20, done: false}


// next 메서드에 인수로 전달된 20은 genFunc 함수의 y 변수에 할당됨
// 이터레이터 리절트 객체의 value 프로퍼티에 genFunc 함수의 반환값 30이 할당됨
let rest = generator.next();
console.log(rest); // {value: 30, done: true}

1.5 제네레이터의 활용

1.5.1 이터러블 구현

  • 제네레이터 함수를 사용하면 이터러블 프로토콜을 준수해 이터러블을 생성하는 방식보다 간단히 이터러블을 구현할 수 있음
    • 예 : 무한 피보나치 수열을 생성하는 함수
// 방법 1 : 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식
const infiniteFibonacci = (function () {
  let [pre, cur] = [0, 1];
  
  return {
    [Symbol.iterator]() { return this; },
    next() {
      [pre, cur] = [cur, pre + cur];
      return { value: cur };
    }
  };
}());

for (const num of infiniteFibonacci) {
  if (num > 10000) break;
  console.log(num); // 1 2 3 8 13 ... 2584 4181 6765
}


// 방법 2 : 제네레이터를 이용한 방식
const infiniteFibonacci = (function* () {
  let [pre, cur] = [0, 1];
  
  while (true) {
    [pre, cur] = [cur, pre + cur];
    yield cur;
  }
}());

for (const num of infiniteFibonacci) {
  if (num > 10000) break;
  console.log(num); // 1 2 3 8 13 ... 2584 4181 6765
}

1.5.1 비동기 처리

  • 제네레이터 함수는 next 메서드와 yield 표현식을 통해 함수 호출자와 함수 상태를 주고 받을 수 있음
    • 이러한 특성을 활용하여 프로미스를 사용한 비동기 처리를 동기 처리처럼 구현할 수 있음
// node-fetch : Node.js 환경에서 window.fetch 함수를 사용하기 위한 패키지
// 브라우저 환경에서 이 예제를 실행한다면 아래 코드는 필요 없음
const fetch = require('node-fetch');

// 제네레이터 실행기
const async = generatorFunc => {
  const generator = generatorFunc();
  
  const onResolved = arg => {
    const result = generator.next(arg);
    
    return result.done 
      ? result.value : result.value.then(res => onResolved(res));
  };
  
  return onResolved;
};

(async(function* fetchTodo() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  
  const response = yield fetch(url);
  const todo = yield response.json();
  console.log(todo); // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
})());
  • 위 예제에 대한 설명

    1. async 함수가 호출되면, 인수로 전달받은 제네레이터 함수 fetchTodo를 호출함
      • 제네레이터 객체를 생성하여 generator 변수에 할당하고, onResolved 함수를 반환함
      • async 함수가 반환한 onResolved 함수를 즉시 호출하여, 제네레이터 객체의 next 메서드를 처음 호출함
    2. next 메서드가 처음 호출되면, 제네레이터 함수 fetchTodo의 첫 번째 yield 문까지 실행됨
      • 첫 번째 yield된 fetch 함수가 반환한 프로미스를 value 프로퍼티 값으로, false를 done 프로퍼티 값으로 가진 이터레이터 리절트 객체를 반환하고 result 변수에 할당함
      • 반환한 이터레이터 리절트 객체의 done 프로퍼티 값이 false이므로, 이터레이터 리절트 객체의 value 프로퍼티 값을 resolve한 Resonse 객체를 onResolved 함수에 인수로 전달하면서 재귀 호출함
    3. onResolved 함수에 인수로 전달한 Response 객체를 next 메서드에 인수로 전달하면서 next 메서드를 두 번째로 호출함
      • next 메서드에 인수로 전달한 Resonse 객체는 제네레이터 함수 fetchTodo의 response에 할당됨
      • 제네레이터 함수 fetchTodo의 두 번째 yield 문까지 실행됨
      • 두 번째 yield된 response.json 메서드가 반환한 프로미스를 value 프로퍼티 값으로, false를 done 프로퍼티 값으로 가진 이터레이터 리절트 객체를 반환함
      • 반환한 이터레이터 리절트 객체의 done 프로퍼티 값이 false이므로, 이터레이터 리절트 객체의 value 프로퍼티 값을 resolve한 todo 객체를 onResolved 함수에 인수로 전달하면서 재귀 호출함
    4. onResolved 함수에 인수로 전달한 todo 객체를 next 메서드에 인수로 전달하면서 next 메서드를 세 번째로 호출함
      • next 메서드에 인수로 전달한 todo 객체는 제네레이터 함수 fetchTodo의 todo 변수에 할당됨
      • 제네레이터 함수 fetchTodo에 남은 yield 문이 없으므로 함수가 끝까지 실행되고 종료됨
      • 제네레이터 함수의 반환값이 undefined를 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 가진 이터레이터 리절트 객체를 반환함
  • 제네레이터 실행기가 필요하다면 직접 구현하는 것보다 co 라이브러리를 사용하는 것을 권장함

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

co(function* fetchTodo() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  
  const response = yield fetch(url);
  const todo = yield response.json();
  console.log(todo); // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
});



2. async/await

  • ES8(ECMAScript 2017)에서 도입됨
  • 프로미스를 기반으로 동작함
  • 프로미스의 후속 처리 메서드 없이 마치 동기 처리처럼 프로미스가 처리 결과를 반환하도록 구현할 수 있음
const fetch = require('node-fetch');

async function fetchTodo() {
  const url = 'https://jsonplaceholder.typicode.com/todos/1';
  
  const response = await fetch(url);
  const todo = await response.json();
  console.log(todo); // {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
};

2.1 async 함수

  • async 함수는 async 키워드를 사용하여 정의함
    • await 키워드는 반드시 async 함수 내부에서 사용해야 함
  • async 함수는 언제나 프로미스를 반환함
    • 명시적으로 프로미스를 반환하지 않더라도, 암묵적으로 반환값을 resolve하는 프로미스를 반환함
    • class의 constructor 메서드는 async 메서드가 될 수 없음
      • 클래스의 constructor 메서드는 인스턴스를 반환하기 때문임
// async 함수 선언문
async function foo(n) { return n; }
foo(1).then(v => console.log(v)); // 1

// async 함수 표현식
const foo = async function (n) { return n; };
foo(1).then(v => console.log(v)); // 1

// async 화살표 함수
const foo = async n => n;
foo(1).then(v => console.log(v)); // 1

// async 메서드
const obj = {
  async foo(n) { return n; }
};
obj.foo(4).then(v => console.log(v)); // 4

// async 클래스 메서드
class MyClass {
  async bar(n) { return n; }
}
const myClass = new MyClass();
myClass.bar(5).then(v => console.log(v)); // 5

2.2 await 키워드

  • await 키워드는 프로미스가 settled 상태(비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환함
  • await 키워드는 반드시 프로미스 앞에서 사용해야 함
const fetch = require('node-fetch');

const getGithubUserName = async (id) => {
  const res = await fetch(`https://api.github.com/users/${id}`);
  const { name } = await res.json();
  console.log(todo); // Ungmo Lee
};

getGithubUserName('ungmo2');
  • 위 예제 설명

    1. fetch 함수가 수행한 HTTP 요청에 대한 서버의 응답이 도착해서 fetch 함수가 반환한 프로미스가 settled 상태가 될 때까지 대기함
    2. 프로미스가 settled 상태가 되면 프로미스가 resolve한 처리 결과가 res 변수에 할당됨
  • 여러 개의 비동기 처리할 때 await 권장 방법

    • 서로 연관이 없이 개별적으로 수행되는 비동기 처리를 여러 개 수행할 경우 순차적으로 처리할 필요가 없으므로, 각 프로미스에 await를 쓸 필요가 없음
    • 앞선 비동기 처리의 결과를 가지고 다음 비동기 처리를 수행해야하는 경우 비동기 처리의 순서가 보장되어야 하므로, 모든 프로미스에 await를 써서 순차적으로 처리해야 함
async function foo() {
  const res = await Promise.all([
    new Promise(resolve => setTimeout(() => resolve(1), 3000)),
    new Promise(resolve => setTimeout(() => resolve(2), 2000)),
    new Promise(resolve => setTimeout(() => resolve(3), 1000))
  ]);
  
  console.log(res); // [1, 2, 3]
}

foo(); // 약 3초 소요됨


async function bar(n) {
  const a = await new Promise(resolve => setTimeout(() => resolve(n), 3000));
  // 두 번째 비동기 처리를 수행하려면, 첫 번째 비동기 처리 결과가 필요함
  const b = await new Promise(resolve => setTimeout(() => resolve(a + 1), 2000));
  // 세 번째 비동기 처리를 수행하려면, 두 번째 비동기 처리 결과가 필요함
  const c = await new Promise(resolve => setTimeout(() => resolve(b + 1), 1000))
  
  console.log(res); // [1, 2, 3]
}

bar(1); // 약 6초 소요됨

2.3 에러 처리

  • async/await에서 에러 처리는 try...catch 문을 사용할 수 있음
    • 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확함
const fetch = require('node-fetch');

async function fetchTodo() {
  try {
    const wrongUrl = 'https://wrong.url';
    
    const response = await fetch(wrongUrl);
    const data = await response.json();
    console.log(data);
  } catch (err) {
    console.error(err); // TypeError: Failed to fetch
  }
};
  • async 함수 내에서 catch 문을 사용해서 에러 처리 하지 않으면, async 함수는 발생한 에러를 reject하는 프로미스를 반환함
    • 이때, async 함수를 호출하고 Promise.prototype.catch 메서드를 사용해 에러를 캐치할 수 있음
const fetch = require('node-fetch');

const foo = async () => {
  const wrongUrl = 'https://wrong.url';
  
  const response = await fetch(wrongUrl);
  const data = await response.json();
  return data;
};

foo()
  .then(console.log)
  .catch(console.error); // TypeError: Failed to fetch

0개의 댓글