JavaScript/함수형 프로그래밍과 JavaScript ES6+

사용자 정의 이터러블, 이터러블/이터레이터 프로토콜 정의 & 전개 연산자

느리지만 꾸준하게 2021. 8. 17. 18:02

내장 이터러블이라고 할 수 있는 Arr와 Set과 Map이 이터러블 이터레이터 프로토콜을 따르고 있는 for of문과 어떻게 함께 동작해서 순회가 이루어지는지에 대해서 살펴보았다.

 

사용자 정의 이터러블을 구현하면서 이터러블에 대해서 더 정확하게 알아보자.

iterable이라는 값을 정의한다. Symbol iterator메소드를 구현하고 있다. 그리고 iterable 가지고 있는 Symbol iterator 메소드는 이터레이터를 반환하는데 iterator는 next를 메소드로 가지고 있고 value와 done을 가지고 있는 객체를 리턴한다.

 

여기서 만들려고 하는 iterable은 iterator를 실행했을 때 (for of문을 순회했을 때) value로 3 2 1 리턴해주고 끝나는 그러한 이터러블을 만들려고 한다. i를 3이라고 정의해주고 value를 계속해서 줄여준다.  i가 0이 된다면 그때 부터는 done을 true로 리턴하도록 해준다.

 

iterable은 Symbol.iterator을 통해서 iterator를  반환할 수 있다. iterator는 next를 통해서 내부의 값을 조회할 수 있다.

const iterable = {
        [Symbol.iterator]() {
            let i = 3;
            return {
                next() {
                    return i == 0 ? { done: true } : { value: i--, done: false }
                }
            }
        }
    };
    let iterator = iterable[Symbol.iterator]();
 log(iterator.next());
 log(iterator.next());
 log(iterator.next());
 log(iterator.next());

=> 

{value: 3, done: false}
{value: 2, done: false}
{value: 1, done: false}
{done: true}

그렇다면 iterable을 순회할 수 있다는 것이다. 즉 iterator에 symbol iterator가 구현되어 있기 때문에 for of문에 들어갈 수 있는 것이다. 안에서  symbol iterator를 실행했을 때 객체가 리턴되는 것이다.(let iterator값과 같다.)

내부적으로 next를 실행하면서 a에 value를 하나씩 담게되고 모든 값을 순회하게 된다. 

const iterable = {
        [Symbol.iterator]() {
            let i = 3;
            return {
                next() {
                    return i == 0 ? { done: true } : { value: i--, done: false }
                }
            }
        }
    };
let iterator = iterable[Symbol.iterator]();
// log(iterator.next());
// log(iterator.next());
// log(iterator.next());
// log(iterator.next());
for (const a of iterable) log(a);
    
=>

3
2
1

array를 통해서 다시 살펴보자. array를 선언하고 arr를 다시 순회하도록 한다.

아래코드와 같이 iter2를 넣고 일부 진행했을 때 진행한 이후에 값들로만 순회를 할 수 있다. 즉 잘구현된 이터러블은 이터레이터를 만들었을 때 이터레이터를 진행하다가 순회를 할 수 있고 이터레이터를 for of문에 넣었을 때 그대로 모든 값들을 순회할 수 있도록 되어있다.

    const iterable = {
        [Symbol.iterator]() {
            let i = 3;
            return {
                next() {
                    return i == 0 ? { done: true } : { value: i--, done: false }
                }
            }
        }
    };
    let iterator = iterable[Symbol.iterator]();
    // log(iterator.next());
    // log(iterator.next());
    // log(iterator.next());
    // log(iterator.next());
    // for (const a of iterable) log(a);

    const arr2 = [1, 2, 3];
    let iter2 = arr2[Symbol.iterator]();
    iter2.next();
    // log(iter2[Symbol.iterator])
    for (const a of iter2) log(a);
    
    =>
    
    2
    3

iter2역시 Symbol.iterator를 가지고 있다. Symbol iterator를 실행한 값은 자기자신이다. 이렇게 iterator가 자기자신을 반환하는 심볼 이터레이터들을 가지고 있을 때 well-formed iterator라고 할 수 있다.

    const arr2 = [1, 2, 3];
    let iter2 = arr2[Symbol.iterator]();
    // iter2.next();
    log(iter2[Symbol.iterator]() == iter2);
    for (const a of iter2) log(a);
    
    
    =>
    
    true
    1
    2
    3

사용하고 있는 사용자 정의 iterable이 well-formed iterable을 반환할 수 있도록 하기 위해서는 심볼 이터레이터를 실행했을 때 반환한 이터레이터가 자기자신 또한 이터러블이면서 심볼 이터레이터를 실행했을 때 자기자신을 리턴하도록 해서 중간에 다시한번 for of문에 들어간다거나 어디에서든 Symbol.iterator로 만들었을 때 이전까지 진행되어 있던 자기의 상태에서 계속해서 next를 할 수 있도록 만들어 둔 것이 well-formed iterator이다.

<script>
    const iterable = {
        [Symbol.iterator]() {
            let i = 3;
            return {
                next() {
                    return i == 0 ? { done: true } : { value: i--, done: false }
                },
                [Symbol.iterator]() { return this; }
            }
        }
    };
    let iterator = iterable[Symbol.iterator]();

well-formed iterator가 아닐때는 이대로 실행했을 때 이터레이터가 이터러블이 아니라고 에러가 나게 된다.

<script>
    const iterable = {
        [Symbol.iterator]() {
            let i = 3;
            return {
                next() {
                    return i == 0 ? { done: true } : { value: i--, done: false }
                },
                // [Symbol.iterator]() { return this; }
            }
        }
    };
    
    let iterator = iterable[Symbol.iterator]();
    // log(iterator.next());
    // log(iterator.next());
    // log(iterator.next());
    // log(iterator.next());
    for (const a of iterable) log(a);

즉 이터레이터도 이터러블이 되도록 만들면 이터레이터를 반환한 값을 넣어주게 되는 것이다.

이터러블을 넣어서 순회를 해도 순회가 되고(for const a of iterable) iterable을 iterator로 만든 상태에서 순회를 해도 순회가 된다. 

<script>
    const iterable = {
        [Symbol.iterator]() {
            let i = 3;
            return {
                next() {
                    return i == 0 ? { done: true } : { value: i--, done: false }
                },
                [Symbol.iterator]() { return this; }
            }
        }
    };
    let iterator = iterable[Symbol.iterator]();
    // log(iterator.next());
    // log(iterator.next());
    // log(iterator.next());
    // log(iterator.next());
    for (const a of iterable) log(a);
    
    =>
    
    3
    2
    1

그리고 일정부분 이터레이터를 진행한 후에 순회가 되도록 하는것이 well-formed iterator이다.

    let iterator = iterable[Symbol.iterator]();
    iterator.next();
    // log(iterator.next());
    // log(iterator.next());
    // log(iterator.next());
    for (const a of iterator) log(a);
    
    =>
    
	2
	1

이터러블 이터레이터 프로토콜은 es6에서 지원하는 내장값만 이 프로토콜을 따르는 것이 아니다. 이미 많은 오픈소스 라이브러리들이나 자바스크립트에서 순회가 가능한 형태의값을 대부분 이터레이터 이터러블 프로토콜을 따르기 시작했다.

 

예를 들어서 페이스북에서 만든 immutable.js같은 경우에도 for of문을 통해서 순회할 수 있도록 심볼 이터레이터가 구현되어 있다.

 

이렇게 오픈소스만 이터러블 이터레이터 프로토콜을 따르는 것이 아니라, 자바스크립트가 사용되고 있는 환경인 브라우저에서 사용할 수 있는 웹APIs에 있는 구현되어있는 많은 값들 브라우저에서 사용할 수 있는 DOM과 관련된 값들 이라던지 여러가지가 이터러블 이터레이터 프로토콜을 따르고 있다.

 

예를 들면 쿼리셀렉트를 통해서 엘리먼트들을 조회를 한 상태에서 아래코드처럼 순회를 할 수 있다.

for (const a of document.querySelectorAll('*')) log(a);

이렇게 순회를 할 수 있는 이유는 all이라는 값이 배열이여서가 아니라 Symbol.iterator가 구현이 되어 있기 때문이다.

    for (const a of document.querySelectorAll('*')) log(a);
    const all = document.querySelectorAll('*');
    log(all[Symbol.iterator]);

그리고 심볼 이터레이터를 실행했을 때 이터레이터를 만든다.

let iter3 = all[Symbol.iterator]();

=>

Array Iterator {}

 

그리고 아래코드와 같이 안에 있는 값들을 리턴해 줄 수 있다. 그래서 순회를 할 수가 있다.

결론

자바스크립트에서 새롭게 바뀐 순회 이터러블 이터레이터 프로토콜은 굉장히 중요하다.

    for (const a of document.querySelectorAll('*')) log(a);
    const all = document.querySelectorAll('*');
    let iter3 = all[Symbol.iterator]();
    log(iter3.next());
    log(iter3.next());
    log(iter3.next());
    
    =>
    
	Array Iterator {}
	{value: html, done: false}
	{value: head, done: false}
	{value: script, done: false}

 

 

전개 연산자

전개 연산자도 마찬가지로 이터러블 이터레이터 프로토콜을 따르는데 아래와 같이 array를 선언했을 때 아래코드가 차례차례 실행이 된다.

 

하나의 array가 되는 값이 나온다.

<script>
    console.clear();
    const a = [1, 2];
    log([...a, ...[3, 4]]);
</script>

=>

[1, 2, 3, 4]

만약에 아래와 같이 만든다면 이터러블이 아니다라고 에러가 나오게 된다. 그래서 전개 연산자 역시 이터러블 프로토콜을 따르고 있는 값들을 펼칠 수 있는 것이다.

<script>
    console.clear();
    const a = [1, 2];
    a[Symbol.iterator] = null;
    log([...a, ...[3, 4]]);
</script>

=>

// index.html:101 Uncaught TypeError: a is not iterable
// at index.html:101

위에서 사용했던 array 역시 전개연산자로 사용할 수 있고 set map역시 모두 사용할 수 있다.

<script>
    console.clear();
    const a = [1, 2];
    // a[Symbol.iterator] = null;
    // log([...a, ...[3, 4]]);
    log([...a, ...arr, ...set, ...map]);
</script>

=>

[1, 2, 1, 2, 3, 1, 2, 3, Array(2), Array(2), Array(2)]
0: 1
1: 2
2: 1
3: 2
4: 3
5: 1
6: 2
7: 3
8: (2) ["a", 1]
9: (2) ["b", 2]
10: (2) ["c", 3]
length: 11
[[Prototype]]: Array(0)

map 같은 경우에는 values, keys메소드를 할당해서 나타나게 할 수도 있다.

 

결론은 이터러블 프로토콜은 굉장히 중요하니까 정확히 익히고 이터러블에 대한 추상을 다루면 자바스크립트에서 이 함수들을 보다 잘 사용한 함수들을 만들 수 있다.

log([...a, ...arr, ...set, ...map.values()]);
log([...a, ...arr, ...set, ...map.keys()]);


=>

[1, 2, 1, 2, 3, 1, 2, 3, 1, 2, 3]
[1, 2, 1, 2, 3, 1, 2, 3, "a", "b", "c"]

 

 

 

<출처 : 유인동 함수형 프로그래밍과 JavaScript ES6+>

https://www.inflearn.com/course/functional-es6/dashboard

 

함수형 프로그래밍과 JavaScript ES6+ - 인프런 | 강의

ES6+와 함수형 프로그래밍을 배울 수 있는 강의입니다. 이 강좌에서는 ES6+의 이터러블/이터레이터/제너레이터 프로토콜을 상세히 다루고 응용합니다. 이터러블을 기반으로한 함수형 프로그래밍,

www.inflearn.com