안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

자바스크립트 (JavaScript)

[JavaScript] 반복 가능 객체 (Iterable object), 유사 배열 객체 (Array-like object)

피그브라더 2021. 5. 16. 23:10

많은 프로그래밍 언어에서 그렇듯, JavaScript에도 iterable 객체iterator 객체의 개념이 존재한다. 여기서 iterable 객체란 반복 가능한 객체(for ... of 등의 문법을 이용하여 각 요소를 반복할 수 있는 객체)를 의미하며, iterator 객체란 해당 iterable 객체에서 각 요소를 반복하기 위해 사용하는 객체를 의미한다. 일반적으로 [1, 2, 3]과 같이 사용하는 배열 객체가 대표적인 iterable 객체이다. 그리고 여기에 추가로 JavaScript는 유사 배열 객체(Array-like object)라는 개념도 정의하고 있는데, 이는 배열은 아니지만 배열과 유사한 객체를 의미한다. 그렇다면 iterable 객체, iterator 객체, 유사 배열 객체가 정확히 무엇인지 이번 포스팅에서 한 번 정리해보도록 하자.

 

1. Iterable 객체 (= 반복 가능 객체)

1-1. Iterable 객체의 조건

iterable 객체란 for ... of 등의 문법을 이용하여 각 요소를 반복할 수 있는 객체를 의미한다. 배열(Array) 객체는 내장 iterable 객체이기 때문에 for ... of 문법을 이용하여 각 요소를 반복할 수 있었던 것이다. 또한 String 객체, Map 객체, Set 객체도 내장 iterable 객체이다. 그렇다면 iterable 객체이기 위한 조건은 무엇일까? 다음과 같은 조건을 만족하면 배열이 아닌 일반적인 객체도 iterable 객체가 될 수 있다.

 

  • 이름이 Symbol.iterator인 메소드를 정의해야 한다(혹은 상위 프로토타입 체인에서 상속받아야 한다).
  • 해당 메소드는 인자가 없으며, iterator 객체를 반환해야 한다.

 

1-2. Iterator 객체의 조건

iterable 객체가 되기 위한 조건이 iterator 객체를 반환하는 메소드를 정의하는 것이라고 했다. 그렇다면 iterator 객체이기 위한 조건은 무엇일까? iterable 객체의 Symbol.iterator 메소드가 반환하는 객체는 다음과 같은 조건을 만족할 때 iterator 객체가 된다.

 

  • next() 메소드를 정의해야 한다(혹은 상위 프로토타입 체인에서 상속받아야 한다).
  • 해당 메소드는 인자가 없으며, done 프로퍼티와 value 프로퍼티를 가지는 객체를 반환해야 한다.
  • done 프로퍼티 : 반복이 끝났다면 true, 아니라면 false(혹은 done 프로퍼티를 정의하지 않음)이다.
  • value 프로퍼티 : 반복이 끝났다면 undefined(혹은 value 프로퍼티를 정의하지 않음), 아니라면 현재 위치의 요소 값이다.

 

결국, 특정 iterable 객체에 대하여 반복을 수행한다는 것은 그 iterable 객체의 Symbol.iterator 메소드를 호출하여 iterator 객체를 얻고, 그 iterator 객체의 next() 메소드를 호출하여 요소를 하나씩 꺼내는 것을 의미한다. 이러한 iterable 객체를 이용하는 대표적인 JavaScript 문법이 바로 for ... of 문법이다. 이 밖에도 iterable 객체를 이용하는 문법들이 몇 개 존재하는데, 이에 대해서는 아래에서 다루도록 한다.

 

참고로, iterable 객체 자신이 iterator 객체일 수도 있다. 다시 말해서, iterable 객체가 Symbol.iterator 메소드를 호출하여 얻는 iterator 객체가 곧 자기 자신일 수도 있다는 것이다. Object.entries() 메소드가 반환하는 iterable 객체가 대표적인 예시이다.

const arr = [1, 5, 7];
const arrEntries = arr.entries();  // Object.entries() method

arrEntries.toString();  // "[object Array Iterator]"
arrEntries === arrEntries[Symbol.iterator]();  // true

또한 제네레이터(Generator) 객체도 iterable 객체이면서 동시에 iterator 객체이다. 다음 예시를 참고하자.

const genObject = function* () {
    yield 1;
    yield 2;
    yield 3;
}();

typeof genObject[Symbol.iterator];  // "function" : 따라서 iterable 객체입니다.

typeof genObject.next;  // "function" : 따라서 iterator 객체입니다.

genObject[Symbol.iterator]() === genObject;  // 동일한 객체입니다.

 

1-3. Iterable 객체를 이용하는 JavaScript 문법

JavaScript에서 iterable 객체를 이용하는 JavaScript 문법들을 몇 가지 알아보자. 즉, 내부적으로 iterable 객체와 iterator 객체를 이용하여 구현되어 있는 문법들을 말하는 것이다. 가장 대표적인 예시는 앞에서 계속 말한 것처럼 for ... of 문법이다. 다음 코드는 for ... of 문법을 iterable 객체와 iterator 객체를 이용하여 구현해본 코드이다. 즉, (1)과 (2)는 사실상 거의 동일하다.

const arr = [2, 4, 6];

// (1)
for (const e of arr) alert(e);

// (2)
const iterator = arr[Symbol.iterator]();
while (true) {
    const data = iterator.next();
    if (data.done) break;
    else alert(data.value);
}

 

이외에도 iterable 객체를 이용하는 문법들이 몇 개 더 존재한다. 전개 연산자(Spread Operator), 제네레이터(Generator)의 yield*, 구조 분해 할당(Destructing Assignment) 등이 그 예시이다. 다음 예시 코드를 참고하자. 문자열(String 객체) 또한 iterable 객체라는 사실에 근거하여 작성된 예시 코드이다.

// (1) Spread Operator
[...'abc'];  // ['a', 'b', 'c']

// (2) Generator's yield*
function* gen(){
  yield* ['a', 'b', 'c'];
}
gen().next();  // { value: 'a', done: false }

// (3) Destructing Assignment
const [a, b, c] = 'abc';
a  // 'a'

 

1-4. Iterable, Iterator 객체 직접 정의해보기

원리를 가장 쉽게 이해하는 방법은 직접 한 번 구현해보는 것이다. 먼저, iterable 객체와 iterator 객체가 서로 다른 객체인 경우를 구현해보자. 1부터 5까지의 연속된 숫자를 담고 있음을 표현하는 range 객체를 정의하고, 이를 iterable 객체로 만들어 보자.

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

// Symbol.iterator 메소드 정의 (iterable 객체의 조건)
range[Symbol.iterator] = function () {
    // iterator 객체를 반환
    return {
        current: this.from,
        last: this.to,

        // next() 메소드 정의 (iterator 객체의 조건)
        next() {
            if (this.current <= this.last) {
                return { done: false, value: this.current++ };
            } else {
                return { done: true, value: undefined };
            }
        }
    };
};

// iterable 객체를 이용하는 for ... of 문법 테스트
for (const num of range) {
  alert(num);  // 1, then 2, 3, 4, 5
}

 

다음으로, iterable 객체와 iterator 객체가 같은 경우를 구현해보자. 위의 코드를 조금 변형시킨 것이다.

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

    // iterable 객체의 조건
    [Symbol.iterator]() {
        this.current = this.from;
        return this;
    },

    // iterator 객체의 조건
    next() {
        if (this.current <= this.to) {
            return { done: false, value: this.current++ };
        } else {
            return { done: true, value: undefined };
        }
    }
};

// iterable 객체를 이용하는 for ... of 문법 테스트
for (const num of range) {
  alert(num);  // 1, then 2, 3, 4, 5
}

그러나 위와 같은 경우는 하나의 iterable 객체에 대하여 두 개의 for ... of 문법을 동시에 사용할 수 없다는 단점이 있다. 한 객체에 저장되어 있는 상태 값을 공유하기 때문이다. 하지만 일반적으로 한 객체에 대하여 동시 반복을 수행하는 일은 흔하지 않다.

 

2. 유사 배열 객체 (= Array-like 객체)

JavaScript에서 유사 배열 객체(Array-like object)란, 배열과 같이 생겼지만 엄밀히 말하면 배열은 아닌 객체를 의미한다. 그렇다면 유사 배열 객체이기 위한 조건은 무엇일까? 다음과 같은 조건을 만족하면 일반적인 객체도 유사 배열 객체가 될 수 있다.

 

  • 숫자 값을 가지는 length 프로퍼티를 가지고 있어야 한다.
  • 숫자 값 기반의 인덱싱이 가능해야 한다.

 

length 프로퍼티를 가지고 숫자 값 기반의 인덱싱이 가능하면 마치 배열인 것처럼 사용할 수 있기 때문에 유사 배열 객체라고 하는 것이다. 그러나 엄밀히 말하면 Array 객체가 아니기 때문에, Array.map() 등의 Array 메소드를 사용할 수 없다는 특징이 있다.

 

문자열(String 객체)이 대표적인 유사 배열 객체이다. 또한, Document.querySelectorAll() 또는 Element.querySelectorAll() 메소드가 반환하는 NodeList 객체도 유사 배열 객체이다. 참고로 많은 경우에 내장 유사 배열 객체들은 iterable 객체이기도 하다. 하지만 유사 배열 객체라고 해서 iterable 객체라는 보장은 없고, iterable 객체라고 해서 유사 배열 객체라는 보장은 없다. 예를 들어, 다음은 유사 배열 객체이지만 iterable 객체가 아니어서 for ... of 문법을 사용할 수 없음을 보여주는 예시 코드이다.

// 유사 배열 객체 (length 프로퍼티가 있고 숫자 값 기반의 인덱싱이 가능)
const arrLikeObject = {
    0: 'IT',
    1: 'Eldorado',
    length: 2
};

// iterable 객체가 아니므로 에러 발생
for (const e of arrLikeObject) alert(e);

 

3. Array.from() 메소드

iterable 객체 혹은 유사 배열 객체를 진짜 배열로 변환시켜주는 메소드이다. 조금 더 정확하게는, 각 요소에 대해 얕은 복사를 진행하여 새로운 배열 객체를 생성하여 반환하는 메소드이다. 다음 예시를 참고하자.

const iterable1 = 'foo';
Array.from(iterable1);  // [ 'f', 'o', 'o' ]

const iterable2 = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(iterable2);  // [ 'foo', 'bar', 'baz' ]

const iterable3 = new Map([[1, 2], [2, 4], [4, 8]]);
Array.from(iterable3);  // [[1, 2], [2, 4], [4, 8]]

const mapper = new Map([['1', 'a'], ['2', 'b']]);

const iterable4 = mapper.keys();
Array.from(iterable4);  // ['1', '2'];

const iterable5 = mapper.values();
Array.from(iterable5);  // ['a', 'b'];

 

두 번째 인자에 맵핑 함수(Mapping Function)를 넘겨줄 수도 있다. 이는 새로 생성하는 배열 객체의 각 요소에 대해 실행된다.

const iterable1 = 'foo';
Array.from(iterable1, x => x + x);  // [ 'ff', 'oo', 'oo' ]

const iterable2 = new Set(['foo', 'bar', 'baz', 'foo']);
Array.from(iterable2, x => x[0]);  // [ 'f', 'b', 'b' ]

 

 

 

 

 

 

본 글은 아래 링크의 내용을 참고하여 학습한 내용을 나름대로 정리한 글임을 밝힙니다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from

https://ko.javascript.info/iterable