Study JavaScript 0523 - 1hr iterable 객체

변승훈·2022년 5월 23일
0

Study JavaScript

목록 보기
19/43

iterable 객체

반복 가능한 객체를 의미하며 이는 배열을 일반화한 객체이다.
iterable 개념을 사용하면 어떤 객체에든 for..of 반복문을 적용할 수 있다.

대표적인 iterable은 배열과 문자열이며 이 외에도 다수의 내장 객체가 반복이 가능하다.

1. Symbol.iterator

직접 iterable객체를 만들어 보자.
for..of를 적용하기에 적합한 객체를 만들어 예를 들어 보겠다.

let range = {
  from: 1,
  to: 5
};
// 아래와 같이 for..of가 동작할 수 있게 하는 것이 목표이다.
// for(let num of range) ... num=1,2,3,4,5

위의 range를 iterable로 만들려면 객체에 Symbol.iterator(특수 내장 심볼)라는 메소드를 추가하여 아래와 같은 일이 벌어지게 해야한다.

  1. for..of가 시작되자마자 for..ofSymbol.iterator를 호출한다.(Symbol.iterator가 없으면 에러가 발생).
    Symbol.iterator는 반드시 이터레이터(iterator, next()메소드가 있는 객체) 를 반환해야 한다.
  2. 이후 for..of는 반환된 객체(iterator)만을 대상으로 동작한다.
  3. for..of에 다음 값이 필요하면, for..of는 이터레이터의 next() 메소드를 호출한다.
  4. next()의 반환 값은 {done: Boolean, value: any}와 같은 형태이어야 한다. done=true는 반복이 종료되었음을 의미하며, done=false일땐 value에 다음 값이 저장된다.

range를 반복 가능한 객체로 만들어주는 코드는 아래와 같다.

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

// 1. for..of 최초 호출 시, Symbol.iterator가 호출
range[Symbol.iterator] = function() {

  // Symbol.iterator는 이터레이터 객체를 반환
  // 2. 이후 for..of는 반환된 이터레이터 객체만을 대상으로 동작하는데, 이때 다음 값도 정해진다.
  return {
    current: this.from,
    last: this.to,

    // 3. for..of 반복문에 의해 반복마다 next()가 호출
    next() {
      // 4. next()는 값을 객체 {done:.., value :...}형태로 반환해야 한다.
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// 이제 의도한 대로 동작하게 된다.
for (let num of range) {
  console.log(num); // 1, then 2, 3, 4, 5
}

iterable 객체의 핵심은 "관심사의 분리(Seperation of concern, SoC)"에 있다.

  • rangenext()메소드가 없다.
  • 대신range[Symbol.iterator]()를 호출해서 만든 "iterator" 객체와 이 객체의 next() 메소드에서 반복에 사용될 값을 만들어 낸다.

이렇게 하면 iterator객체와 반복 대상인 객체를 분리할 수 있다.

iterator 객체와 반복 대상 객체를 합쳐서 range 자체를 iterator로 만들면 코드가 아래와 같이 더 간단해진다.

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, then 2, 3, 4, 5
}

이제 rangeSymbol.iterator가 객체 range 자체를 반환한다.
반환된 객체엔 필수인 next()메소드가 있고 this.current에 반복이 얼마나 진행되었는지를 나타내는 값도 저장되어 있다.

단점은 두 개의 for..of 반복문을 하나의 객체에 동시에 사용할 수 없다. iterator(객체 자신)가 하나뿐이어서 두 반복문이 반복 상태를 공유하기 때문인데, 동시에 두 개의 for..of를 사용하는 것은 비동기 처리에서도 흔한 케이스는 아니다.

2. 문자열

문자열은 iterable이며 배열과 같이 가장 광범위하게 쓰이는 내장 이터러블이다.
for..of는 문자열의 각 글자를 순회한다.

for (let char of "test") {
  // 글자 하나당 한 번 실행(4회 호출)
  console.log( char ); // t, e, s, t가 차례대로 출력됨
}

서로게이트 쌍(surrogate pair)에도 잘 동작한다.

let str = '𝒳😂';
for (let char of str) {
    console.log( char ); // 𝒳와 😂가 차례대로 출력됨
}

3. iterater를 명시적으로 호출하기

for..of를 사용했을 때와 동일한 방법으로 문자열을 순회하는데 직접 호출을 통해서 해보자.
다음 코드는 문자열 iterater를 만들고, 여기서 값을 '수동'으로 가져온다.

let str = "Hello";

// for..of를 사용한 것과 동일한 작업
// for (let char of str) alert(char);

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

while (true) {
  let result = iterator.next();
  if (result.done) break;
  console.log(result.value); // 글자가 하나씩 출력
}

iterator 명시적으로 호출하는 경우는 거의 없다. 하지만 위의 방법을 사용하면 for..of를 사용하는 것 보다 반복 과정을 더 잘 통제할 수 있다는 장점이 있다.
반복 과정을 여러개로 쪼개는 것이 가능하다.

4. iterable & Array-like

  • iterable 객체: Symbol.iterator메소드가 구현된 객체이다.
  • Array-like(유사 배열) 객체: indexlength 프로퍼티가 있어서 배열처럼 보이는 객체이다.

이터러블 객체라고 해서 유사 배열 객체는 아니고 유사 배열 객체라고 해서 이터러블 객체인 것도 아니다.

둘은 비슷해 보이지만 다르므로 잘 구분해서 이해를 하고 있어야 한다.

아래 예시의 객체는 Array-like(유사 배열) 객체이긴 하지만 iterable 객체가 아니다.

let arrayLike = { // 인덱스와 length프로퍼티가 있음 => 유사 배열
  0: "Hello",
  1: "World",
  length: 2
};

// Symbol.iterator가 없으므로 에러 발생
for (let item of arrayLike) {}

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

5. Array.from

바로 위의 메소드를 지원하지 않는 문제를 해결하기 위해 범용 메소드인 Array.from을 통해 iterable이나 Array-like(유사 배열)을 "진짜 Array"를 만들어준다.
Array.from은 객체를 받아 iterable이나 Array-like(유사 배열)인지를 조사한다.
넘겨 받은 인수가 iterable이나 Array-like(유사 배열)인 경우, 새로운 배열을 만들고 객체의 모든 요소를 새롭게 만든 배열로 "복사"한다.

이 과정을 거치게 되면 iterable이나 Array-like(유사 배열)에서 배열 메소드를 사용할 수 있게 된다.

  • Array-like(유사 배열)에서 Array.from을 사용
let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

let arr = Array.from(arrayLike);
console.log(arr.pop()); // World
  • iterable에서 Array.from을 사용
let range = {
  from: 1,
  to: 5
};

let arr = Array.from(range);
console.log(arr); // 1,2,3,4,5

Array.from엔 '매핑(mapping)'함수를 선택적으로 넘겨줄 수 있다.

Array.from(obj[, mapFn, thisArg])

mapFn을 두 번째 인수로 넘겨주면 새로운 배열에 obj의 요소를 추가하기 전에 각 요소를 대상으로 mapFn을 적용할 수 있다.
새로운 배열엔 mapFn을 적용하고 반환된 값이 추가된다.
세 번째 인수 thisArg는 각 요소의 this를 지정할 수 있도록 해준다.

Array.from은 str.split과 달리, 문자열 자체가 가진 iterable 속성을 이용해 동작한다.

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

let arr = Array.from(range, num => num * num);

console.log(arr); // 1,4,9,16,25

Array.fromstr.split과 달리, 문자열 자체가 가진 이터러블 속성을 이용해 동작한다. 따라서 for..of처럼 서로게이트 쌍에도 제대로 적용된다.

위 예시는 기술적으로 아래 예시와 동일하게 동작한다고 보면 된다.

let str = '𝒳😂';

let chars = []; // Array.from 내부에선 아래와 동일한 반복문이 돌아간다.
for (let char of str) {
  chars.push(char);
}

console.log(chars);

Array.from을 사용하면 서로게이트 쌍을 처리할 수 있는 slice를 직접 구현할 수도 있다.

function slice(str, start, end) {
  return Array.from(str).slice(start, end).join('');
}

let str = '𝒳😂𩷶';

console.log( slice(str, 1, 3) ); // 😂𩷶

// 내장 순수 메서드는 서로게이트 쌍을 지원하지 않는다.
console.log( str.slice(1, 3) ); // 쓰레깃값 출력 (영역이 다른 특수 값)
profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글