부트캠프/자바스크립트 완벽 가이드

12장. 이터레이터와 제너레이터

하이고니 2023. 2. 12. 17:55

 

 

1. 자바스크립트의 순회

2. 이터러블과 이터레이터

3. 배열과 이터레이터

4. 이터러블 기반으로 map(), filter() 고쳐 쓰기

5. 이터레이터와 제너레이터

 

 

 

 

 

자바스크립트의 순회

 

자바스크립트의 순회를 이해하려면 세 가지를 이해해야 한다.

 

1. 이터러블 객체(순회할 수 있는 객체)

2. 순회를 수행하는 이터레이터 객체

3. 순회 결과 객체(value & done)

 

이터러블 객체를 순회할 때는 먼저 이터레이터 메서드를 호출해 이터레이터 객체를 얻는다.

그리고 반환 값의 done 프로퍼티가 true일 때까지 이터레이터 객체의 next() 메서드를 반복 호출한다.

 

이터러블과 이터레이터

 

이터러블: [Symbol.iterator]() 를 가진 객체

[Symbol.iterator]() : 이터레이터를 리턴하는 메서드 

(배열은 Array.prototype의 Symbol.iterator를 상속받기 때문에 이터러블이다. 문자열도)

 

이터레이터: next()를 가진 객체. next() 메서드로 순환할 수 있는 객체.

next() : { value: 값, done: true / false } 형태의 객체를 리턴하는 메서드

 

 

 

 

 

 

배열과 이터레이터

 

이터레이터는 생성된 후에 next() 만 가지고 있다.

이전에 얻어온 값의 바로 다음 값만 가져올 수 있다는 것.

 

배열은 랜덤 액세스 가능. 인덱스로 어디에 있는 값이든 가져올 수 있다.

이터레이터를 배열로 전환하려면 순회를 다 끝마쳐야 함.

 

기능적으로 봤을 때 이터레이터는 배열의 부분집합이다.

배열 역시 이터러블 객체라는 소리다.

그럼 만일 배열에서 이터러블 표식을 없애버리면 어떻게 될까?

 

let arr = [1, 2, 3];
for(const el of arr) console.log(el);	// 1, 2, 3

arr[Symbol.iterator] = null;
for(const el of arr) console.log(el);	// TypeError: arr is not iterable

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Q. 배열에 이터레이터의 기능이 다 있는 거라면, 배열만 있어도 되는 거 아닌가요? 이터레이터는 왜 있어야 하죠?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1. 기능이 더 많다는 게 무조건 더 좋은 것은 아니다.

2. 당연하게도 이터레이터는 메모리를 덜 사용한다.

 

const array = [1, 2, 3, 4, 5, 6, 7];
// array[0] == 1, array[3] == 4
// array.length == 7

const iterator = (function() {
  let num = 1;

  return {
    next: function() {
      return (
        num > 7 ?
        { done: true } :
        { done: false, value: num++ }
      );
    }
  };
})();

// only "next()" is supported

console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

 

위 예시에서 배열은 생성되는 순간 자신의 모든 아이템(1, 2, 3, 4, 5, 6, 7)을 메모리에 올려놓아야 한다.

반면, 이터레이터는 num 변수 하나로 끝.

 

배열의 길이가 10000 이상이라면?

100000 이상이라면?

 

차이는 어마어마할 것이다.

 

이터레이터는 외부 데이터를 다루기 위한 수단으로도 사용된다.

이미지 분석을 위해, 특정 디렉토리에 있는 수많은 이미지들을 불러와서 처리를 하는 작업을 떠올려보자.

배열로 처리하려면 모든 이미지를 메모리에 올려놓고 작업을 시작해야 한다.

 

이터레이터는 현재 다루고 있는 이미지 하나만 메모리에 올려놓으면 된다.

 

프로그래머 입장에서는 비슷하게 루프를 돌린다고 생각하겠지만, 시스템에 주는 부담에는 큰 차이가 있다.

 

우리가 많이 사용하는 map, filter, reduce 같은 메서드도 곰곰히 생각해보면 이터레이터에 더 잘 맞는 것들이다.

바로 다음 값만 알면 되기 때문에.

 

 

이터러블 기반으로 고쳐 쓴 map(), filter()

 

function map(iterable, f) {
    let iterator = iterable[Symbol.iterator]();
    return {
        [Symbol.iterator]() { return this; },
        next() {
            let v = iterator.next();
            if (v.done) {
                return v;
            } else {
                return { value: f(v.value) };
            }
        }
    };
}

 

 

Range의 인스턴스 fromOnetoFive 와, 입력값을 제곱해주는 콜백함수를 map의 인자로 넣어주고 ... 연산자를 사용했다.

map은 이터러블 fromOneToFive를 순회하면서 done의 값이 true가 될 때까지 value를 함수 f에 입력한다.

 

next() 메서드 안에 있는 let v = iterator.next(); 는

fromOneToFive의 [Symbol.iterator]() 가 리턴하는 객체 속 next()를 호출하고,

next <= last 가 될 때까지 이터러블을 순회한다.

 

 

 

 

function filter(iterable, predicate) {
    let iterator = iterable[Symbol.iterator]();
    return {
        [Symbol.iterator]() { return this; },
        next() {
            for(;;) {
                let v = iterator.next();
                if (v.done || predicate(v.value)) {
                    return v;
                }
            }
        }
    };
}

 

 

 

제너레이터

이터러블이며 동시에 이터레이터

= 이터레이터를 리턴하는 함수

 

(async가 Promise를 리턴하는 함수이듯, 제너레이터는 이터레이터를 리턴하는 함수다.)

 

제너레이터 함수를 사용하면, 이터레이터 프로토콜을 준수하는 방식보다 간편하게 이터러블을 구현할 수 있다.

 

 

이터러블 객체를 만드는 방식

 

let range = {
    from: 1,
    to: 5,

    [Symbol.iterator]() {

        return {
            current: this.from,
            last: this.to,

            next() {
                if (this.current <= this.last) {
                    return { done: false, value: this.current++ };
                } else {
                    return { done: true };
                }
            }
        };
    }
};

console.log([...range]);	// 1, 2, 3, 4, 5

 

 

 

제너레이터 (function 옆에 *을 붙인다.)

 

const range = function* () {
    let i = 1;
    while(true) {
        if (i <= 5) yield ++i;
        else return;
    }
};

for (let i of range()) {
    console.log(i); // 1, 2, 3, 4, 5
}

 

제너레이터 함수는 일반 함수와는 다른 독특한 동작 방식을 가지고 있다.

일반 함수처럼 함수의 코드 블록을 한 번에 실행하지 않고

함수 코드 블록의 실행을 일시 중지했다가 필요한 시점에 재시작할 수 있는 특수한 함수다.

 

range 함수는 이터레이터를 반환한다.

어차피 while 문 안에 있는 yield에 의해 코드가 멈추기 때문에,

while을 무한 루프로 두고 원할 때까지 진행을 이어나갈 수 있게 한다.

 

(range()는 이터레이터면서 이터러블 하므로 for..of 를 사용할 수 있다.)

 

코드에서 yield가 나타나면 실행이 일시정지되고, 바깥에 값을 건네준다.

그리고 for..of에 의해서 내부적인 next()가 호출되면 함수 실행을 이어나간다.

 

기존의 이터러블일 경우, next()를 정의하고, 안에다가는 리턴값을 { value: ~~ } 이런 식으로 일일히 정의해줘야 하는데

제너레이터는 yield 하나로 끝낼 수 있다.

 

 

좀 더 압축된 방식

 

let range = {
    from: 1,
    to: 5,

    *[Symbol.iterator]() {  // [Symbol.iterator]: function*() 함수를 짧게 줄임
        for(let value = this.from; value <= this.to; value++) {
            yield value;
        }
    }
};

console.log([...range]);    // 1, 2, 3, 4, 5

range[Symbol.iterator]()는 제너레이터를 반환하고,

제너레이터 메서드는 for..of가 동작하는데 필요한 사항을 충족시키므로 잘 동작한다.

 

필요한 사항

1. .next() 메서드가 있음

2. 반환값의 형태는 { value: ..., done: true/false } 여야 함

 

 

 

 

 

 

제너레이터 사용법

 

 

함수 표현식, 메서드, 클래스 메서드 등으로도 사용할 수 있다.

 

// 제너레이터 함수 선언식
function* genDecFunc() {
    yield 1;
}
let generatorObj1 = genDecFunc();

// 제너레이터 함수 표현식
const genExpFunc = function* () {
    yield 1;
};
let generatorObj2 = genExpFunc();

// 제너레이터 메서드 식
const obj = {
    * generatorObjMethod() {
        yield 1;
    }
};
let generatorObj3 = obj.generatorObjMethod();

// 제너레이터 클래스 메서드 식
class MyClass {
    * generatorClassMethod() {
        yield 1;
    }
}
const newClass = new MyClass();
let generatorObj4 = newClass.generatorClassMethod();

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Q. 자바스크립트 초기에는 *를 안 붙여도 return 대신 yield를 쓰면 제너레이터가 됐다고 한다. 최종적으로 *를 붙이기로 결정한 이유는?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

기능적으로만 봤을 때,

function 뒤에 *를 붙이면 해당 함수는 이터레이터 인스턴스를 반환한다.

 

* 여부에 따라서 함수의 return type이 바뀌게 되는 셈.

 

사실 * 없이 yield로 구분하는 것도 크게 상관은 없다.

하지만 그렇게 되면 함수가 코드에 등장할 때마다 함수 내부 코드를 파싱해서 yield를 썼는지 체크하고

썼다면 이터레이터 반환, 안 썼다면 기존의 return value를 반환하게 만들어야 한다.

 

코드를 해석하고 돌려줄 인터프리터 레벨에서도 상당히 번거롭지만, 코드 가독성도 떨어진다.

프로그래머가 특정 함수의 return type이 이터레이터인지 아닌지 알기 위해서는 내부 코드를 모두 읽어봐야 하기 때문.

 

function 뒤에 *를 붙이는 것만으로 위의 문제는 해결된다. (이 내용은 async에도 똑같이 적용된다)

 

 

 

 

yield / next

 

yield는 제너레이터 함수의 실행을 일시적으로 정지시키며, yield 뒤에 오는 표현식은 제너레이터의 caller에게 반환된다.

 

next 메서드는 { value, done } 프로퍼티를 갖는 이터레이터 객체를 반환한다.

 

즉, value 프로퍼티는 yield 문이 반환한 값이고

done 프로퍼티는 제너레이터 함수 내의 모든 yield 문이 실행되었는지를 나타내는 boolean 타입의 값이다.

 

마지막 yield 문까지 실행된 상태에서 next 메서드를 호출하면 done 프로퍼티 값은 true가 된다.

 

function* gen(){
    // ... 실행 코드
    yield 1;    // 첫 번째 호출 시 이 지점까지 실행된다.
    // ... 실행 코드
    yield 2;    // 두 번째 호출 시 이 지점까지 실행된다.

    return 3;
}

// 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다.
// 제너레이터 객체는 이터러블이며 동시에 이터레이터다.
// 따라서 Symbol.iterator 메서드로 이터레이터를 별도 생성할 필요가 없다.
let iter = gen();

// 실행 결과가 자기 자신인 Symbol.iterator를 가지고 있다.
console.log(iter[Symbol.iterator]() == iter)    // true

// value, done 이 있는 객체를 반환하는 next를 호출하면 이터러블 객체를 반환하고 함수는 일시 중단된다.
console.log(iter.next());   // { "value": 1, "done": false } + 함수 실행 중단
console.log(iter.next());   // { "value": 2, "done": false } + 함수 실행 중단

console.log(iter.next());   // { "value": 3, "done": true } + 순회 종료

 

- 제너레이터 함수를 호출하면 제너레이터 객체를 반환한다. 그런데 제너레이터 객체는 이터러블이며 동시에 이터레이터다.

 

Symbol.iterator를 호출하는 과정 없이 이터레이터로 인식되므로,

Symbol.iterator 메서드로 이터레이터를 별도 생성할 필요가 없다.

 

// 실행 결과가 자기 자신인 Symbol.iterator를 가지고 있다.
console.log(iter[Symbol.iterator]() == iter)    // true

 

- 제너레이터에서 yield를 통해 몇 번의 next 값을 꺼낼 수 있게 할지 정할 수 있다.

 

- next() 함수가 실행되면 yield 순서대로 실행되고 일시 중단된다.

 

- 제너레이터의 실행 결과가 이터레이터이기 때문에 for..of 역시 사용 가능하다. 단 next()를 통한 순회가 끝난 이후에는 for..of 를 사용할 수 없다. 그리고 next()와는 달리 for..of 에서는 return 값이 반환되지 않는다.

 

 

순회 가능. return 값 반환 X

 

 

 

순회가 끝났으므로 for..of 동작 X

 

 

 

 

yield* (제너레이터 컴포지션)

 

다음과 같은 제너레이터 함수를 만들어보자.

 

- 숫자 0-9, 알파벳 대문자 A-Z, 알파벳 소문자 a-z를 생성한다. (문자 코드: 48-57, 65-90, 97-122)

일반적인 함수로 구현하려면 여러 개의 함수를 만들고 그 호출 결과를 어딘가에 저장해둔 후, 다시 그 결과들을 조합해야 원하는 기능을 구현할 수 있다.

 

하지만 제너레이터의 특수 문법 yield*를 사용하면 제너레이터를 다른 제너레이터에 끼워 넣을 수 있다.

 

(일반적인 방법)

 

function* generateAlphaNum() {

    for (let i = 48; i <= 57; i++) yield i;    // 0-9
    
    for (let i = 65; i <= 90; i++) yield i;    // A-Z

    for (let i = 97; i <= 122; i++) yield i;    // a-z
    
}

let str = '';
for (let code of generateAlphaNum()) {
    str += String.fromCharCode(code);
}

console.log(str);   // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

 

(yield* 를 사용한 방법)

 

function* generateSequence(start, end) {
    for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {
    
    // 제너레이터 함수를 실행할 땐 보통 let a = generateSequence(48, 57);
    // 이런 식으로 변수에 넣은 뒤에 a.next()를 통해 순회한다.
    // 하지만 yield* 를 이용하면 바로 순회가 가능하다.
    // 이는 마치 비동기 파트에서 Promise().then() 대신 await Promise()로 쓰는 것과 같다.
    
    yield* generateSequence(48, 57);
    yield* generateSequence(65, 90);
    yield* generateSequence(97, 122);
}

let str = '';
for (let code of generateAlphaNum()) {
    str += String.fromCharCode(code);
}

console.log(str);   // 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

 

 

yield* 지시자는 자신의 실행을 다른 제너레이터에 넘겨준다(위임한다).

 

여기서 말하는 '위임'은 yield* generateSequence가 제너레이터 generateSequence을 대상으로 반복을 수행하고,

산출 값을 바깥으로 전달한다는 것을 의미한다. 마치 외부 제너레이터에 의해 값이 산출되는 것과 같다. 

 

 

 

+ 배열 풀어서 순회하기

 

function* innerGenerator() {
    yield* ['a', 'b', 'c'];
}

function* generator() {

    // 그냥 yield 하면 배열 자체를 반환한다.
    yield [1, 2, 3];    

    // yield*는 받은 값이 이터러블일 경우 순회한다. 즉 배열을 풀어서 순회한다.
    yield* [4, 5, 6];
    
    yield* innerGenerator();
}

[...generator()];
// [[1, 2, 3], 4, 5, 6, 'a', 'b', 'c']

 

 

 

 

'yield'를 사용해 제너레이터 안/밖으로 정보 교환하기

 

yield는 결과를 바깥으로 전달할 뿐만 아니라, 값을 제너레이터 안으로 전달하기까지 한다.

값을 안, 밖으로 전달하려면 generator.next(arg)를 호출해야 한다. 이때 인수 argyield의 결과가 된다.

 

 

 

 

gen 안에서 yield는 "2 + 2 = ?" 라는 value를 내보내고,

next(arg)를 통해 인자값을 받는다.

 

1. 처음 호출할 땐 next()에 인수가 없어야 한다. 인수가 넘어가더라도 무시된다. generator.next()를 호출하면 실행이 시작되고 첫 번째 yield "2 + 2 = ?" 의 결과값이 반환된다. 이 시점에서 제너레이터는 (*) 로 표시한 줄에서 실행을 잠시 멈춘다.

 

2. 그 후 yield의 결과가 question에 할당된다.

 

3. generator.next(4)를 통해 제너레이터가 다시 시작되고 4result에 할당된다. (let result = 4)

 

 

 

... 전개연산자 제너레이터

 

spread 문법은 이터레이터 객체에 한해서 작동한다.

제너레이터는 이터러블이면서 이터레이터이기 때문에 이 역시 적용 가능하다.

 

전개연산자를 이용하면 굳이 yield 문을 변수에 담아서 next()를 반복문으로 돌릴 필요가 없고,

요소값들이 바로바로 펼쳐져 순회, 나열된다.

 

function* generateName() {
    yield 'J';
    yield 'O';
    yield 'N';
    yield 'G';
    yield 'O';
    yield 'N';
}

// for..of
const genForForOf = generateName();
for (let i of genForForOf) {
    console.log(i);
}

// 'J'
// 'O'
// 'N'
// 'G'
// 'O'
// 'N'

const genForSpread = generateName();
console.log([...genForSpread]); // ['J', 'O', 'N', 'G', 'O', 'N']

 

 

제너레이터 종료

 

제너레이터는 next 외에도 throw, return 등의 메서드가 있는데 이 두 가지를 통해 제너레이터를 종료할 수 있다.

다만, 이 둘은 차이가 있다.

 

[ return ]

 

function* increment() {
    let i = 0;

    try {
        while (true) {
            yield i++;
        }
    } catch (e) {
        console.log('[ERROR]', e);
    }
}

const withReturn = increment();

console.log(withReturn.next()); // { value: 0, done: false } : i++ 라서 0부터 찍힌다.
console.log(withReturn.next()); // { value: 1, done: false }
console.log(withReturn.next()); // { value: 2, done: false }
console.log(withReturn.next()); // { value: 3, done: false }

console.log(withReturn.return(42)); // { value: 42, done: true }

 

[ throw ]

 

const withThrow = increment();

console.log(withThrow.next()); // { value: 0, done: false } 
console.log(withThrow.next()); // { value: 1, done: false }
console.log(withThrow.next()); // { value: 2, done: false }
console.log(withThrow.next()); // { value: 3, done: false }

console.log(withThrow.throw(-1)); // Uncaught -1

 

throw가 호출되고 나면, catch 블록에 throw의 인자가 전달된다.

 

 

 

 

 

 

 

 

 

참조

 

0. 모던 자바스크립트 튜토리얼

 

제너레이터

 

ko.javascript.info

 

1. Inpa Dev

 

[JS] 📚 이터러블 & 이터레이터 - 💯완벽 이해

이터러블(interable) 이터러블(interable)이란 자료를 반복할 수 있는 객체를 말하는 것이다. 우리가 흔히 쓰는 배열 역시 이터러블 객체이다. 그럼 만일 이 배열에게 이터러블 표식을 없애버리면 어

inpa.tistory.com

 

 

2. Youtube Channel - Taehoon

 

 

 

'부트캠프 > 자바스크립트 완벽 가이드' 카테고리의 다른 글

11장(1). 자바스크립트 표준 라이브러리 (Set, Map, 정규 표현식)  (0) 2023.02.20
10장. 모듈  (0) 2023.02.13
9장. 클래스  (0) 2023.02.06
8장. 함수  (0) 2023.01.31
7장 배열  (0) 2023.01.26