[Javascript] Generator 활용과 장점 (iterable, iterator, lazy evaluation)
들어가며
generator, iterator, iterable의 상세한 설명은 없습니다.
궁금하신 분들은 아래의 포스팅을 먼저 읽고 보는 것을 추천드립니다.
무한 루프 (무한수열 만들기)
일반 함수에서 무한 루프가 발생한다면 프로세스가 죽을 수 있습니다.
function infinity() {
let i = 0;
while (true) console.log(++i);
}
위 함수에서 우리가 원할 때 증가한 i값을 출력하고 싶어도 그렇게 동작할 수 없습니다.
function infinity() {
let i = 0;
while (true) return ++i;
}
console.log(infinity()); // 1
console.log(infinity()); // 1
console.log(infinity()); // 1
꺼내올 때마다 계속 1만 꺼내올 것입니다.
하지만 제너레이터를 활용한다면 증가하는 수를 원할 때 꺼내올 수 있게 됩니다.
먼저 제너레이터를 만들어줍니다.
function* infinity() {
let i = 0;
while (true) yield ++i;
}
제너레이터는 이터레이터를 만드는 함수로서 이터레이터의 next()를 활용하여 값을 지연 평가할 수 있게 됩니다.
여기서 지연 평가란, 로직에서 뒤늦게 값이 필요할 때 만들어내는 방식입니다.
이터레이터를 가지고 무한수열을 만들어보겠습니다.
function* infinity() {
let i = 0;
while (true) yield ++i;
}
const iter = infinity();
console.log(iter.next().value); // 1
console.log(iter.next().value); // 2
console.log(iter.next().value); // 3
실행 결과는 1, 2, 3 으로 정상 동작합니다.
마치 데이터베이스의 Auto Increment처럼 우리가 원할 때 iter만 공유한다면 언제든지 1이 증가된 수를 받아서 사용할 수 있게 됩니다.
하지만 스프레드 연산자나 for of 문을 그대로 실행시키면 모두 평가되어 무한 루프가 발생하므로 주의해야 합니다.
지연 평가의 장점
앞서 언급한 지연 평가를 좀 더 확실히 경험해 보겠습니다.
1부터 99까지 순회할 수 있는 반복자를 만들어 보겠습니다.
먼저 배열을 만드는 방법입니다.
function newArr(n) {
let i = 1;
const res = [];
while (i < n) res.push(i++);
return res;
}
const arr = newArr(100);
console.log(arr);
코드를 실행하면
[
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60,
61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96,
97, 98, 99
]
1부터 99까지 배열이 출력됩니다.
다음은 이터레이터 즉, 반복 가능한 객체를 만드는 방법입니다. (배열도 반복 가능한 객체입니다)
function* newArrGen(n) {
let i = 1;
while (i < n) yield i++;
}
const iter = newArrGen(100);
console.log(iter);
코드를 실행하면 (node.js)
Object [Generator] {}
object 타입이 출력된 것을 확인할 수 있습니다.
다시 말해 반복 가능한 객체가 만들어졌고 즉시 평가되지 않아서 1~99까지 리스트가 만들어지지 않았습니다.
이제 위의 두 함수를 가지고 만들어진 반복 가능한 객체로 5의 배수를 작은 수부터 2개만 찾도록 구현해보겠습니다.
function newArr(n) {
let i = 1;
const res = [];
while (i < n) res.push(i++);
return res;
}
function* newArrGen(n) {
let i = 1;
while (i < n) yield i++;
}
function fiveArr(iter) {
const res = [];
for (const item of iter) {
if (item % 5 == 0) res.push(item);
else if (res.length == 2) break;
}
return res;
}
console.log(fiveArr(newArr(100)));
console.log(fiveArr(newArrGen(100)));
실행 결과
[ 5, 10 ]
[ 5, 10 ]
같은 결과를 만들어냅니다. 하지만 제너레이터를 활용한 코드는 좀 더 빠르게 동작합니다.
이 차이가 즉시 평가와 지연 평가인데요.
fiveArr(newArr(100))
코드는 newArr 함수가 배열을 즉시 만들어냅니다. (1~99까지) 만들어진 배열을 리턴해서 fiveArr 함수를 수행하게 됩니다. 쉽게 말해 fiveArr([1, 2, 3, 4, 5, 6, ..., 97, 98, 99]) 된다는 것입니다.
fiveArr(newArrGen(100))
코드는 newArrGen 함수가 이터레이터만 만들어내고 fiveArr 함수에서 필요할 때 이터레이터에서 평가된 값을 사용하게 됩니다.
극단적으로 크기를 올려서 시간을 비교해보겠습니다.
console.time('');
console.log(fiveArr(newArr(10000000))); // [ 5, 10 ]
console.timeEnd(''); // : 285.535ms
console.time('');
console.log(fiveArr(newArrGen(10000000))); // [ 5, 10 ]
console.timeEnd(''); // : 7.296ms
어마어마한 크기의 반복 가능한 객체를 만든 다음에 fiveArr 함수를 실행하는 모습입니다.
그러나 즉시 평가와 달리 지연 평가는 확실히 빠르게 동작하는 것을 볼 수 있습니다.
newArrGen 함수에 log를 찍어보면 필요할 때만 값을 평가해서 동작하는 모습을 볼 수 있습니다.
function* newArrGen(n) {
let i = 1;
while (i < n) {
console.log(i);
yield i++;
}
}
...
...
...
console.time('');
console.log(fiveArr(newArrGen(10000000)));
console.timeEnd('');
while문 안에 log를 출력하도록 코드를 수정하고 실행합니다.
1
2
3
4
5
6
7
8
9
10
11
[ 5, 10 ]
: 11.452ms
아무리 큰 수까지 반복해도 결과적으로 필요할 때 평가되어 실행되므로 빠르게 동작할 수 있습니다.
log를 출력하느라 시간이 조금 더 걸린 것 같네요.
값이 필요할 때 이터레이터에서 꺼내 쓰므로 무한대로 이터레이터를 만들어도 결과는 같습니다.
console.time('');
console.log(fiveArr(newArrGen(Infinity))); // [ 5, 10 ]
console.timeEnd(''); // : 7.441ms
미완성
추후 더 좋은 예제와 정보로 다시 업데이트 하겠습니다.