본문 바로가기

Javascript

[모던 자바스크립트 Deep Dive] 42. 제너레이터와 async/await

제너레이터란?

  • 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재개할 수 있는 특수한 함수
    1. 제너레이터 함수는 함수 호출자에게 함수 실행의 제어권을 양도할 수 있다
    2. 제너레이터 함수는 함수 호출자와 함수의 상태를 주고받을 수 있다
    3. 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다

 

제너레이터 함수의 정의

  • function* 키워드로 선언
  • 하나 이상의 yield 표현식 포함
  • 에스터리스크의 (*) 위치는 function 키워드와 함수 이름 사이라면 어디든지 상관 X 하지만 일관성을 위해 function 키워드 바로 뒤를 권장
  • 제너레이터 함수는 화살표 함수로 정의 X
  • new 연산자와 함께 생성자 함수로 호출 X
// 제너레이터 함수 선언문
function* getDecFunc(){
 yield 1;
};

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

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

// 제너레이터 클래스 메서드
class MyClass{
 *genClsMethod(){
  yield 1;
 }
}

 

제너레이터 객체

  • 제너레이터 함수를 호출하면 일반 함수처럼 함수 코드 블록을 실행하는 것이 아니라 제너레이터 객체를 생성해 반환
  • 제너레이터 함수가 반환한 제너레이터 객체는 이터러블이면서 동시에 이터레이터
  • Symbol.iterator 메서드를 상속받는 이터러블이면서 value , done 프로퍼티를 갖는 이터레이터 리절트 객체를 반화하는 next 메서드를 소유하는 이러테이터 (Symbol.iterator 메서드 호출하여 이터레이터 생성 필요 X) 
// 제너레이터 함수
function* getFunc(){
 yield 1;
 yield 2;
 yield 3;
}

// 제너레이터 함수를 호출하면 제너레이터 객체를 반환
const generator = getFunc();

// 제너레이터 객체는 이터러블이면서 동시에 이터레이터
// 이터러블은 Symbol.iterator 메서드를 직접 구현하거나 프로토타입 체인을 통해 상속받은 객체
console.log(Symbol.iterator in generator); // true
// 이터레이터는 next 메서드를 갖는다
console.log('next' in generator); // true
  • 제너레이터 객체는 next 메서드를 갖는 이터레이터이지만 이터레이터에는 없는 return, throw 메서드를 가짐
    • next 메서드 호출 : 제너레이터 함수의 yield 표현식까지 코드 블록을 실행하고 yield 된 값을 value 프로퍼티 값으로 false를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환
    • return 메서드 호출 : 인수로 전달받은 값을 value 프로퍼티 값으로, true를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체를 반환
    • throw 메서드 호출 : 인수로 전달받은 에러를 발생시키고 undefined 를 value 프로퍼티 값으로, true 를 done 프로퍼티 값으로 갖는 이터레이터 리절트 객체 반환
function* getFunc(){
  try{
  	yield 1;
  	yield 2;
  	yield 3;
  } catch(e){
 	console.error(e);
  
  }
}

const generator = getFunc();

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 }

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

  • yield 키워드와 next 메서드를 통해 실행을 일시 중지했다가 필요한 시점에 재개 가능
  • 제터레이터 함수를 호출하면 함수의 코드 블록이 실행되는 것이 아니라 제너레이터 객체를 반환하므로 제너레이터 객체의 next 메서드를 호출하면 제너레이터 함수의 코드 블록 실행
  • yield 키워드는 제너레이터 함수의 실행을 일시 중지시키거나 yield 키워드 뒤에 오는 표현식의 평가 결과를 제너레이터 함수 호출자에게 반환
// 제너레이터 함수
function* getFunc(){
 yield 1;
 yield 2;
 yield 3;
}

// 제너레이터 함수를 호출하면 제너레이터 객체 반환
// 이터러블이면서 동시에 이터레이터인 제너레이터 객체는 next 메서드를 가짐
const generator = getFunc();

// 처음 next 메서드를 호출하면 첫 번째 yield 표현식까지 실행되고 일시 중지
// next 메서드는 이터레이터 리절트 객체 ({ value, done }) 를 반환
// value 프로퍼티에는 첫 번째 yield 표현식에서 yield된 값 1이 할당
// done 프로퍼티에는 제너레이터 함수가 끝까지 실행되었는지를 나타내는 false가 할당
console.log(generator.next()); // {value : 1, done : false}

// 다시 next 메서드를 호출하면 두 번째 yield 표현식까지 실행되고 일시 중지
// next 메서드는 이터레이터 리절트 객체 ({ value, done }) 를 반환
// value 프로퍼티에는 첫 번째 yield 표현식에서 yield된 값 2 할당
// done 프로퍼티에는 제너레이터 함수가 끝까지 실행되었는지를 나타내는 false가 할당
console.log(generator.next()); // {value : 2, done : false}

// 다시 next 메서드를 호출하면 세 번째 yield 표현식까지 실행되고 일시 중지
// next 메서드는 이터레이터 리절트 객체 ({ value, done }) 를 반환
// value 프로퍼티에는 첫 번째 yield 표현식에서 yield된 값 3 할당
// done 프로퍼티에는 제너레이터 함수가 끝까지 실행되었는지를 나타내는 false가 할당
console.log(generator.next()); // {value : 3, done : false}

// 다시 next 메서드를 호출하면 남아있는 yield 표현식이 없으므로 제너레이터 함수의 마지막까지 실행됨
// next 메서드는 이터레이터 리절트 객체 ({ value, done }) 를 반환
// value 프로퍼티에는 함수의 반환값 undefined 가 할당
// done 프로퍼티에는 제너레이터 함수가 끝까지 실행되었음을 나타내는 true 가 할당
console.log(generator.next()); // {value : undefined, done : true}
  • 제너레이터 객체의 next 메서드를 호출하면 yield 표현식까지 실행되고 일시 중지 (이 때 함수의 제어권이 호출자로 양도됨)
  • 이때 제너레이터 객체의 next 메서드는 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환
  • next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티에는 yield 표현식에서 yield 된 값 (yield  키워드 뒤의 값)이 할당되고 done 프로퍼티에는 제너레이터 함수가 끝까지 실행되었는지를 나타내는 불리언 값이 할당

 

제너레이터의 활용

1. 이터러블의 구현

// 무한 이터러블을 생성하는 제너레이터 함수
const infiniteFibo = (funciton* (){
 let [pre, cur] = [0, 1];
 
 while(true){
  [pre, cur] = [cur, pre + cur];
  yield fur;
 }
})());

for(const num of infiniteFibo){
 if(num > 1000) break;
 console.log(num); // 1 2 3 5 8 13 ... 6765 
}

 

2. 비동기 처리

// 제너레이터 함수를 인자로 받아 비동기적으로 실행하는 함수
const async = generatorFunc => {
  // 제너레이터 함수를 실행하여 제너레이터 객체를 생성
  const generator = generatorFunc();

  // 재귀적으로 호출되는 함수로, 제너레이터의 각 단계를 진행
  const onResolved = arg => {
    // 제너레이터의 next 메서드를 호출하여 다음 단계로 진행
    // 여기서 arg는 이전 yield의 결과값으로 사용됩니다.
    const result = generator.next(arg);

    // 만약 제너레이터 함수가 완료되었다면(result.done === true), 
    // 최종 결과값을 반환
    if (result.done) {
      return result.value;
    } else {
      // 제너레이터 함수가 아직 완료되지 않았다면(result.done === false),
      // yield된 프로미스를 기다린 후, 완료되면 onResolved를 다시 호출
      // 이는 비동기 작업이 완료될 때까지 반복됩니다.
      return result.value.then(res => onResolved(res));
    }
  };

  // onResolved 함수를 호출하여 제너레이터 함수의 실행 시작
  return onResolved;
};

// 위에서 정의한 async 함수를 사용하여 제너레이터 함수를 비동기적으로 실행
(async(
  function* fetchTodo(){
    // 데이터를 받을 URL을 정의
    const url = '데이터를 받을 url';

    // 첫 번째 yield에서 fetch 요청을 보내고, 그 결과를 기다림
    const response = yield fetch(url);
    // 두 번째 yield에서 fetch 요청의 결과를 JSON으로 변환하고, 그 결과를 기다림
    const data = yield response.json();
    // JSON으로 변환된 데이터를 콘솔에 출력
    console.log(data);
  }
))();

async/ await

  • 프로미스의 후속 처리 메서드 없이 마치 동기처럼 프로미스가 처리 결과를 반환하도록 구현 가능

1. async 함수

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

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

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

// 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. await 함수

  • 프로미스가 settled 상태 (비동기 처리가 수행된 상태)가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve 한 처리 결과 반환
  • 비동기 처리 순서가 보장되어야할 때 사용 (비동기 처리가 서로 연관 없이 개별적으로 수행되는 처리이면 await 불필요)
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([a, b, c]);	// [1, 2, 3]
}

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

 

3. 에러 처리

  • async/await 에서 에러 처리는 try ... catch 문 사용 가능
  • 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문
  • async 함수 내에서 catch 문을 사용해서 에러 처리를 하지 않으면 async 함수는 발생한 에러를 reject 하는 프로미스 반환 (Promise.prototype.catch 후속 처리 메서드 사용 가능)
const foo = async () => {
  try {
    const response = await fetch('잘못된 url');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error(err);	// TypeError: Failed to fetch
  }
};

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