Javascript Generator & Iterator 설명 및 게임 sample

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

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

목표

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

 

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

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

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

 

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>;
}

Array Constructor에서 iterable object를 만들고,

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

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

 

iterator protocol

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

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

 

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}
    }
}

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])

결과

[
   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
]

 

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

 

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>;
}

위는 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]);

결과는

[ 1, 2, 3 ]

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

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

단지 이게 이유일까요?

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

 

Generator 구조

new 가 없다

function* generator() {

}

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

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

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

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

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

generator는 new가 없이 

let gen = generator(31);

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

 

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());

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

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

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

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

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

 

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

 

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

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

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

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

npm install prompt-sync

위의 패키지를 인스톨 후에

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

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

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

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

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

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

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

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

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

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

count += computer;

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

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

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

let userInput = yield count;

그런데 어떻게 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 += userInput

사용자로 부터 입력받은 값은 다시 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;
}

가 됩니다.

이제 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;
}

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

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

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

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

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

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

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

 

작동 결과

위의 코드를 합쳐서 실행 시켜 보면 아래와 같이 컴퓨터와의 배스킨라빈스 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 = 33

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

 

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

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

 

javascript generator 기능 검증

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

gist.github.com

 

728x90
반응형