Async 함수 (ES2017)-https://github.com/nhnent/fe.javascript/wiki/%23165:-%EB%AA%A8%EB%8D%98-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D:-Generator-Yield-vs.-Async-Await

자, 제너레이터 함수와 yield가 async/await과 어떤 관련이 있을까? Async/await은 ES2017에서 정식으로 채택된 Javascript에 새롭게 제안된 것이다. 제너레이터보다 더 특별한 방식으로 함수의 실행을 잠시 멈추는 함수를 작성할 수 있다. Async/await을 사용하면 제너레이터의 일부 사용 사례를 더 쉽게 구현할 수 있다는 것을 기억해라. 제너레이터 함수/yield와 Async 함수/await은 모두 "기다리는" 비동기 코드를 작성하는 데 사용된다. 그래서 비동기 함수이지만 동기 함수처럼 보이게 한다; 콜백을 사용하지도 않는다.

제너레이터에서부터 시작해보자: yield는 제너레이터의 실행을 어떤 시점에서 멈출 수 있기 때문에, 비동기 요청이 끝날 때까지 기다린 후 다음 코드가 실행되게 할 수 있다. 다음 예제에서 생각해보자:

우리의 애플리케이션이 백엔드로부터 필요한 정보를 받는 "init" 함수를 갖고 있다고 하자. 예를 들어 사용자 목록을 받아올 때는, XHR 요청을 필요로 하기 때문에 비동기 메서드일 것이다. 프로미스를 사용한다면, 우리는 반드시 프로미스가 끝났을 때 실행될 콜백함수를 정의해야 한다.

img04-async-function-using-promise

제너레이터 함수에서는 비동기 함수인 "getUsersFromDataBase"의 종료를 기다리기 위해 yield를 사용하면 된다. 그리고 users에 반환 값을 넣어주면 된다.

img04-async-function-using-yield

이 방식이 확실히 읽기 쉽지만, 엄청 단순하게 동작하는 것은 아니다. 용어가 암시하듯이, "yield" 키워드는 정말 실행 권한을 제너레이터 함수의 호출자에게 맡긴다. 이것은 프로미스를 양도받는 역할을 하는 외부 함수가 있어야 한다는 것을 의미한다. 프로미스가 끝날 때까지 기다렸다가 제너레이터 함수에 반환 값을 넘겨주어, 함수의 실행이 재개되고 이 값이 users라는 변수에 할당되도록 한다.

위 예제는 우리가 이전에 알지 못했던 제너레이터 함수의 특징을 알게 해준다: 제너레이터 함수는 "멈춤 지점"이라면 외부 함수로 값을 반환할 뿐만 아니라, 외부함수로부터 값을 받을 수 있다. 이는 두 코드 간의 back-and-forth 커뮤니케이션을 허용한다.

이터레이터의 "next" 메서드의 변수로 전달된 값은 제너레이터 함수로 전달된다. 다음 예제는 어떻게 제너레이터 함수를 호출하는 "외부" 코드가 프로미스를 끝내고, 반환된 값을 제너레이터 함수로 보내는 역할을 하는지에 대한 설명이다:

img05-how-exteranl-code-get-generators-return-value

getUsersFromDatabase의 구현은 중요하지 않다. 이 함수는 2초 후에 "Test Users"라는 문자열을 반환하면서 종료되는 프로미스를 반환한다. 어떻게 외부 코드(6-30번째 줄)가 제너레이터의 마지막 값을 얻기 위해 제너레이터를 호출하고(6-7번째 줄), 프로미스가 반환한 값을 처리해서(16번째 줄에서 시작하는 콜백), 제너레이터에 전달하는지(20번째 줄)에 주목하자. 13, 18, 28번째 줄은 각 시점의 변수의 값을 보여주기 위한 곳으로, 그 값은 어두운 파란색 글자로 표시되며, 인라인 실행 도구에 의해 추가됐다.

이 예제는 제너레이터 함수가 하나의 값만 양도(yield)하는 특정한 경우에 대한 것으로 단순한 시나리오이다. 이론적으로, 외부 함수는 제너레이터의 마지막 return 구문에 다다르기 까지 지나치는 모든 프로미스를 반환해야 한다. 서드 파티 라이브러리에서도 같은 관계이다. 외부 함수가 프로미스를 어떻게 처리하고 해결하는지 상관하지 않고, 비동기로 기다리는 코드를 generators/yield 방식으로 작성할 수 있게 한다. 이 라이브러리들은 제너레이터 함수를 argument로 받아서, 제너레이터를 실행하고, yield 된 프로미스를 다루는 기능 제공한다.

이 긴 예제는 Async/await이 엄청 유용한 기능인지 보여주기 위한 코드이다: 비동기 코드를 generator 함수 예제(1-4번째 줄)와 비슷한 데, 심지어는 프로미스를 다루기 위한 외부의 헬퍼 함수가 필요하지도 않는다! async/await을 쓰면, 이렇게만 쓰면 된다:

img05-async-function-using-async-await

제너레이터의 별표(*)는 함수 선언부 앞에 오는 async 키워드로, "yield" 키워드는 "await"으로 대체되었다. Await은 프로미스를 반환하는 구문이기만 하면 그 앞에 놓일 수 있다. 그리고 await 키워드는 그 자신보다 먼저 선언된, async 키워드가 있는 함수 안에서만 사용할 수 있다. 이제 테스트 함수가 실행될 때, 다른 함수의 도움없이 await 키워드에서 멈추고, 프로미스가 끝나길 기다렸다 자동으로 프로미스에서 반환된 값을 users라는 const 변수에 할당할 것이다.

Async/await은 프로미스를 .then() 메서드나 콜백 정의, 그리고 이것들의 중첩현상(악명높은 죽음의 피라미드) 없이 다루는 코드를 작성하게 한다. 좋은 해결방법인 것처럼 보이나 이것을 사용하기 전에 반드시 생각해야 할 몇 가지 중요한 점이 있다. 때때로, 이전의 .then()의 프로미스 방식을 고수하는 것이 더 좋은 방법일 수 있다. 다음에 오는 내용을 꼭 생각해봐야 한다:

1) 비동기 함수는 항상 프로미스를 반환한다: 비동기 함수는 모든 "await" 키워드에서 실행을 잠깐 멈추고 비동기 구문이 종료되길 기다린다. 그래서 await이 붙는 함수 자체가 비동기적이다(이 때문에 비동기 함수 앞에 async 키워드가 붙는 것이다). 이는 async 키워드를 가진 함수는 무엇을 반환하든 간에 항상 리졸브되거나 에러를 던지는 프로미스를 반환한다는 것이다. 이전 예제에서 "test" 함수는 문자열 "Test Users Correctly received"라는 문자열을 반환했다. 그러나 실제로는 이 문자열과 함께 해결되는 프로미스가 반환됐다. 그래서 코드를 설계할 때나 코드의 다른 부분이 주어진 함수와 어떻게 상호작용하도록 고민할 때, 프로미스를 받기 원하는지 아닌 지를 꼭 생각해야 한다.

2) Await은 항상 프로미스를 병렬적이 아닌, 순차적으로 기다린다 await 키워드는 한 번에 여러 개 아닌, 하나의 프로미스만 기다릴 수 있다. 그래서 만약 여러 개의 프로미스를 다루면서, 이 각각이 await 키워드를 통해 기다리길 바란다면, 하나의 동작이 완전히 끝나야 다음 동작으로 넘어갈 수 있다.

동시에 수백 개의 요청을 보내 네트워크에 부하를 주는 것을 방지하고 싶을 때처럼 이 방식이 최선일 때가 있지만, 이 방식은 동시에 여러 개의 프로미스를 처리하는 것보다 훨씬 느리다.

img06-sequencial-async-functions

사용자 목록에 대한 배열인 "users"가 이전에 선언되어 있다고 가정하자. 그리고 "getProfileImage"가 프로필 이미지를 반환하는 프로미스를 반환한다고 가정하자. 이 예제는 각각의 사용자들을 순회하는데, "profileImages" 배열에 프로필 이미지를 넣기 위해 매 차례에서 잠깐 멈춘다. 이것은 현재 이터레이션의 프로미스가 끝났을 때만 다음 이터레이션으로 이동한다.

동시에 여러 프로미스를 처리하는 것에 대한 대안은 await과 프로미스를 같이 쓰는 것이다. 예를 들어, "Promise.all"로 프로미스 그룹을 관리할 수 있는 데, 이는 그룹의 모든 프로미스가 끝날(또는 실패 시 에러를 반환) 때 끝나는 하나의 프로미스를 반환한다. 그러면, 이 하나의 통합된 프로미스에 await을 사용하면 된다.

img07-parallel-async-functions

이 예제는 "map"을 사용해서 users 배열을 순회한다: 각각의 차례에서 "getProfileImage"가 실행되고, 실행되지 않는 프로미스를 반환한다. Map은 각 차례에서 일시정시하지 않고, 모든 프로미스를 가지는 배열을 반환한다는 것에 주목하자. 그러고 나서 우리는 이 모든 프로미스들을 "Promise.all"로 묶고, 이 한 개의 프로미스가 해결되는 것을 기다리기 위해 딱 이 시점에서만 await을 사용한다.

기억하자 - 제너레이터와 비동기 함수는 항상 특별한 타입의 객체를 반환한다.

  • Generator 함수: 값 X를 yield/return하면, 이것은 항상 {value: X, done: Boolean} 형태의 이터레이션 객체를 반환한다.
  • 비동기 함수: 값 X를 반환하면, X를 반환하면서 끝나거나 에러를 던지는 프로미스를 반환한다.

결론

제너레이터는 실행을 잠깐 멈출 수 있는 함수이다. 이터레이터 객체가 다음 값을 요청할 때마다 위임된(yield) 값을 생성한다. 이런 의미에서, 제너레이터는 수동적인 생산자인 반면 이터레이터는 적극적인 소비자이다(왜냐하면 값을 요청하는 것에 대한 주도권을 갖고 있기 때문이다). 이것은 일반적인 옵저버 패턴과 대조적이다. 옵저버 패턴에서는 적극적인 생산자(옵저버블, 주체)가 필요할 때 그 값을 반환하고, 하나 이상의 수동적인 소비자(옵저버)가 있어서 값이 반환되기를 기다리고 있다. 제너레이터 함수는 리스트에서 한 번에 하나의 값만 반환하는 데 사용할 수 있다. 아마도 필요에 따라 무한한 값을 생성하는 데 사용할 수 있다.

제너레이터 함수의 특별한 사용법 중의 하나는 프로미스를 양도하고(yielding) 동기식으로 동작하는 것처럼 보이는("기다리는") 비동기 코드를 작성하는 것이다. 그런데 이 방식은 반환되는 프로미스들을 다룰 다른 함수의 도움이 필요하다. 이런 경우에는 헬퍼 함수가 필요없는 async/await을 사용하는 것이 더 나은 방법이다.

비동기 함수와 await 키워드는 비동기 코드를 "기다리는" 방식으로 작성하기 위한 훌륭한 방법이다. 그러나 한 번에 여러 개의 프로미스를 기다릴 수 없기 때문에, 이런 한계 상황에서는 이전의 프로미스 폴백 방식을 사용하는 것이 낫다.







async-await를 사용한  비동기처리와 generator-yield를 사용한 비동기처리의 차이

Link to section

generator-yield의 경우 generator의 리턴값이 iterator으로, 각 yield에서 작업이 중단되면 .next()를 통해 다음의 작업으로 넘어가야 하는 불편함이 있었다. co 모듈이나 aa모듈은 그러한 불편함을 줄이기 위해 사용된 모듈이다.

그러나 async-await의 경우 await 이후에  promise가 오게 되면 promise의 resolve가 반환될때 까지 기다려주기 때문에 별도의 모듈을 사용할 필요가 없다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const co = require('co');
 
const func1 = function() {
    return  new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log('a');
            resolve();
            //reject('func1 fail');
        }, 2000);
    });
  };
 
const func2 = function() {
    return  new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log('b');
            resolve();
            //reject('func2 fail');
        }, 3000);
    });
};
 
const func3 = function() {
    console.log('c');
};
 
//func1().then(func2).then(func3)
    
co(function*(){
    yield func1();
    yield func2();
    yield func3();
    })
    
 
async function add1(){
    await func1();
    await func2();
    await func3();    
}
cs


'C Lang > JS Technic' 카테고리의 다른 글

Stream API입문  (0) 2018.09.05
파라미터의 유효성체크(validation check of parameter)  (0) 2018.08.31
in 연산자  (0) 2018.08.23
for in vs for of 반복문  (0) 2018.08.23
array.shift()  (0) 2018.08.15

+ Recent posts