이터러블, 이와 관련된 문법

seeen·2023년 1월 11일
0
post-thumbnail

이 글은 '이웅모'님의 '모던 자바스크립트 Deep Dive' 책을 통해 공부한 내용을 정리한 글입니다. 저작권 보호를 위해 책의 내용은 요약되었습니다.

이터레이션 프로토콜(Iteration Protocol)

이터레이션 프로토콜에는 다음과 같이 두 가지의 프로토콜이 있다.

이터러블 프로토콜(Iterable Protocol)

Well-known Symbol인 Symbol.iterator를 프로퍼티 키로 사용한 메서드를 직접 구현하거나 프로토타입을 통해 상속 받은 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환하는데 이러한 규약을 이터러블 프로토콜이라 한다.

  • for of 문에서 순회 가능하며 Spread 문법의 대상, 디스트럭쳐링 할당 등으로도 가능하다.
// 일반 배열은 Symbol.iterator 메서드를 소유한다.
console.log(Symbol.iterator in [1,2,3]); // true

for (const el of [1,2,3]) {
  console.log(el); // 1 2 3
}

// 일반 객체는 Symbol.iterator 메서드를 소유하지 않는다.
console.log(Symbol.iterator in {a:1, b:2}); // false

for (const el of {a:1, b:2}) {
  console.log(el); // Uncaught TypeError
}

이터레이터 프로토콜(Iterator Protocol)

이터러블의 Symbol.iterator 메서드를 호출하면 이터레이터 프로토콜을 준수한 이터레이터를 반환하는데, 이는 next 메서드를 소유한다. next 메서드를 호출하면 이터러블을 순회하며 valuedone 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는데 이러한 규약을 이터레이터 프로토콜이라 한다.

// Symbol.iterator 메서드는 이터레이터를 반환
const iterator = [1,2,3][Symbol.iterator]();

console.log('next' in iterator); // true

// 이터레이터의 next 메서드를 호출하면 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

필요성

Array, String, Map, Set, TypedArray(Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array), DOM data structure(NodeList, HTMLCollection), Arguments

위 데이터 소스는 모두 이터레이션 프로토콜을 준수하는 이터러블이다. 이터러블은 데이터 공급자의 역할을 수행한다.

만약 각 데이터 소스마다 순회하는 방식이 모두 다르다면 데이터 소비자(for of문 등)는 다양한 순회 방식을 지원해야한다. 이는 효율적이지 않기에 데이터 소스가 이터레이션 프로토콜을 준수하도록 규정하고, 데이터 소비자는 이터레이션 프로토콜만 지원하도록 구현한다.

즉, 이터레이션 프로토콜은 데이터 공급자와 소비자 간을 연결하는 인터페이스 역할을 한다.

커스텀 이터러블

위 예제 코드에서 일반 객체는 이터러블 프로토콜을 준수하지 않아 for of문으로 순회가 불가능했다. 이처럼 이터러블 프로토콜을 준수하지 않는 소스들은 직접 이터레이션 프로토콜을 준수하도록 구현하면 이터러블이 된다.

const fibonacci = {
  // Symbol.iterator 메서드 구현함으로써 이터러블 프로토콜 준수
  [Symbol.iterator]() {
    let [pre, cur] = [0, 1];
    const max = 100;

    // Symbol.iterator 메서드는 next 메소드를 소유한 이터레이터를 반환    
    return {
      // next 메소드는 이터레이터 리절트 객체를 반환
      next() {
        [pre, cur] = [cur, pre + cur];
        return {
          value: cur,
          done: cur >= max
        };
      }
    };
  }
};

// done 프로퍼티가 true가 되면 반복 중지
for (const el of fibonacci) {
  console.log(el); // 1 2 3 5 ...
}

이터러블이면서 이터레이터

이터레이터는 이터러블의 Symbol.iterator 메서드를 호출함으로써 생성할 수 있다. 이터러블이면서 이터레이터인 객체를 생성하면 Symbol.iterator 메서드를 호출하지 않아도 된다.

const fibonacciFunc = function (max) {
  let [pre, cur] = [0, 1];

  // 이터러블이면서 이터레이터인 객체를 반환
  return {
    // Symbol.iterator 메서드
    [Symbol.iterator]() {
      return this;
    },
    // next 메서드
    next() {
      [pre, cur] = [cur, pre + cur];
      return {
        value: cur,
        done: cur >= max
      };
    }
  };
};

let iter = fibonacciFunc(10);

// iter 이터레이터
console.log(iter.next()); // {value: 1, done: false}
console.log(iter.next()); // {value: 2, done: false}
console.log(iter.next()); // {value: 3, done: false}
console.log(iter.next()); // {value: 5, done: false}

iter = fibonacciFunc(10);

// iter 이터러블
for (const num of iter) {
  console.log(num); // 1 2 3 5 8
}

fibonacciFunc 함수는 Symbol.iterator 메서드와 next 메서드를 소유한 이터러블이면서 이터레이터 객체를 반환한다. Symbol.iterator 메서드에서 this를 반환하므로 next 메서드를 갖는 이터레이터를 반환할 수 있다.

무한 이터러블과 지연 평가(Lazy Evaluation)

// fibonacciFunc 함수는 무한 이터러블을 생성
const fibonacciFunc = function () {
  let [pre, cur] = [0, 1];

  return {
    [Symbol.iterator]() {
      return this;
    },
    next() {
      [pre, cur] = [cur, pre + cur];
      // done 프로퍼티를 생략
      return { value: cur };
    }
  };
};

for (const num of fibonacciFunc()) {
  if (num > 10000) break;
  console.log(num); // 1 2 3 5 ...
}

// 무한 이터러블에서 3개만을 취득한다.
const [f1, f2, f3] = fibonacciFunc();
console.log(f1, f2, f3); // 1 2 3

위 예제 코드의 이터러블은 지연 평가를 통해 데이터를 생성한다. 지연 평가란 데이터가 필요한 시점에서 데이터를 생성하는 기법이다.

fibonacciFunc 함수는 무한 이터러블을 생성하는데, 데이터 소비자인 for of 문이나, 디스트럭처링 할당이 되기 전까지 데이터를 생성하지 않는다. for of 문에 경우는 이터러블을 순회할 때 내부 이터레이터의 next 메서드를 호출하면서 데이터를 생성한다.

즉, 데이터가 필요한 시점까지 데이터 생성을 지연시킨 후 데이터가 필요한 순간에 데이터를 생성한다.

스프레드 문법 (Spread Syntax)

스프레드 문법은 하나로 뭉쳐 있는 여러 값들의 집합을 전개하여 개별적인 값들의 목록으로 만든다.

스프레드 문법의 대상은 이터러블로 한정된다.

console.log(...[1,2,3,4]); // 1 2 3 4
console.log(...'hello'); // h e l l o

스프레드 문법의 결과물은 값으로 사용할 수 없고, 쉼표로 구분한 값의 목록을 사용하는 다음과 같은 문맥에서만 사용 가능하다.

  • 함수 호출문의 인수 목록
  • 배열 리터럴의 요소 목록
  • 객체 리터럴의 프로퍼티 목록
// 함수 호출문의 인수에서
console.log(Math.max(...[1,4,10,3,5])); // 10

// 배열 리터럴의 요소에서
console.log([...[1,2], ...[3,4,5]]); // [1,2,3,4,5]

// 배열 복사 slice 함수 대신 사용
const arr = [1,2,3];
const copyArr = [...arr];
console.log(copyArr, arr === copyArr); // [1,2,3] false

// 객체 리터럴의 프로퍼티에서
// 2021년 1월 TC39 에서 제안된 내용
console.log({a:1, b:2, ...{x:4, y:5}}); // {a: 1, b: 2, x: 4, y: 5}

Rest 파라미터와 스프레드 문법

Rest 파라미터는 함수에 전달된 인수들의(그리고 배열 또는 객체) 목록을 배열로 전달받기 위해 매개변수 앞에 ...을 삽입하는 것이다. 즉, Spread 문법과 Rest 파라미터는 반대의 성질을 띈다고 볼 수 있다.

function abc(...num) {
  console.log(num); // [1,2,3,4]
}

abc(...[1,2,3,4]);

디스트럭처링 할당

디스트럭처링 할당(구조분해할당)은 이터러블 또는 객체를 1개 이상의 변수에 개별적으로 할당하는 것을 말한다.

객체 디스트럭처링 할당 기준은 프로퍼티 키이다. 즉, 순서는 의미가 없으며 선언된 변수 이름과 프로퍼티 키가 일치할 경우 할당된다.

// 배열의 디스트럭처링 할당
const arr = [1,2,3];

const [a,b,c] = arr;

console.log(a,b,c); // 1 2 3


// 객체의 디스트럭처링 할당
const obj = { age : 111, name : 'sem' };

const { age, name } = obj;

console.log(age, name); // 111 'sem'

객체 디스트럭처링 할당은 객체에서 프로퍼티 키로 필요한 프로퍼티 값만 추출하여 변수에 할당하고 싶을 때 유용하다. 다음과 같이 래퍼 객체로부터 특정 프로퍼티만 추출하는 것 또한 가능하다.

const { length } = 'hello';
console.log(length); // 5
profile
woowacourse FE 5th, depromeet Web 15th

0개의 댓글