ABOUT ME

-

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

     

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

    댓글

Designed by Tistory.