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 제너레이터에 대해 정리해보았습니다.

 

참고도서: [ 실전 리액트 프로그래밍 / 저자_ 이재승 / 출판사_ 프로그래밍 인사이트 ]