컬렉션과 생성기 (Collections and Generators)
Iterable vs Iterator
Iterable과 Iterator 모두 ES5에서 추가된 새로운 규약(protocol)이다.
Iterable
반복 가능한 객체들을 말한다. 프로퍼티로 Symbol.iterator를 반드시 가져야 Iterable한 객체로 정의될 수 있다. Array나 Map 같은 컬렉션들은 기본적으로 Iterable한 객체로 설정되어 있으며 일반 Object는 가지고 있지 않다. String 같은 빌트인 객체도 Iterable한 객체로 설정되어 있다.
Iterable한 객체들은 아래와 같은 연산이 가능하다.
- for..of 구문을 통한 객체 접근 ( for child of someIterableObj )
- Spread 연산자 ( 전개 연산자, …someIterableObj )
- Destructuring Assignment ( 분해 할당, { child1, child2, child3 } = someIterableObj;
- Iterable을 받도록 정의해놓은 함수의 인자로 전달가능
[]()
[]()
[]()
일반 Object의 경우에는 iterator가 구현되지 않은것을 확인할 수 있다.
[]()
[]()
Iterator
위에서 살펴본 Iterable한 객체의 값을 sequence대로 순회하면서 연산을 수행하는 프로토콜
Iterator는 반드시 next() 라는 함수를 가져야하고, next 함수는 done 과 value를 가지는 객체를 반환해야한다.
[]()
[]()
let noIterableObj = {
first: "lee",
second: "hyo",
third: "won"
};
// Custom iterator 구현
noIterableObj[Symbol.iterator] = () => {
let i = 0;
// next 함수 구현
const _next = () => {
if (i < 4) i++;
switch (i) {
case 1: return { value: { first: "lee" }, done: false }
case 2: return { value: { second: "hyo" }, done: false }
case 3: return { value: { third: "won" }, done: false }
default:
return { value: undefined, done: true }
}
}
return { next: _next }
};
console.log(noIterableObj);
console.log("-----------");
for (let item of noIterableObj) {
console.log(item);
}
[]()
Iterable과 Iterator가 왜 필요할까?
그 답은 아래 그림을 참고해서 살펴보자.
[]()
JS에서 데이터 소비자(데이터를 소비하면서 연산을 수행하는 것)들이 처리해야 하는 데이터들은, 다양한 데이터 타입으로 존재하며 이를 단일한 방법으로 처리하기 위해 공통의 인터페이스로 정해놓은 것이다.
디자인패턴의 Iterator 패턴과 유사하며, 목적 또한 이에 근거한다.
Generator
Generator in Computer Science =
In computer science, a generator is a routine that can be used to control the iteration behaviour of a loop. All generators are also iterators.[1] A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately. In short, a generator looks like a function but behaves like an iterator.
컴퓨터과학에서 제너레이터란, 순회 (하나씩 방문하는 것) 하면서 반복하는 것을 제어하기 위한 (반복적인) 방식을 말하며, 모든 제너레이터는 또한 이터레이터이다.
제너레이터는 배열을 반환하는 함수와 매우 유사하며, 실제로 제너레이터는 파라미터들을 가지고 있고, 호출될 수 있으며 값들을 순서대로 만들어 낼 수 있다.
그러나, 모든 값을 포함하는 배열을 만들어 내고 한 순간 모두 반환하는 대신에,
( 한 번에 모두 리턴하는 함수의 경우와는 반대로)
제너레이터는 한 번에 하나씩 적은 메모리를 사용하고 호출자가 처음 약간의 값을 즉시 얻어내어 처리하기 시작할 수 있다.
( 모든 값을 한 번에 생산하지 않으니 메모리가 당연히 적게 들고, 생산한 값을 바로 바로 얻을 수 있으니 빠르다. 컴파일 방식과 인터프리팅 방식의 차이를 생각해보자)
한마디로 말해서 제너레이터는 함수처럼 보이지만, 이터레이터 처럼 행동한다.
[]()
아이스크림 기계를 생각해보자.
소프트 아이스크림을 주문하면 버튼만 누르면 처음부터 톡 하고 완전히 만들어진 아이스크림이 나오는 방식이 아니다.
레버를 당겨서 조금씩 바로바로 아이스크림을 생산한다.
이때 아이스크림은 lazy하게 생산된다고 할 수 있다.
처음부터 아이스크림을 쭉 만들어놓고 손님을 기다리는게 아니고 필요할 때 마다 조금씩 뽑는것이다.
이런 방식이면 손님이 안와서 아이스크림이 안팔릴것을 걱정할 필요도 없고 만들어놓은 아이스크림을 얼려놓을 전기세가 들지도 않는다.
또한 10개 이상의 대량 주문이 들어와도 손님 10명이 모두 기다릴 필요가 없다. 그냥 먼저온 순서대로 아이스크림을 받아서 맛있게 먹으며 돌아가면 된다. 가게는 손님이 기다릴 공간을 크게 마련해두지 않아도 괜찮고 손님들은 더위에 지쳐서 오래 기다릴 필요가 없다.
JS에서 제너레이터란?
자바스크립트에서 제너레이터란 멈췄다가 나중에 다시 실행할 수 있는 함수이다. 멈췄다가 다시 실행하면, 그때 그 context를 기억하고 있어 멈춘 지점 바로 거기에서 다시 시작할 수 있다.
자바스크립트는 명백히 싱글 스레드 위에서 돌아가는 언어지만, 제너레이터는 마치 멀티스레드 환경에서 자유롭게 context switching 하는것 같은 착각을 불러 일으킨다.
제너레이터 함수가 호출되면 바로 실행되는 것이 아니라, Iterator 객체가 반환되고, Iterator 안에 있는 next() 함수를 호출하면 제너레이터 함수가 실행되어 yield 문을 만날때까지 진행하고 yield 표현식이 명시하는 iterator로 부터 값을 반환한다.
다음 next() 함수가 수행되면 진행이 전에 멈췄던 위치에서 부터 다시 시작한다.
next가 반환하는 객체는 yield문이 반환할 값을 가진 value 속성과 제너레이터 안의 모든 yield문이 실행되었는지 나타내는 done 속성을 가진다.
next 함수에 인자를 넣어서 실행할 경우 (예를 들어 next(3) 이런 식으로)
진행을 멈췄던 위치의 yield문을 next 함수에서 받은 인자값으로 치환하고 그 위치에서 다시 시작하게 된다.
만약 yield* 표현식을 만나면 다른 제너레이터 함수가 위임(delegate)되어 진행된다.
제너레이터 정의하는 법
1. function* 선언문
function* generator(i) {
yield i;
yield i + 10;
}
var gen = generator(10);
console.log(gen.next().value);
// expected output: 10
console.log(gen.next().value);
// expected output: 20
2. function* 표현식
var generator = function* (i) {
yield i;
yield i + 10;
}
var gen = generator(10);
console.log(gen.next().value);
// expected output: 10
console.log(gen.next().value);
// expected output: 20
3. GeneratorFunction()
var GenOrigin = Object.getPrototypeOf(function* () { }).constructor;
var generator = new GetOrigin("i", "yield i; yield i + 10;");
var gen = generator(10);
console.log(gen.next().value);
// expected output: 10
console.log(gen.next().value);
// expected output: 20
1) yield
제너레이터를 멈추게하거나 다시 실행시키도록 하는 역할
[returnVal] = yield [expression]
expression을 작성하면 이를 평가하고 평가 결과를 반환.
표현식이 없으면 undefined를 반환한다.
expression의 결과를 [resultValue]에 할당하지 않고, 제너레이터 객체가 next 함수를 호출하면 next 함수의 파라미터 값이 returnValue에 설정됌.
- 참고로 yield을 번역하면 ‘산출하다’라는 뜻이 있으며, 어떤 연산의 결과로 값을 만들어 낸다는 뜻을 가진다. 자세한 사항은 위키 참고
[]()
2) next
[]()
<span id="now">0</span>
<br />
<button id="generator">next</button>
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator() {
let currentVal = 0;
yield ++currentVal;
}
const numGen = numberGenerator();
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = numGen.next().value;
});
한 번 클릭했을때 1이 반환되고, 한 번 더 클릭했을때는 undefined가 뜬다.
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator() {
let currentVal = 0;
yield ++currentVal;
yield ++currentVal;
yield ++currentVal;
}
const numGen = numberGenerator();
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = JSON.stringify(numGen.next(), null, 2);
});
이것은 yield가 모두 실행되었기 때문에 이터레이터의 done이 true로 바뀌기 때문이다.
약간 변형해서 yield를 3번 실행하고 next함수를 출력해보자.
yield가 3번째 호출되면 next 함수의 done은 true로 바뀌고 값은 더이상 나오지 않는다.
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator(param) {
let currentVal = 0;
yield ++currentVal;
yield ++currentVal + param;
yield ++currentVal;
}
const numGen = numberGenerator(10);
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = numGen.next().value;
});
제너레이터는 함수적인 성질을 가지고 있기 때문에 파라미터 또한 전달할 수 있다고 했는데, 제너레이터를 생성할 때 인자를 전달하여 사용할 수 있다.
인자는 255개 까지 만들어 전달이 가능하므로 참고하자.
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator(param) {
let currentVal = 0;
const param = yield;
yield param + ++currentVal;
}
const numGen = numberGenerator(10);
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = numGen.next(10).value;
});
Next 함수로도 인자를 전달할 수 있는데, 해당 순서의 yield로 초기화 된다.
위의 코드를 분석해보면 아래와 같다.
- 첫번째 next(10)을 전달했을때 param 변수가 초기화 된다. yield 뒤에 표현식이 없으니 리턴되는 값고 undefined로 돌아옴
- 두번때 yield가 돌때는 초기화 된 param과 선증가된 currentVal이 더해져 반환된다.
여기서 헷갈릴 수 있으니 몇가지 예제로 더 연습해보자.
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator(param) {
let currentVal = 0;
const param = yield (1 + 2);
yield param + ++currentVal;
}
const numGen = numberGenerator();
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = numGen.next(10).value;
});
- (1-1)Next 함수에서 전달된 10이 param으로 초기화 된다.
- (1-2) yield 오른쪽에 있는 표현식이 호출한 곳으로 반환된다. 그래서 3이 찍힌다.
- 그리고 다음 next 호출시 초기화된 param과 선증가된 currentVal이 더해서 11이 반환된다.
- 더이상 yield문이 없으므로 이터레이터의 done이 true로 변경
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator() {
let currentVal = 0;
yield '난 처음에만 나오고 없어질 거야 안녕';
yield yield;
const param = yield;
yield (++currentVal + param);
}
const numGen = numberGenerator();
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = numGen.next(10).value;
});
- 처음 yield문을 만나면 10이 전달되지만 받을 변수가 없으므로 쓰레기값이 되어버림.
- yield문 오른쪽에 있는 문자열이 반환됌
- 10이 전달되어 초기화 되지만 오른쪽에 반환될 값이 없으니 undefined가 반환
- yield 수행시 10이 전달되지만 초기화 시킬 변수가 없으므로 쓰레기값 됌
- 이전 yield에서 전달되었던 10이 반환됌
- 10이 전달되어 param에 초기화 됌, undefined 반환
- 선증가된 currentVal과 param이 합쳐져 반환
정리해보면, 먼저 인자로 전달한 something이 제네레이터의 yield로 전달되고 expression으로 산출되는 값이 Caller에게 반환된다.
[]()
[]()
3) return
gen.return(value)
매개 변수
value
반환될 값.
반환 값
이 함수의 호출과 함께 주어진 인수 값을 반환한다.
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator() {
let currentVal = 0;
while (true) {
yield ++currentVal;
}
}
const numGen = numberGenerator();
const sum = () => {
return JSON.stringify(numGen.next(), null, 2);
};
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = sum();
});
(코드펜에서 중간에 지멋대로 순회가 종료되는 현상이 있어서 크롬에서 테스트함)
위에서 보듯이 무한으로 증가하는 제네레이터를 생성했다.
let genBtn = document.getElementById("generator");
let nowPanel = document.getElementById("now");
function* numberGenerator() {
let currentVal = 0;
while (true) {
yield ++currentVal;
}
}
let counter = 0;
const numGen = numberGenerator();
const sum = () => {
let result = null;
if (counter === 5) {
return JSON.stringify(numGen.return(-1), null, 2);
} else {
return JSON.stringify(numGen.next(), null, 2);
}
counter++;
return result;
};
genBtn.addEventListener("click", (e) => {
nowPanel.innerHTML = sum();
});
Return 함수로 넘겨준 인자가 next의 value로 세팅되고 이터레이션이 종료되는것을 알 수 있다.
코루틴
- 서브루틴 (Subroutine) : 루틴 안에서 불린 하위 루틴으로 종료되면 값을 호출한 루틴(Caller)에게 반환한다.
- 코루틴 (Coroutine) : 메인 루틴(Caller)에서 호출하여 시작되고 종료될 때 값을 반환하는 대신 잠시 중지하고 나중에 다시 Caller의 호출에 의해 재개될 수 있는 패턴. 서브루틴은 특수한 코루틴이라고 볼 수 있다.
제네레이터의 활용
- 비동기 상황에서 동기처럼 사용가능
- 깔끔한 비동기 프로그래밍이 가능
- 무한 루프에 빠질 걱정없는 재귀적 처리
1. ES7에 도입될 async - await 또한 Promise와 제너레이터를 기반으로 트랜스파일 될 수 있다.
2. 마치 동기적으로 작동하듯이 깔끔한 코드 작성이 가능해진다.
3. While 무한 루프에 빠져 콜스택을 뻗어버리게 만들 일 없이, 메모리 낭비 안하고 필요할때마다 제너레이팅 하면서 재귀적으로 처리할 수 있는 효율적인 코드를 작성 가능