-
ES6+ | Generator 제너레이터JavaScript/ES6+ 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 제너레이터에 대해 정리해보았습니다.
참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]
'JavaScript > ES6+' 카테고리의 다른 글
ES6+ | Array Methods 배열 메서드 간단 정리 (0) 2020.05.21 ES6+ | Async Await (0) 2020.05.18 ES6+ | Promise (0) 2020.04.07 ES6+ | Destructuring 구조 분해 할당 & 객체 확장 표현식 (0) 2020.03.06 ES6+ | Spread Operator 전개 연산자 (0) 2020.03.06