[모던 JavaScript 튜토리얼] 25. iterable 객체

3 분 소요

본문 바로가기


반복 가능한(iterable, 이터러블) 객체는 배열을 일반화한 객체다. iterable 개념을 이용하면 어떤 객체든 for...of 반복문을 적용할 수 있다.

배열은 대표적인 iterable 객체이다. 배열 뿐만 아니라, 여러 내장 객체는 반복 가능하다. 배열이 아닌 객체가 컬렉션을 의미하는 경우, for…of 문법을 적용해 컬렉션을 순회할 수 있게 된다.


Symbol.iterator

직접 iterable 객체를 만들어보자. for…of 를 적용하기 적합한 배열이 아닌 객체를 만들려고 한다.


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

for (let num of range) {
    console.log(num); // TypeError: range is not iterable
}

위 객체의 fromto까지의 숫자를 출력하려고 한다. range는 반복 가능한 객체가 아니기에, for...of 구문을 사용하려고 하면 위와 같이 iterable 하지 않다는 에러 문구가 출력된다.


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

range[Symbol.iterator] = function () {
    return {
        current: this.from,
        last: this.to,
        next() {
            if (this.current <= this.last) {
                return { done: false, value: this.current++ };
            } else {
                return { done: true };
            }
        },
    };
};

for (let num of range) {
    console.log(num); // 1 2 3 4 5
}
// console.log([...range]);

반복 가능한 객체로 만들기 위해서는 Symbol.iterator 라는 특수 메서드를 설정해주어야 한다. 이 심볼을 추가해주면 아래와 같이 작동된다.

  1. for...of 가 시작되자 마자, for…of 는 Symbol.iterator 를 호출한다.
  2. 이후 for…of 는 반환된 이터레이터 객체를 대상으로 동작하게 된다.
  3. for…of 에 다음 값이 필요하면, for…of는 이터레이터의 next() 메서드를 호출한다.
  4. next() 의 반환 값은 { done: Boolean, value: any } 형식의 객체여야 한다. done=true반복이 종료됐음을 의미한다. done=false 일 땐 value 에 다음 값이 저장된다.

위와 같이 객체 내에 next() 메서드를 작성하지 않고, 따로 분리하는 디자인 패턴을 관심사의 분리(Separation of Conecrn, SoC) 라고 한다. 객체 내의 작성 방법은 아래와 같다. 아래와 같이 작성하면 코드가 더 간결해진다.


let range = {
    from: 1,
    to: 5,
    [Symbol.iterator]() {
        this.current = this.from;
        return this;
    },
    next() {
        if (this.current <= this.to) {
            return { done: false, value: this.current++ };
        } else {
            return { done: true };
        }
    },
};

for (let num of range) {
    console.log(num); // 1 2 3 4 5
}
// console.log([...range]);


이터레이터를 명시적으로 호출하기

let str = "Hello";

let iterator = str[Symbol.iterator]();

while (true) {
    let ch = iterator.next();
    if (ch.done) break;

    // { value: 'H', done: false }
    // { value: 'e', done: false }
    // { value: 'l', done: false }
    // { value: 'l', done: false }
    // { value: 'o', done: false }
    console.log(ch);
}

이터레이터를 만든 뒤, 값을 수동으로 가져올 수 있다. 이 때, next() 메서드의 호출 결과는 위에서 봤듯 valuedone 을 프로퍼티로 갖는 객체임을 유의하자.


이터러블과 유사 배열

  • 이터러블 : Symbol.iterator 메서드가 구현된 객체이다.
  • 유사 배열 : 인덱스length 프로퍼티가 있어 배열처럼 보이는 객체이다.


let arrayLike = {
    0: "Hello",
    1: "World",
    length: 2,
};

for (let item of arrayLike) {
    console.log(item); // TypeError: arrayLike is not iterable
}

위의 arrayLike인덱스length 프로퍼티가 존재하므로 유사 객체이지만, Symbol.iterator 가 정의되어 있지 않기 때문에 for...of 구문 사용이 불가능하다.


let arrayLike = {
    0: "Hello",
    1: "World",
    length: 2,
};

arrayLike.push("!!!"); // TypeError: arrayLike.push is not a function

또한, 이터러블과 유사 배열은 배열이 아니기 때문에 push, pop 등의 배열 메서드를 지원하지 않는다.


Array.from

let arrayLike = {
    0: "Hello",
    1: "World",
    length: 2,
};

let arr = Array.from(arrayLike);
arr.push("!!!");

console.log(arr); // [ "Hello", "World", "!!!" ]
for (let el of arr) console.log(el); // "Hello", "World", "!!!"

Array.from이터러블이나 유사 배열을 매개변수로 받아, 진짜 Array 를 만들어 준다. 위 메서드를 이용해 진짜 배열을 만들면, 해당 배열 메서드를 이용할 수 있게 된다. 또한, arr 은 배열이므로 for...of 구문도 사용 가능해진다.


let range = [1, 2, 3];
let arr = Array.from(range, (val) => val * val);

console.log(arr); // [ 1, 4, 9 ]

Array.from 의 두 번째 인수에 매핑 함수를 작성해주면, 모든 요소를 대상으로 매핑 함수를 거친 배열을 만들어 준다.

댓글남기기