[JS-책 편김에 끝까지] 이터레이션

짱쫑·2023년 3월 12일
0

javascript

목록 보기
10/10
post-thumbnail

이터레이션 프로토콜 iteration protocol

이터레이션 프로토콜은 순회 가능한(iterable) 데이터 컬렉션(자료구조)을 만들기 위해 ECMAScript 사양에 정의하여 미리 약속한 규칙이다.

ES6 이전에는 배열, 문자열, 유사 배열 객체, DOM 컬렉션 등은 통일된 규약없이 각자 구조를 가지고 for문, for-in문, forEach메서드 등 많은 방법으로 순회할 수 있었지만 ES6에서는 이터레이션 프로토콜을 준수하는 이터러블로 통일하여 for-of문, 스프레드문법, 배열 디스트럭처링 할당의 대상으로 사용할 수 있도록 일원화 시켰다.

이터레이션 프로토콜에는 이터러블 프로토콜과 이터레이터 프로토콜이 있다.

  • 이터러블 프로토콜(iterable protocol)
    Symbol.iterator 메서드를 구현하거나 프로토타입 체인을 통해 상속한 객체이며 Symbol.iterator 메서드를 호출하면 이터레이터를 반환한다. 이터레이터는 이터러블 프로토콜을 준수한 객체를 말한다. 이터러블은 for-of문으로 순회할 수 있으며 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용할 수 있다.

    const array = [1, 2, 3];
    
    // 배열은 Array.prototype의 Symbol.iterator 메서드를 상속받는 이터러블이다.
    console.log(Symbol.iterator in array); // true
    
    // 이터러블인 배열은 for-of 문으로 순회 가능
    for (const item of array) {
    	console.log(item);
    }
    
    // 이터러블인 배열은 스프레드 문법의 대상으로 사용할 수 있다.
    console.log([...array]); // [1, 2, 3]
    
    // 이터러블인 배열은 배열 디스트럭처링 할당의 대상으로 사용할 수 있다.
    const [a, ...rest] = array;
    console.log(a, rest); // 1, [2, 3]

    Symbol.iterator 메서드를 직접 구현하지 않거나 상속받지 않은 일반 객체는 이터러블 프로토콜을 준수한 이터러블이 아니다. 그러므로 일반 객체는 for-of 문으로 순회할 수 없고 스프레드 문법과 배열 디스트럭처링 할당의 대상으로 사용하지 못한다.

    const obj = { a: 1, b: 2 };
    
    // 일반 객체는 Symbol.iterator 메서드를 구현하거나 상속받지 않는다.
    // 따라서 일반 객체는 이터러블 프로토콜을 준수한 이터러블이 아니다.
    console.log(Symbol.iterator in obj); // false
    
    // 이터러블이 아닌 일반 객체는 for-of 문으로 순회 불가능
    for (const item of obj) {
    	console.log(item); // TypeError: obj is not iterable
    }
    
    // 이터러블이 아닌 일반 객체는 배열 디스트럭처링 할당의 대상으로 사용 불가능
    const [a, b] = obj; // TypeError: obj is not iterable
  • 이터레이터 프로토콜(iterator protocol)
    이터레이터는 next 메서드를 소유하며 next메서드를 호출하면 이터러블을 순회하며 value, done 프로퍼티를 갖는 이터레이터 리절트 객체를 반환하는데 이터레이터 프로토콜을 준수한 이터레이터는 next 메서드를 갖는다. 이터레이터는 이터러블의 요소를 탐색하기 위한 포인터 역할을 한다.

    // 배열은 이터러블 프로토콜을 준수한 이터러블이다.
    const array = [1, 2, 3];
    
    // Symbol.iterator 메서드는 이터레이터를 반환한다.
    const iterator = array[Symbol.iterator]();
    
    // Symbol.iterator 메서드가 반환한 이터레이터는 next 메서드를 갖는다.
    console.log('next' in iterator); // true
    
    // 이터레이터의 next 메서드를 호출하면 value, done 프로퍼티를 갖는 이터레이터 리절트(iterator result) 객체를 반환한다.
    let iteratorResult = iterator.next();
    console.log(iteratorResult); // { value: 1, done: false }
    
    // 이터레이터의 next 메서드는 이터러블의 각 요소를 순회하기 위한 포인터 역할을 한다.
    // next 메서드를 호출할 때마다 이터러블을 순회하며 이터레이터 리절트 객체를 반환한다.
    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 }

    이터레이터의 next 메서드가 반환하는 이터레이터 리절트 객체의 value 프로퍼티는 현재 순회중인 이터러블의 값을 반환하고 done 프로퍼티는 이터러블의 순회 완료 여부를 반환한다.

  • Array라는 interface의 정의를 살펴보면
    interface Array {
    /* Iterator /
    Symbol.iterator: IterableIterator;
    ...
    }
    이터러블한이터레이션을 반환하는 것을 확인 할 수 있다.

또한 array.value(); 를 호출해보면

...
/* Returns an iterable of values in the array /
values(): IterableIterator;
...
}
이터러블한이터레이션을 반환하는 것을 확인 할 수 있다.

이외에도 keys와 entries 함수가 있다.

여기에서 Iterator를 반환하지 않아도 배열처럼 Symbol.iterator라는 함수를 가지고 있다면 for-of에서 바로 사용이 가능하다.


정리하자면 iteration은 말 그대로 반복.순회 라는 말이다. 이터레이션을 자바스크립트에서는 이터레이션(반복.순회) 프로토콜(규격.약속.인터페이스) - (순회가 가능하기 위해서 따라야 하는 규칙) 이라고 한다.
이터레이션 프로토콜의 기본 자바스크립트의 자료구조는 Array, String, Map, Set 등이 있다.

이 규격을 따른다는 것은 어떤 객체든지 순회가 가능하기 위해서는 1. iterable 프로토콜을 따라야 한다. 어떤 객체 안에서도 2.Symbol.iterator 메서드를 호출 했을 때 iterable 프로토콜을 반환만 하면 이 객체는 순회가 가능한 객체가 되는 것이다.

  • iterable: 순회 가능한, iterator: 순회하는, 반복자

이때 반환되는 iterable 프로토콜을 따른다는 것은, 객체를 리턴하면 되는 것인데 그 객체 안에서 3. next() 라는 함수를 정의하면 된다. 그리고 next를 호출할 때마다 다음값을 리턴하면 된다.

iterable을 조금 더 뜯어보면 Iterator 인터페이스 안에는 next뿐 아니라 return과 throw라는 함수가 더 있다. return과 throw는 옵셔널(옵션사항)이다.

return?(value?: [] | [TNext]): IteratorResult<T, TReturn>;
throw?(e?: any): IteratorResult<T, TReturn>;
  • return(value)
    0개 혹은 1개의 인수를 허용하고 iteratorResult 인터페이스를 준수하는 객체를 반환하는 함수로, 일반적으로 전달된 값과 true와 동일한 값으로 수행된다. 이 메서드를 호출하면 호출자가 더 이상 next()를 호출하지 않으며 정리 작업을 수행할 수 있음을 iterator에게 알린다.
  • throw(exception)
    0개 혹은 1개의 인수를 허용하고 iteratorResult 인터페이스를 준수하는 객체를 반환하는 함수로, 일반적으로 true와 같다. 이 메서드를 호출하면 호출자가 오류 조건을 감지하고 예외가 일반적으로 오류 인스턴스임을 iterator에게 알린다.

빌트인 이터러블 built-in iterable

자바스크립트는 다음과 같은 빌트인 이터러블을 제공한다.

빌트인 이터러블Symbol.iterator 메서드
ArrayArray.prototype[Symbol.iterator]
StringString.prototype[Symbol.iterator]
MapMap.prototype[Symbol.iterator]
SetSet.prototype[Symbol.iterator]
TypedArrayTypedArray.prototype[Symbol.iterator]
argumentsarguments.prototype[Symbol.iterator]
DOM 컬렉션NodeList.prototype[Symbol.iterator]
HTMLCollection.prototype[Symbol.iterator]

for-of 문

for-of 문은 내부적으로 이터레이터의 next 메서드를 호출하여 이터러블을 순회하며 next 메서드가 반환한 이터레이터 리절트 객체의 value 프로퍼티 값을 for-of 문의 변수에 할당한다. 또한 이터레이터 리절트 객체의 done 프로퍼티 값이 false이면 이터러블의 순회를 계속하고 true이면 이터러블의 순회를 중단한다.

for (변수선언문 of 이터러블) { ... }

for (const item of [1, 2, 3]) {
	// item 변수에 순차적으로 1, 2, 3이 할당된다.
	console.log(item);
} // 1 2 3 

for-of문을 for문으로 만들면

const iterable = [1, 2 , 3];

const iterator = iterable[Symbol.iterator]();

for(;;) {
	// 이터레이터의 next 메서드를 호출하여 이터러블을 순회한다.
  // 이때 next 메서드는 이터레이터 리절트 객체를 반환한다.
	const res = iterator.next();

  // next 메서드가 반환한 이터레이터 리절트 객체의 done 프로퍼티 값이 true이면 이터러블의 순회를 중단한다.
	if (res.done) break;

	// 이터레이터 리절트 객체의 value 프로퍼티 값을 item 변수에 할당한다.
	const item = res.value;
	console.log(item); // 1 2 3 
}

for-of문은 for-in문과 매우 유사하다.

for (변수선언문 in 객체) { ... }

for-in문은 객체의 프로토타입 체인 상에 존재하는 모든 프로토타입의 프로퍼티 중에서 프로퍼티 어트리뷰트 [[Enumerable]]의 값이 true인 프로퍼티를 순회하며 열거한다. 이때 프로퍼티 키가 심벌인 프로퍼티는 열거하지 않는다.

이터레이션 프로토콜의 필요성

for-of 문, 스프레드 문법, 배열 디스트럭처링 할당 등은 Array, String, Map, Set, TypedArray(Int8Array, Unit8Array, Unit8ClampedArray, Int16Array, Int32Array, Unit32Array, Float32Array, Float64Array), DOM 컬렉션(NodeList, HTMLCollection), argument와 같이 다양한 데이터 소스를 사용할 수 있다. 모두 이터레이션 프로토콜을 준수하는 이터러블이다.

이러터블은 for-of문, 스프레드 문법, 배열 디스트럭처링 할당과 같은 데이터 소비자(data consumer)에 의해 사용되므로 데이터 공급자(data provider)의 역할을 할 수 있다.

다양한 데이터 소스가 각자의 순회 방식을 갖는다면 데이터 소비자는 다양한 데이터 소스의 순회 방식을 모두 지원해야 한다. 그다지 효율적이지 않다. 하지만 다양한 데이터 소스가 이터레이션 프로토콜을 준수하도록 규정하면 데이터 소비자는 이터레이션 프로토콜만을 지원하도록 구현하면 된다.

이터러블을 지원하는 데이터 소비자는 내부에서 Symbol.iterator 메서드를 호출해 이터레이터를 생성하고 이터레이터의 next 메서드를 호출하여 이터러블을 순회하며 이터레이터 리절트 객체를 반환한다. 이터레이터 리절트 객체의 value / done 프로퍼티 값을 취득한다.

이터레이션 프로토콜은 다양한 데이터 공급자가 하나의 순회 방식을 갖도록 규정하여 데이터 소비자가 효율적으로 다양한 데이터 공급자를 사용할 수 있도록 데이터 소비자와 데이터 공급자를 연결하는 인터페이스의 역할을 한다.

커스텀 이터러블

일반 객체는 이터러블이 아니기에 Symbol.itetaor 메서드를 소유하지 않는다. 이터러블 프로토콜을 준수하지 않으므로 for-of문으로 순회할 수 없다.

제너레이터 generator

제너레이터는 제너레이터 함수로부터 반환되며,

const multiple = {
	[Symbol.iterator]: () => {
	  const max = 10;
	  let num = 0;
	  return {
		next() {
		  return { value: num++ * 2, done: num > max };
		},
	  };
	} ,
};

function* multipleGenerator() { // function에 *을 붙이면 generator가 만들어짐
	for (let i = 0; i < 10; i++) {
    yield i ** 2; 
   // yield는 사용자가 next를 호출할 때까지 가다렸다가 사용자가 next를 호출하면 
   // 그 다음 코드를 실행하여 순회하고 다시 기다렸다가 next를 호출하면 또 순회하는 방식 
   // (사용자에게 제어권을 양도하는 개념)
	}
}
const multiple = multipleGenerator();
let next = multiple.next();
console.log(next.value, next.done); // 0 false
multiple.next();
console.log(next.value, next.done); // 1 false
           
// generator 종료
multiple.return(); // return을 하는 순간 generator는 끝이 남
                         
multiple.next();
console.log(next.value, next.done); // undefined false
                         
// error 던지기
multiple.throw('Error!');
multiple.next();
console.log(next.value, next.done); 
// Error! (Use `node --trace--uncaught ...` 
// to show where the exception was thrown) 크러쉬 발생
                         
// Error 방지하기
function* multipleGenerator() {
  try{
    for (let i = 0; i < 10; i++) {
		yield i **2;
	  }
	} catch(error) {
	    console.log(error); // Error!
  }
}

iterator를 좀 더 간단히 만들 수 있는 것이 generator라고 해서 알아봤는데 비동기처리를 할 때 더 자세히 알아봐야 할 것 같다. iterator나 generator나 개발자가 직접적으로 처리할 일이 별로 없다고는 하지만 자료구조를 공부할 때 나오는 개념이니 숙지해야 겠다.

profile
不怕慢, 只怕站

0개의 댓글