Javascript Generator & Iterator 설명 및 게임 sample

Javascript로 개발을 하면서 generator 기능을 활용해볼 기회가 거의 없었는데, 고민해보니까 재미있는 Sample를 만들 수 있어서 포스트를 남겨 봅니다.

이 포스트를 보시는 분들도 Generator가 이렇게 쓰일 수도 있구나 하는 생각을 하실 수 있는 기회가 되었으면 좋겠습니다.

1. 목표

  • Generator가 무엇인지 알아본다
  • Iterator가 무엇인지 알아본다
  • Generator를 이용해서 베스킨라비스 31 게임을 만들어 본다

 

2. Generator란?

Generator는 iterable protocol과 iterator protocol을 따르는 일종의 데이터 생성 Object입니다.

벌써 부터 무슨소리인지 모르겠죠? 네, 저도 잘 모르겠습니다. 

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol

위의 두가지에 자세히 내용이 나왔는데요. 제가 나름 이해한건 아래와 같습니다.

(혹시 잘못 이해한거라면 알려주세요~ 겸허히 고치겠습니다.)

 

3. iterable protocol

Array와 Map과 같이 기본적으로 iterable 가능한 객체는 내부에 기본적으로 iterator 기능을 제공하고 있습니다.

ES2015 대상으로 정의된 Array의 typescript의 내용을 보면 아래와 같습니다.

interface ArrayConstructor {
...
    /**
     * Creates an array from an iterable object.
     * @param arrayLike An array-like object to convert to an array.
     * @param mapfn A mapping function to call on every element of the array.
     * @param thisArg Value of 'this' used to invoke the mapfn.
     */
    from<T, U>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => U, thisArg?: any): U[];
...
}

interface Array<T> {
    /** Iterator */
    [Symbol.iterator](): IterableIterator<T>;

    /**
     * Returns an iterable of key, value pairs for every entry in the array
     */
    entries(): IterableIterator<[number, T]>;

    /**
     * Returns an iterable of keys in the array
     */
    keys(): IterableIterator<number>;

    /**
     * Returns an iterable of values in the array
     */
    values(): IterableIterator<T>;
}javascript

Array Constructor에서 iterable object를 만들고,

그 내부적으로 Symbol.iterator가 존재하고 Iterable을 제공가능한 entries, keys, values가 제공됩니다.

짧게 말하자면 [Symbol.iterator] 가 제공 가능하다면 iterable 프로토콜을 따른다고 볼수 있는것 같습니다.

 

4. iterator protocol

위에서 제공된 iterable object는 iterator protocol을 따라야 합니다.

  • next() 메서드가 제공되어야 한다.
  • next() 메서드의 결과값은
    • done : iterator 시퀀스가 완료 되었는지 표시
    • value : 현재 시퀀스의 특정 부분 결과 값

 

5. 2가지 조건을 따르는 Custom Iterator Class

위 2가지 조건을 만족하는 class 하나를 만들어 보겠습니다.

class CustomIterator {
    constructor(start = 0, step = 1, end = 100){
        this.start = start;
        this.step = step;
        this.end = end;
    }

    [Symbol.iterator](){
        return this;
    }

    next() {
        this.start += this.step
        return this.start >= this.end ? {done:true,value:undefined} : {done:false,value: this.start}
    }
}javascript

Custom itoerator는 [sysmbo.iterator]를 제공함으로써 iterable을 만족시키고, next기능을 제공함으로써 iterator protocol을 제공 하였습니다.

  • step 값만큼 start에서부터 end까지 jump하면서 숫자를 return하는 class입니다.

위의 코드를 아래와 같이 실행해 보면 iterator 작동이 정상적으로 진행 되는 것을 확인할 수 있습니다.

const itor = new CustomIterator(0,2,100);

console.log([...itor])javascript

결과

[
   2,  4,  6,  8, 10, 12, 14, 16, 18, 20, 22,
  24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44,
  46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66,
  68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88,
  90, 92, 94, 96, 98
]javascript

 

그럼 Generator는 위 두가지를 따르는지 알아볼까요?

 

6. Generator의 Iterator 정의

interface Generator<T = unknown, TReturn = any, TNext = unknown> extends Iterator<T, TReturn, TNext> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return(value: TReturn): IteratorResult<T, TReturn>;
    throw(e: any): IteratorResult<T, TReturn>;
    [Symbol.iterator](): Generator<T, TReturn, TNext>;
}javascript

위는 generator의 타입스크립트입니다. 

Iterator를 extends하고 있는 것을 확인할 수있습니다.

이를 통해서 iterable을 제공하고 있고, next 메서드를 통해서 iterator protocol을 따르는 것을 확인할 수 있습니다.

즉, Generator는 custom Iterator의 역할을 하고 있습니다.

그래서 Generator를 통해서 Iterator 처리를 하는 Sample코드를 만들 수 있습니다.

function* generator() {
    yield 1;
    yield 2;
    yield 3;
  }
  
  const gen = generator(); // "Generator { }"
  
  console.log([...gen]);javascript

결과는

[ 1, 2, 3 ]javascript

iterator custom class를 직접 만드는 것 보다는 generator를 만드는게 편하겠네요.

그래서 generator를 iterator 대용으로 많이 사용 합니다.

단지 이게 이유일까요?

이제 generator의 구조에 대해서 알아보면서 다른 방식으로 사용이 가능하다는 것을 확인해 보겠습니다.

 

7. Generator 구조

7.1. new 가 없다

function* generator() {

}javascript

위에 조금 특이한 모양이 보이지 않나요?

function 옆에 '*' 이 별이 붙어있습니다.

이 별을 붙임으로써 generator function이라는 것을 확인 시켜 줍니다.

이를 통해서 사용자는 new Instance를 더이상 활용할 수 없습니다.

기존 function에 대한 instance를 생성하려고 하면 new 키워드를 사용해야 했지만,

generator는 new가 없이 

let gen = generator(31);javascript

이런식으로 generator function을 직접 call함으로써 instance를 얻어낼 수 있습니다.

 

7.2. yield 키워드를 제공한다.

yield 키워드는 오직 generator만을 위해서 만들어진 키워드 입니다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/yield

generator안에서 pause와 resume 기능을 제공합니다.

사용자가 next() 메서드를 call하면 yield가 있는 곳까지만 실행을 하고 결과 값을 돌려줍니다.

다음에 또 next() 메서드를 call하면 또 다음 yield가 있는 곳까지만 작동합니다.

function* generator() {
    console.log("next를 한번 불렀어요");
    yield 1;
    console.log("next를 두번 불렀어요");
    yield 2;
    console.log("next를 세번 불렀어요");
    yield 3;
    console.log("next를 네번 불렀어요");
  }
  
  const gen = generator(); // "Generator { }"
  
  console.log(gen.next());
  console.log(gen.next());
  console.log(gen.next());
  console.log(gen.next());javascript

위의 코드를 실행시킨 결과는 아래와 같습니다.

next를 한번 불렀어요
{ value: 1, done: false }
next를 두번 불렀어요
{ value: 2, done: false }
next를 세번 불렀어요
{ value: 3, done: false }
next를 네번 불렀어요
{ value: undefined, done: true }javascript

next를 4번 부른 후에 드디어 done이 true가 되었습니다.

참고로 next메서드는 파라메터를 넣을 수 있습니다.

이건 아래 게임만들기에서 예를 보여드리겠습니다.

 

이제 지금까지 알아본 Generator 기능을 통해서 베스킨라빈스 31을 만들어 보겠습니다.

 

8. 베스킨라빈스 31 게임 만들기

nodejs를 기본적으로 사용한다고 생각하겠습니다.

혹시 이거 무슨 게임인지 모르는 분 없겠죠? 1부터 3까지의 숫자를 말해서 숫자를 sum해가면서 31이라는 숫자를 말하는 사람이 지는 게임입니다.

우선 사용자의 입력을 받을 package를 install하겠습니다.

npm install prompt-sync

위의 패키지를 인스톨 후에

import prompt from "prompt-sync"
const prom = new prompt();javascript

위와 같이 모듈을 로드해 줍니다.

function* generator(limit=31){
    let count = 0;
    while(count < limit){

    }
    console.log("User failed = " + count)
    return count;
}javascript

이제 31을 기준으로 while문을 진입하면

  • 컴퓨터가 1에서 부터 3까지 선택을 하고
    • 만약에 해당 선택이 31을 초과하면 컴퓨터가 진다
  • 사용자가 1에서 3까지 선택을 한다
    • 만약에 사용자가 31을 초과하면 사람이 진다.

라는 기준으로 코드를 만들어 보겠습니다.

우선 컴퓨터가 1에서 3까지 선택할 수 있도록 코드를 만들어 보겠습니다.

const computer = Math.floor((Math.random() * 10) % 3) + 1;javascript

선택된 값은 count에 더해줍니다.

count += computer;javascript

더해진 값이 limit즉 31을 포함해서 넘어가면 컴퓨터의 패배가 됩니다.

        if(limit <= count) {
            console.log("Computer failed = " + count)
            return count;
        }javascript

넘어가지 않았다면 사용자가 1에서 3사이에 입력을 합니다.

let userInput = yield count;javascript

그런데 어떻게 yield로 입력을 할 수 있을까요?

yield를 만나면 count의 값을 next() 메서드를 콜한 결과값으로 return합니다.

이후 다시 next를 call할때 우리는 method를 넣을 수 있게 됩니다.

javascript iterator and generator
iterator and generator

예를 들자면 사용자가 첫번째 next를 call하면 컴퓨터가 3을 선택하고, 사용자가 2를 선택하고 next()를 콜하면 기존 3+2값으로 5가 입력되고 다시 컴퓨터가 2를 선택하면 7이라는 값이 결과로 return되게 됩니다.

다소 헷깔릴 수 있지만 작동하는 코드를 보면 이럴수도 있구나! 할껍니다.

        let userInput = yield count;
        console.log("=====  User Input   ====> " + userInput)
        count += userInputjavascript

사용자로 부터 입력받은 값은 다시 count에 sum이 되고 만약에 31을 넘어서게 되면 사용자의 패배가 됩니다.

이 코드를 모두 합치면

function* generator(limit=31){
    let count = 0;
    while(count < limit){
        const computer = Math.floor((Math.random() * 10) % 3) + 1;
        console.log("=====  computer Input   ====> " + computer)
        count += computer;
        if(limit <= count) {
            console.log("Computer failed = " + count)
            return count;
        }

        let userInput = yield count;
        console.log("=====  User Input   ====> " + userInput)
        count += userInput
    }
    console.log("User failed = " + count)
    return count;
}javascript

가 됩니다.

이제 generator function을 이용해서 사용자입력을 받아보겠습니다.

let gen = generator(31);
let lastObj = gen.next();
let done = lastObj.done;
let lastValue = lastObj.value;

while(!done){
    console.log("Current Value = " + lastValue)
    const result = parseInt(prom("what is your number?"));
    lastObj = gen.next(result);
    lastValue = lastObj.value;
    done = lastObj.done;
}javascript

게임의 시작은 컴퓨터 부터임으로 파라메터가 없이 next()를 시작했습니다.

그리고 generator가 종료되기 전까지 지속적으로 사용자 입력을 받을 수 있도록 done을 flag를 활용했습니다.

마지막으로 prompt 기능을 이용해서 사용자의 입력을 next에 파라메터로 넣어줬습니다.

이게 yield의 멋있는 기능인데요.

  • (이후 코드) = yield (이전 코드);

이전 코드의 결과 값이 next()의 return 값이 되고

next(파라메터값) 의 파라메터 값이 (이후 코드)의 입력 값이 되게 됩니다.

 

9. 작동 결과

위의 코드를 합쳐서 실행 시켜 보면 아래와 같이 컴퓨터와의 배스킨라빈스 31 게임을 할 수 있습니다.

=====  computer Input   ====> 3
Current Value = 3   
what is your number?3
=====  User Input   ====> 3
=====  computer Input   ====> 2
Current Value = 8
what is your number?3
=====  User Input   ====> 3
=====  computer Input   ====> 3
Current Value = 14
what is your number?3
=====  User Input   ====> 3
=====  computer Input   ====> 1
Current Value = 18
what is your number?3
=====  User Input   ====> 3
=====  computer Input   ====> 3
Current Value = 24
what is your number?3
=====  User Input   ====> 3
=====  computer Input   ====> 3
Current Value = 30
what is your number?3
=====  User Input   ====> 3
User failed = 33javascript

컴퓨터에게 패배 하고 말았군요 ㅠㅠ

 

full 소스는 여기에 있습니다.

https://gist.github.com/theyoung/ec26ec8d5408664118e0217b4ab677f7

 

javascript generator 기능 검증

javascript generator 기능 검증. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

 

728x90
반응형