객체의 맴버를 반복할 수 있는 객체 (반복이 가능하다)
iterator를 리턴하고, Object property에 Symbol.iterator 를 갖고 있어야 한다.
→ 객체가 Symbol.iterator를 프로퍼티의 key로 갖고 있으면, 자바스크립트 엔진은 객체를 이터레이션 프로토콜을 따르는 것으로 인식한다.
기본적으로 Iterable 객체를 가지고 있는 객체
const array = [1,2,3,4,5] // 이터러블
const iterator = array[Symbol.iterator]() // Symbol.iterator 메소드로 iterator 반환
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: 4, done: false }
console.log(iterator.next()) // { value: 5, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
이터레이터의 next 메소드는 value, done으로 이루어진 객체를 반환하는데
value는 현재 Iterable 값,
done은 더이상 순회할 수 있는지 판단해주는 값이다.
데이터 컬렉션을 순회하기 위한 규칙이라고 볼 수 있다.
Iterable과 Iterator로 이루어져 있다.
Iteration protocol이 필요한 이유?
데이터 소비자(for of, spread, 구조분해할당 ...)와 데이터 공급자(Array, String, Map ...)를 연결하는 인터페이스 역할을 한다.
만약 데이터 공급자가 각자의 순회방식을 갖는다면, 모든 순회 방식을 지원해야하기 때문이다.
const tenObject = {
1: 1,
2: 2,
...
9: 9,
10: 10,
};
1부터 10까지 key, value로 정의되어 있는 일반 객체가 있다고 했을 때,
for (const n of tenObject) {
console.log(n);
} // error
console.log([...tenObject]) // error
위의 결과로 TypeError: tenObject is not iterable
를 볼 수 있는데,
for ... of
나 spread
문법의 경우 Iterable 한 객체에만 사용할 수 있기 때문이다.
따라서 객체에 Symbol.iterator 를 구현하면, 일반객체를 iterable 객체로 만들 수 있다.
const tenObject = {
[Symbol.iterator]() {
const maxNum = 10;
let num = 0;
return {
next() {
num++;
return {
value: num,
done: num > maxNum ? true : false,
};
},
};
},
};
Symbol.iterator 메소드를 통해 next 메소드를 가진 이터레이터를 리턴한다.
for (const n of tenObject) {
console.log(n); // 1 2 3 ... 9 10
}
console.log([...tenObject]) // ... [1, 2, 3, ... 9, 10]
위 결과로 일반 객체를 Iterable 객체로 만들어 여러 메소드를 사용할 수 있다.
하지만 이터러블 객체를 만들어 줄 뿐, 정해진 값을 사용하며 외부에서 조건을 변경할 수 없고 next함수 또한 사용할 수 없다.
따라서 조금 까다롭지만, 다음과 같이 함수로 Symbol.iterator 메소드와 next 메소드를 갖는 객체를 만들어 사용할 수 있다.
const tenObjectFunction = function () {
const maxNum = 10;
let num = 0;
return {
[Symbol.iterator]() {
return this;
},
next() {
num++;
return {
value: num,
done: num > maxNum ? true : false,
};
},
};
};
const iter = tenObjectFunction();
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]); //[4,5,6,7,8,9,10]
위에서 까다롭게 구현한 이터러블 생성함수는 제너레이터로 비교적 간편하게 대체할 수 있다.
function* tenGenerator() {
let num = 1;
while (num <= 10) {
yield num++;
}
}
const iter = tenGenerator();
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]); //[4,5,6,7,8,9,10]
제너레이터 함수를 호출하여 제너레이터 객체로 간단하게 표현된 것을 파악할 수 있다.
제너레이터 함수에서는 *
, yield
이 두가지 키워드를 기억하고 있어야 한다.
*
: 별표, asterisk 라고 부르는데 function 뒤에 붙여주면서 제너레이터 함수를 정의하는 키워드이다.yield
: 제너레이터 함수에서 사용되며 제너레이터가 한번 멈추고 다음 next가 호출 되었을때 yield 이후의 코드가 실행 되는 일종의 블락?의 기능을 한다.Lazy Evaluation
예를들어, 어떠한 반복을 많이 해야하는 상황에서 매우 큰 배열을 선언한다고 가정하면 다음과 같은 두가지의 문제가 발생할 것이다.
큰 배열을 생성하는데 걸리는시간, 큰 배열이 차지하는 메모리
이 문제를 제너레이터를 통해 선언한다면, iterable 객체를 생성해두고 반복할 때 마다 값을 얻어내기 때문에 해결할 수 있다.
비동기
사실, Redux-saga로 프로젝트를 하는 도중에 제너레이터 함수를 처음 접해서 무척이나 당황했다. 학습을 하고 사용해야 겠다는 생각이 들어서, ES6에 반영된 제너레이터를 먼저 학습하여 포스팅하게 되었다.
제너레이터는 yield 키워드에 따라 잠깐 멈추는 특징이 있는데 이를 사용해 비동기로 이루어 지는 작업들의 순서를 보장할 수 있다.
async/await 을 쓰면 되는거 아닌가? → 맞다 ES7에 나온 async/await 은 제너레이터를 통해 구현되어 있다.