일반 함수는 하나의 값(혹은 0개의 값)만을 반환합니다.
하지만, 제너레이터(generator)를 사용하면 여러 개의 값을 필요에 따라 하나씩 반환(yield)할 수 있습니다. 제너레이터와 이터러블 객체를 함께 사용하면 손쉽게 데이터 스트림을 만들 수 있습니다.
Generator 함수는 기존 함수 선언(표현)과 다르게 function뒤에 *
키워드가 붙습니다. 그래서 function* 를 키워드로 사용합니다. 제너레이터 함수 형태는 일반 함수 형태처럼 function* 선언문, function* 표현식, GeneratorFunction이 있습니다.
// 선언문 function* sports(one){} // 표현식 const book = function*(one){}; // GeneratorFunction const music = Object.getPrototypeOf(function* (){}).constructor; const gen = new music();
일반 함수 선언문과 동일하게 function* 뒤에 함수명을 작성합니다. 제너레이터 함수를 호출하면 함수 블록({ }) 을 실행한 결과값이 아니라 Generator 오브젝트를 생성하여 반환합니다. 여기서 Generator 오브젝트는 iterator 오브젝트로 next()를 통해 결과값을 받을 수 있습니다.
function* sports(one, two) { yield one + two; } console.log(typeof sports); const obj = sports(1, 2); console.log(typeof obj); console.log(obj.next()); // function // object // {value: 3, done: false}
함수표현식과 표기법은 동일합니다. function* 다음에 함수 이름을 작성하는것은 필수가 아니며 만약 작성하게 된다면 재귀적 호출(recursive call)에 사용할 수 있습니다.
하지만, 일반적으로는 함수 이름을 작성하지 않고 좌측에 설정되는 변수명이 함수 이름이 됩니다. 일반 함수 표현식과 마찬가지로 선언형태만 다를 뿐 function* 선언문과 동일합니다.
const sports = function* (one) { yield one;}; const obj = sports(100); console.log(obj.next()); // {value: 100, done:false}
GeneratorFunction.constructor를 사용해 제너레이터 함수를 생성합니다. 주의할 점은 제너레이터 오브젝트가 아닌 함수를 생성한다는 점입니다.
파라미터를 문자열로 작성하고 마지막 파라미터가 함수 코드가 되고 앞은 파라미터 이름이 됩니다. Generator에는 생성자가 없기 때문에 GeneratorFunction의 constructor를 사용하여 Generator의 constructor를 구하는 사전작업을 먼저 해줘야 합니다.
const gen = function*(){}
const fn = new Function("one", "return one"); console.log(fn(100)); const create = Object.getPrototypeOf(function*(){}).constructor; console.log(create); const sports = new create("one", "yield one"); const obj = sports(100); console.log(obj.next()); // 100 // {value: 100, done: false}
[returnValue] = yield[표현식]
yield 키워드는 next()로 호출할 때마다 하나씩 실행되며 여러개의 yield가 있다면 next()를 호출할 때마다 차례대로 실행 후 반환됩니다. yield 키워드는 제너레이터 함수 실행을 멈추거나 다시 실행할때 사용합니다
예를 들어 제너레이터 함수 내에 yield가 3개가 있다고 할 때 next()를 호출하면 첫 yield 우측 표현식이 평가되어 결과가 반환되면 거기서 함수는 종료합니다.
그리고 다시 next()를 호출할 때 다음 yield 우측 표현식이 평가되어 반환되며 진행되죠. 여기서 표현식을 작성하지 않을 경우에는 undefined
가 반환됩니다.
[returnValue]는 오른쪽의 평가 결과가 설정되지 않고 다음 next()에서 파라미터로 넘겨준 값이 설정됩니다. const returnValue = yield 10;이라면 10이 설정되는 것이 아니라 다음 next()호출 시 넘겨주는 파라미터 값이 returnValue에 설정된다는 의미입니다.
function* sports(one) { yield one + 10; yield; const value = yield one + 50; }; const obj = sports(30); console.log(obj.next()); console.log(obj.next()); console.log(obj.next()); console.log(obj.next(200)); // {value: 40, done: false} // {value: undefined, done: false} // {value: 80, done: false} // {value: undefined, done: true}
yield의 표현식을 평가하면 호출한 곳으로 {value: 값, done: true/false}를 반환합니다.
function* sports(one) { yield one; const check = 20; }; const obj = sports(10); console.log(obj.next()); console.log(obj.next()); // {value: 10, done: false} // {value: undefined, done: true}
function* sports(one) { let two = yield one; let param = yield one + two; yield param + one; }; const obj = sports(10); console.log(obj.next()); console.log(obj.next()); console.log(obj.next(20)); console.log(obj.next()); // {value: 10, done: false} // {value: NaN, done: false} // {value: 30, done: false} // {value: undefined, done: true}
next()는 yield 단위로 실행
GeneratorObject에서 next()의 호출은 yield 수 만큼 할 수가 있으며, 수 만큼 호출이 끝나면 그 뒤로는 next()를 호출해도 {value: undefined, done: true}가 출력됩니다.
next()의 실행 범위
next()를 호출하면 이전 yield의 표현식 종료 지점부터 다음 yield 표현식 종료까지 실행합니다.
function* sports(value) { value += 20; const param = yield ++value; value = param + value; yield ++value; }; const obj = sports(10); console.log(obj.next()); console.log(obj.next(20)); // {value: 31, done: false} // {value: 52, done: false}
yield를 미작성
function* sports(value) { ++value; console.log(value); }; const obj = sports(10); console.log(obj.next()); // 11 // {value: undefined, done: true}
⇒ next()를 호출하면 제너레이터 함수를 실행하여 value 값은 증가하지만, yield가 없으므로 값이 반환되지 않습니다.
return문 작성
function * sports(value){ return ++value; }; const obj = sports(10); console.log(obj.next()); console.log(obj.next()); // {value: 11, done: true} // {value: undefined, done: true}
⇒ next()를 호출하면 제너레이터 함수를 실행하여 증가된 value값을 반환합니다.
⇒ 하지만, return으로 값을 반환하면 {done: true}이며 이터레이션이 종료됩니다. 더 이상 호출해도 반환값이 없습니다.
일반적인 함수는 호출할 때 마다 변수의 초기값(undefined)를 설정하는 작업을 거칩니다. 하지만, 제너레이터 함수는 다릅니다.
제너레이터 함수에 파라미터로 10을 작성하고 next()를 호출하며 [returnValue]에 값을 세팅하는 것들과 평가결과 등이 모두 저장됩니다. 이 후 다음 next()를 호출할 때 yield 표현식이 평가될 때 사용합니다.
마치 비디오 테이프의 pause/play
와 같습니다. 일반 함수가 로직을 완전히 수행 후 종료되어 재호출하면 처음부터 다시 시작한다면, 제너레이터 함수는 1회성이며 next()를 호출 할 때마다 yield에 따라 함수 실행이 진행됩니다.
const sports = function* (param) { const one = param + 10; yield one; var two = 2; yield one + two; }; const obj = sports(10); console.log(obj.next()); console.log(obj.next());
const obj = sports(10);
제너레이터 오브젝트를 생성하는 단계에서 파라미터를 할당하고 초깃값을 설정합니다.
두 번째 console.log(obj.next());
next()로 실행할 때 마다 초깃값을 설정하지 않기에 첫 번째 next()에서 실행하여 설정된 변수들이 유지됩니다.
동적으로 yield를 사용하거나 반복문(while, for..)를 통한 사용시 반드시 중단점을 설정해야 합니다.
let status = true; function* sports() { let count = 0; while(status) { yield ++count; }; } const obj = sports(); console.log(obj.next()); console.log(obj.next()); status = false; console.log(obj.next()); // {value: 1, done: false} // {value: 2, done: false} // {value: undefined, done: true}
한 줄에 다수의 yield와 return을 작성하여 처리할 수도 있습니다.
function* sports(){ return yield yield yield; }; const obj = sports(); console.log(obj.next()); console.log(obj.next(10)); console.log(obj.next(20)); console.log(obj.next(30)); // {value: undefined, done: false} // {value: 10, done: false} // {value: 20, done: false} // {value: 30, done: true}
yield를 대괄호 안에 여러개 작성할 수도 있습니다.
function* sports() { return [yield yield]; }; const obj = sports(); console.log(obj.next()); console.log(obj.next(10)); const last = obj.next(20); console.log(last); console.log(last.value); // {value: undefined, done: false} // {value: 10, done: false} // {value: [20], done: true} // [20]
제너레이터 함수를 매번 next()를 호출하여 반환 오브젝트{value, done}에서 꺼내어 쓰고, 또 done을 검사해 추가 반복 가능성 여부를 검사하는 것은 불편합니다.
따라서, 이터레이터 오브젝트인 제너레이터 함수이기에 for-of사용하여 필요한 결과를 얻는 것이 가능합니다. next()와 달리 value만 받아오기에 반복문을 종료시키는 작업이 필요합니다.
function* sports(count) { while(true) { yield ++count; }; }; for (let point of sports(10)) { console.log(point); if(point > 12) { break; }; } // 11 // 12 // 13
이터레이터를 종료시키는 메소드입니다. 파라미터로 작성한 값을 반환값으로 가져옵니다. 기존의 next()가 play/pause와 비슷하다면 return()은 stop이라 볼 수 있습니다.
function* sports(count) { while(true) { yield ++count; }; }; const obj = sports(10); console.log(obj.next()); console.log(obj.return(70)); console.log(obj.next(50)); // {value: 11, done: false} // {value: 70, done: true} // {value: undefined, done: true}
의도적으로 Error를 발생시켜 예외상황을 유도할 때 사용하는 메소드입니다. 제너레이터 함수의 catch()문에서 에러를 받습니다.
throw() 메소드
function* sports(){ try { yield 10; } catch(message){ yield message; }; yield 20; }; const obj = sports(); console.log(obj.next()); console.log(obj.throw("에러 발생")); console.log(obj.next()); // {value: 10, done: false} // {value: 에러 발생, done: false} // {value: 20, done: false}
throw 문
function* sports() { throw "에러 발생"; yield 10; } const obj = sports(); try { const result = obj.next(); } catch(message){ console.log(message); } console.log(obj.next()); // 에러 발생 // {value: undefined, done: true}
⇒ obj.next()를 실행하면 제너레이터 안에서 throw를 만나며 에러가 발생됩니다. 이렇게 제너레이터 함수에서 에러가 발생하면 이터레이터는 종료됩니다.
⇒ 따라서, 마지막 줄에서 next()를 호출해도 {value: undefined, done:true}를 반환할 뿐, yield 10;은 실행되지 않습니다.
yield* expression
yield에 *키워드를 붙히면 우측의 표현식에 따라 다른 처리로직을 따르게 되는데요. 기본 골조는 play/pause를 유지하지만, 그 처리되는 방식이 달라집니다.
yield*의 표현식이 배열인 경우 next()를 호출할 때마다 배열의 엘리먼트를 순서대로 반환한 뒤 다음 yield를 찾습니다.
function* sports() { yield* [10, 20]; }; const obj = sports(); console.log(obj.next()); console.log(obj.next()); console.log(obj.next()); // {value: 10, done: false} // {value: 20, done: false} // {value: undefined, done: true}
function* point(count) { yield count + 5; yield count + 10; }; function* sports(value) { yield* point(value); yield value + 20; }; const obj = sports(10); console.log(obj.next()); console.log(obj.next()); console.log(obj.next()); // {value: 15, done: false} // {value: 20, done: false} // {value: 30, done: false}
function* sports(point) { yield point; yield* sports(point + 10); }; const obj = sports(10); console.log(obj.next()); console.log(obj.next()); console.log(obj.next()); // {value: 10, done: false} // {value: 20, done: false} // {value: 30, done: false}