0%

제너레이터와 async-await

1.제네레이터

es6에서 도입된 제너레이터 함수는 이터러블을 생성하는 함수이다. 제너레이터 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구현 할 수 있다. 또한 제너레이터 함수는 비동기 처리에 유용하게 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//이터레이션 ㅍ,로토콜을 구현하여 무한 이터러블을 생성하는 함수
const createInfinittbyIteration = function (){
let i =0;//자유 변수
return {
[Symbol.iterator](){return this;},
next(){
return {value:++i};
}
};
};
for(const n of createInfinitbyIteration()){
if(n>5)break;
console.log(n)//1 2 3 4 5
};

//무한 이터러블을 생성하는 제너레이터 함수
function* createInfinittbyIteration(){
let i =0;
while(true){yield ++i;}
}
for(const n of createInfinittbyIteration()){
if(n>5) break;
console.log(n) // 1 2 3 4 5
}

제너레이터 함수는 일반 함수와는 다른 독특한 동작을 한다. 제너레이터 함수는 일반 함수와 같이 함수의 코드 블로그을 한번에 실행하지 않고 함수 코드 블록의 실행을 일시 중단했다가 필요한 시점에 재시작 할 수 있는 특수한 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
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 문으로 반환값을 리턴하지만 제너레이터 함수를 호출하면 제너레이터를 반환한다. 이 제너레이터는 이터러블이면서 동시에 이터레이터인 객체이다. 다시 말해 제너레이터 함수가 생성한 제너레이터는 Symbol.iterator메소드를 소유한 이터러블이다. 그리고 제너레이터는 next()메소드를 소유하며 next메소드를 호출하면 value,done 프로퍼티를 갖는 이터레이터 객체를 반환하는 이터레이터이다.

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
//제너레이터 함수 정의
function* counter(){
for(const v of [1,2,3])yield v;
//=>yield* [1,2,3];
}
//제너레이터 함수를 호출하면 제너레이터를 반환한다.
let generatorObj = counter();

//제너레이터는 이터러블이다.

console.log(Symbol.iterator in generatorObj) //true;

for(const i int generatorObj){
console.log(i)// 1 2 3
}

generatorObj = counter();

//제너레이터는 이터레이터이다.
console.log('next' in generatoroBJ) //true;

console.log(generatorObj.next()) //{value:1,done:false};
console.log(generatorObj.next()) //{value:2,done:false};
console.log(generatorObj.next()) //{value:3,done:false};
console.log(generatorObj.next()) //{value:undefined,done:true};

2.제너레이터 함수의 정의

제너레이터 함수는 function* 키워드로 선언을 한다. 그리고 하나 이상의 yield문을 포함한다.

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
//제너레이터 함수 선언문
function* genDecFunc(){
yield 1;
}
let generatorObj = genDecFunc();

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

//제너레이터 메소드
const obj ={
*generatorObjMethod(){
yield 1;
}
}
generatorObj = obj.generatorObjMethod();

//제너레이터 클래스 메소드
class MyClass{
*generatorClsMethod(){
yield 1;
}
}
const myClass = new MyClass();
generatorObj = myClass.generatorClsMethod();

3.제너리이터 함수의 호출과 제너레이터 객체

제너레이터 함수를 호출하면 제너레이터 함수의 코드 블록이 실행되는 것이 아니라 제너레이터 객체를 반환한다. 앞에서 살펴본 바와 같이 제너레이터 객체는 이터러블이며 동시에 이터레이터이다. 따라서 next메소드를 호출하기 위해 Symbol.iterator 메소드로 이터레이터를 별도로 생성할 필요가 없다.

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
//제너레이터 함수 정의 
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')


}

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

//첫번째 next 메소드 호출:첫번째 yield문까지 실행되고 일시 중단된다.
// 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}

제너레이터 함수가 생성한 제너레이터 객체의 next메소드를 호출하면 처음만나는 yield문까지 실행되고 실시 중단된다. 또 다시 next메소드를 호출하면 중단된 위치에서 다시 실행이 시작하여 다음 만나는 yieldㅁ누 까지 실행되고 또 다시 일시 중단 된다.

1
start -> generatorObj.next() -> yield 1 -> generatorObj.next() -> yield 2 -> ... -> end

next 메소드는 이터레이터 결과 객체와 가티이 value,done이라는 프로퍼티를 갖는 객체를 반환한다.vlaue프로퍼티는 yield문이 실행되었는지를 나타내는 boolean타입의 값이다. 마지막 yield문까지 실행된 상태에서 next메소드를 호출하면 done프로퍼티 값을 true가 된다.

4.제너레이터의 활용

4.1. 이터러블의 구현

제너레이터는 함수를 사용하면 이터레이션 프로토콜을 준수해 이터러블을 생성하는 방식보다 간편하게 이터러블을 구현할 수 있다. 이터레이션 프로토콜을 준수하여 무한 피보나치 수열을 생성하는 함수를 구현해 보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//무한 이터러블을 생성하는 함수
const infinityFibonacci =(function (){
let [pre,cur] =[0,1];

return {
[sYMBOL.iterator](){return this;},
next(){
[pre,cur] = [cur,pre+cur];
//done프로퍼티를 생략한다.
return {value:cur};
}
}
}());

//infinityFibonacci는 무한 이터러블이다.
for(const num of infinityFibonacci){
if(num > 10000) break;
console.log(num) //1 2 3 5 8..
}

이터레이션 프로토콜을 보다 간단하게 처리하기 위해 제너레이터를 활용할 수 있다. 제너레이터는 활용하여 무한 피보나치수열을 구현한 이터러블을 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//무한 이터러블틀 생성하는 제너레이터 함수
const infinityFibonacci =(function* (){
let [pre,cur] = [0,1];

while(true){
[pre,cur] = [cur,pre+cur];
yield cur;
}
}());
//infinityFibonacci는 무한 이터러블이다.
for(const num of infinityFibonacci){
if(num >10000) break;
console.log(num);
}

제너레이터 함수에 최대값을 인수를 전달해보자

1
2
3
4
5
6
7
8
9
10
11
12
//무한 이터러블을 생성하는 제너레이터 함수
const createInfiniteFibByGen =function* (max){
let [prec,curr] = [0,1];
while(true){
[prev,curr] =[curr,prev+curr];
if(curr >= max)return//제너레이터 함수 종료
yield curr;
}
};
for(const num of createInfiniteFibByGen(10000)){
console.log(num);
}

이터레이터의 next메소드와 다르게 제너레이터 객체의next메소드에는 인수를 전달할 수도 있다. 이를 통해서 제너레이터 객체에 데이터를 전달 할 수 있다.

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
function* gen(n){

let res;
res = yield n //n:0<= gen함수에 전달한 인수

console.log(res) //res:1 <= 두번째 next 호출 시 전달한 데이터
res = yield res;

console.log(res) //res:2 <= 세번째 next 호출 시 전달한 데이터
res = yield res;

console.log(res); // res: 3 ⟸ 네번째 next 호출 시 전달한 데이터
return res;
}

const generatorObj = gen(0);

console.log(generatorObj.next()); // 제너레이터 함수 시작
console.log(generatorObj.next(1)); // 제너레이터 객체에 1 전달
console.log(generatorObj.next(2)); // 제너레이터 객체에 2 전달
console.log(generatorObj.next(3)); // 제너레이터 객체에 3 전달

/*
{ value: 0, done: false }
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: true }
*/

이터레이터의 next메소드는 이터러블의 데이터를 꺼내 온다. 이에 반해 제너레이터의 next메소드에 인수를 전달하면 제너레이터 객체에 데이터를 밀어 넣는 다. 제너레이터의 이런 특성은 동시성 프로그래밍을 가능케 한다.

4.비동기 처리

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

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
const fetch = require('node-fetch');

function getUsers(genObj,username){
fetch(`https://api.github.com/users/${username}`)
.then(res =>res.json())
//1.제너레이터 객체에 비동기 처리 결과를 전달한다.
.then(user=> genObj.next(user.name));
}
//제너레이터 객체 생성
const g =(function* (){
let user;
//2.비동기 처리 함수가 결과를 반환한다.
//비동기 처리의 숭성가 보장된다.
user = yield getUser(g,'kim');
console.log(user); //

user =yield getUser(g,'lee');
console.log(user);

user = yield getUser(g,'park');
console.log(user);
}());

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

1.비동기 처리가 완료되면 next메소드를 통해 제너레이터 객체에 비동기 처리 결과를 전달한다.
2.제너레이터 객체에 전달된 비동기 처리 결과는 user 변수에 할당한다.
제너레이터를 통해 비동기 처리를 동기 처럼 구현 할 수 있으나 코드는 장황해졌다. 따라서 좀더 간편하게 비동기 처리를 구현할 수있는 async/await가 es7에 도입되었다.

async -await 을 통한 비동기 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const fetch = require('node-fetch');

//Promise을 반환하는 함수정의
function getUsers(username){
return fetch(`https://api.github.com/users/${username}`)
.then(res=>res.json())
.then(user =>user.name);
};
async fucntion getUsersAll(){
let user;
user = await getUsers('kim');
console.log(user);

user = await getUsers('park');
console.log(user);

user = await getUsers('lee');
console.log(user);
}
getUsersAll();