JavaScript/ES6+
ES6+ | Generator 제너레이터
pathas
2020. 5. 13. 16:38
Generator
제너레이터(generator)는 함수의 실행을 중간에 멈추고 재개할 수 있는 ES6의 독특한 기능
특징
- 실행을 멈출 때 값을 전달할 수 있기 때문에 반복문에서 제너레이터가 전달하는 값을 하나씩 꺼내서 사용할 수 있음
- 제너레이터는 보통의 컬렉션과 달리 값을 미리 만들어 놓지 않음
→ 필요한 순간에 값을 계산해서 전달 가능하기에 메모리 측면에서 효율적 - 다른 함수와 협업 멀티태스킹(cooperative multitasking) 가능
- 제너레이터가 실행을 멈추고 재개할 수 있기 때문에 멀티태스킹이 가능해짐
- 협업이라고 부르는 이유는 제너레이터가 실행을 멈추는 시점을 자발적(non-preemptive)으로 선택하기 때문
제너레이터 구조
별표와 함께 정의된 함수와 그 함수가 반환하는 제너레이터 객체로 구성
function* f1() {
yield 10;
yield 20;
return 'finished';
}
const gen = f1();
console.log(gen); // f1 {<suspended>}
- 별표와 함께 정의된 함수는 제너레이터 함수
- yield 키워드를 사용하면 함수의 실행을 멈출 수 있음
- 제너레이터 함수를 실행하면 제너레이터 객체가 반환됨
제너레이터 객체 메서드
| 메서드 | 설명 |
|---|---|
| next | yield 키워드를 만날 때까지 실행되고 데이터 객체 반환 yield 사전적 의미: (수익 등의 결과물을) 내다, 넘겨주다, 양도하다 |
| return | 데이터 객체의 done 속성값이 참이 됨 |
| throw | 예외가 발생한 것으로 처리되어 제너레이터 함수 내부의 catch 문으로 들어감 |
next 메서드
function* f1() {
console.log('f1-1');
yield 10;
console.log('f1-2');
yield 20;
console.log('f1-3');
return 'finished';
}
const gen = f1();
console.log(gen.next());
// f1-1
// {value: 10, done: false}
console.log(gen.next());
// f1-2
// {value: 20, done: false}
console.log(gen.next());
// f1-3
// {value: 'finished', done: true}
- 제너레이터 함수를 실행하면 제너레이터 객체만 반환되고 실제로 함수 내부 코드는 실행되지 않음
→ 제너레이터 함수 호출 시 로그가 출력되지 않는 이유 - next 메서드 호출 시 yield 키워드를 만날 때까지 실행되고 데이터 객체 반환
- yield 키워드를 만나면 데이터 객체의 done 속성은 거짓이 되고, 만나지 못하면 참이 됨
- yield 키워드 오른쪽에 입력한 값이 데이터 객체의 value 속성의 값으로 넘어옴
- 제너레이터 객체가 next 메서드를 갖고 있다는 사실은 제너레이터 객체가 반복자(iterator)라는 것을 암시
return 메서드
const gen = f1();
console.log(gen.next());
// f1-1
// {value: 10, done: false}
console.log(gen.return('abc'));
// {value: 'abc', done: true}
console.log(gen.next());
// {value: undefined, done: true}
- return 메서드를 호출하면 데이터 객체의 done 속성값은 참이 됨
이후에 next 메서드를 호출해도 done 속성값은 참 - return 메서드 호출 시 인수로 전달한 값이 value 속성의 값이 됨
이후 next 호출 시 value 속성값은 'undefined'
throw 메서드
function* f1() {
try {
console.log('f1-1');
yield 10;
console.log('f1-2');
yield 20;
} catch(e) {
console.log('f1-catch', e);
}
}
const gen = f1();
console.log(gen.next());
// f1-1
// {value: 10, done: false}
console.log(gen.throw('some error'));
// f1-catch some error
// {value: undefined, done: true}
- try catch 문을 사용해서 제너레이터 함수 내부에서 예외 처리
- throw 메서드 호출 시 예외가 발생한 것으로 처리되어 catch 문으로 들어감
- 데이터 객체의 done 속성값은 참이 됨
반복 가능하면서 반복자인 제너레이터 객체
제너레이터 객체는 다음 조건을 만족하기 때문에 반복 가능한 객체이면서 반복자
반복 가능(iterable)한 객체 조건
- Symbol.iterator 속성값으로 함수를 갖고 있음
- 해당 함수를 호출하면 반복자를 반환
- 배열은 대표적인 반복 가능 객체
const arr = [10, 20, 30];
// Symbol.iterator 속성값으로 함수를 가짐
const iter = arr[Symbol.iterator]();
// 함수가 반환한 iter 변수는 반복자
console.log(iter.next()); // {value: 10, done: false}
반복자 조건
- next 메서드를 가짐
- next 메서드는 value와 done 속성값을 가진 객체를 반환
- done 속성값은 작업이 끝났을 때 참이 됨
반복 가능한 객체이면서 반복자인 제너레이터
function* f1() {
console.log('f1-1');
yield 10;
console.log('f1-2');
yield 20;
console.log('f1-3');
return 'finished';
}
const gen = f1();
console.log(gen[Symbol.iterator]() === gen); // true
- Symbol.iterator 속성값을 호출한 결과가 반복자인 자기 자신임
for of 문 활용
반복 가능한 객체는 for of 문과 전개 연산자에서 유용하게 쓰임
function* f1() {
yield 10;
yield 20;
yield 30;
}
for(const v of f1()) {
console.log(v);
}
// 10
// 20
// 30
const arr = [...f1()];
console.log(arr); // [10, 20, 30]
- for of 문은 반복 가능한 객체로부터 반복자를 얻음
done 속성값이 참이 될 때까지 next 메서드 호출 - 전개 연산자도 done 속성값이 참이 될 때까지 값을 펼침
제너레이터 활용
제너레이터 , 반복자, 반복 가능한 객체를 이용하면 함수형 프로그래밍의 대표적인 함수를 쉽게 구현 가능
map, filter, take 함수 구현
함수형 프로그래밍의 대표적인 함수로는 map, filter, take 함수 등이 있음
function* map(iter, mapper) {
for (const v of iter) {
yield mapper(v);
}
}
function* filter(iter, test) {
for (const v of iter) {
if (test(v)) {
yield v;
}
}
}
function* take(n, iter) {
for (const v of iter) {
if (n <= 0) return;
yield v;
n--;
}
}
const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const result = take(3, map(filter(values, n => n % 2 === 0), n => n * 10));
console.log([...result]); // [20, 40, 60]
- 제너레이터 함수 내부에서 반복 가능한 객체 이용
- 세 함수는 제너레이터 덕분에 새로운 배열 객체를 생성하지 않음
- 세 함수는 연산이 필요한 순간에만 실행
- 함수를 호출하면 제너레이터 객체만 생성되고 실제 연산은 수행 X
- 값이 필요한 [...result] 에서 제너레이터 객체를 통해서 다음 값을 요청
- 이렇게 필요한 순간에만 연산하는 방식을 지연 평가(lazy evaluation)라고 함
제너레이터로 자연수 집합 표현
제너레이터를 사용하면 필요한 연산만 수행한다는 장점이 있음
function* naturalNumbers() {
let v = 1;
while (true) {
yield v++;
}
}
const values = naturalNumbers();
const result = take(3, map(filter(values, n => n % 2 === 0), n => n * 10));
console.log([...result]); // [20, 40, 60]
- 제너레이터 함수를 사용하지 않았다면 while 문이 계속 실행되면서 프로그램이 먹통이 되었을 것
- 전개 연산자를 사용하면 자연수 1부터 6까지만 연산에 사용
제너레이터 함수끼리 호출
제너레이터 함수에서 다른 제너레이터 함수를 호출할 때는 yield* 키워드 사용
function* g1() {
yield 2;
yield 3;
}
function* g2() {
yield 1;
yield* g1();
yield 4;
}
console.log(...g2()); // 1 2 3 4
- yield* 키워드 오른쪽에는 반복 가능한 객체가 올 수 있도록 설계되어 있음
yield* 키워드로 반복 가능한 객체 처리하기
위에서 작성한 g2()함수를 반복 가능한 객체로 바꿔서 작성
function* g2_second() {
yield 1;
for (const value of g1()) {
yield value;
}
yield 4;
}
function* g2_third() {
yield 1;
yield* [2,3];
yield 4;
}
console.log(...g2_second()); // 1 2 3 4
console.log(...g2_third()); // 1 2 3 4
- 앞에서 작성한 yield* g1(); 코드는 g2_second() 함수 내부의 반복문과 같이 해석될 수 있음
- yield* 키워드 오른쪽에는 반복 가능한 모든 객체가 올 수 있음
제너레이터 함수로 데이터 전달하기
제너레이터 함수는 외부로부터 데이터를 받아서 소비할 수 있음
next 메서드를 호출하는 쪽에서 제너레이터 함수로 데이터 전달
function* f1() {
const data1 = yield;
console.log(data1); // 10
const data2 = yield;
console.log(data2); // 20
}
const gen = f1();
gen.next();
gen.next(10);
gen.next(20);
- 첫 번째 next 메서드 호출은 제너레이터 함수의 실행이 시작되도록 하는 역할만 수행
- next 메서드의 인수로 데이터를 전달할 수 있으며, 전달된 인수는 yield 키워드의 결괏값으로 받을 수 있음
협업 멀티태스킹
제너레이터는 실행을 멈추고 재개할 수 있기 때문에 멀티태스킹이 가능,
멈추는 시점을 제너레이터가 자발적으로 선택하기 때문에 협업이라는 단어가 붙음↔ 멈추는 시점을 자발적으로 선택하지 못하면 선점형(preemptive) 멀티태스킹
function* minsu() {
myMsgList = [
'안녕 나는 민수야',
'만나서 반가워',
'내일 영화 볼래?',
'시간 안 되니?',
'내일모레는 어때?',
];
for (const msg of myMsgList) {
console.log('수지:', yield msg);
}
}
function suji() {
const myMsgList = [
'', '안녕 나는 수지야', '그래 반가워', '...'
];
const gen = minsu();
for (const msg of myMsgList) {
console.log('민수:', gen.next(msg).value);
}
}
suji();
/*
민수: 안녕 나는 민수야
수지: 안녕 나는 수지야
민수: 만나서 반가워
수지: 그래 반가워
민수: 내일 영화 볼래?
수지: ...
민수: 시간 안 되니?
*/
- 제너레이터 함수는 yield 키워드를 통해서 자발적으로 자신의 실행을 멈춤
- 일반 함수에서는 제너레이터 객체의 next 메서드를 호출해서 제너레이터 함수가 다시 실행되도록 함
제너레이터 함수 예외 처리
제너레이터 함수에서 발생한 예외는 next 메서드를 호출하는 외부 함수에 영향을 미침
function* genFunc() {
throw new Error('some error');
}
function func() {
const gen = genFunc();
try {
gen.next();
} catch (e) {
console.log('in catch:', e);
}
}
func(); // in catch: Error: some error
- 제너레이터 객체 생성 시점에는 예외 발생 X
- next 메서드가 호출되면 제너레이터 함수의 예외가 일반 함수에 영향을 줌
- 일반 함수의 실행은 catch 문으로 이동
리덕스 사가를 공부하기 전에 ES6 제너레이터에 대해 정리해보았습니다.
참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]