Javascript로 개발을 하면서 generator 기능을 활용해볼 기회가 거의 없었는데, 고민해보니까 재미있는 Sample를 만들 수 있어서 포스트를 남겨 봅니다.
이 포스트를 보시는 분들도 Generator가 이렇게 쓰일 수도 있구나 하는 생각을 하실 수 있는 기회가 되었으면 좋겠습니다.
목표
- Generator가 무엇인지 알아본다
- Iterator가 무엇인지 알아본다
- Generator를 이용해서 베스킨라비스 31 게임을 만들어 본다
Generator란?
Generator는 iterable protocol과 iterator protocol을 따르는 일종의 데이터 생성 Object입니다.
벌써 부터 무슨소리인지 모르겠죠? 네, 저도 잘 모르겠습니다.
위의 두가지에 자세히 내용이 나왔는데요. 제가 나름 이해한건 아래와 같습니다.
(혹시 잘못 이해한거라면 알려주세요~ 겸허히 고치겠습니다.)
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를 넣을 수 있게 됩니다.

예를 들자면 사용자가 첫번째 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
'Web' 카테고리의 다른 글
| React JS Old Browser "undefined" Errors (0) | 2022.06.22 |
|---|---|
| Visual Studio Code Live Server SPA url mapping 설정 하기 (0) | 2022.03.06 |
| Vanilla javascript URL Router 만들기 (web components) (0) | 2021.09.05 |
| Vanilla javascript와 Redux (Web component) (0) | 2021.09.04 |
| Web Component 상태관리 만들기 (Vanillajs) (2) | 2021.08.22 |