
프론트엔드 분야에서 iterator javascript 스펙에 대해서 여러가지 이야기가 존재한다. redux-saga라는 라이브러리를 이해하기 위해서 간단하게 알면된다는 이야기부터 javascript 내부 코어 스펙이기에 정확하게 이해하고 사용할 수 있어야한다는 이야기정도로 두 분류가 주류인 것으로 생각된다.
나는 javascript의 내부 코어 스펙이자 ES6+의 근간이라고 생각하기에 매우 중요하다고 생각하였지만 정확하게 프론트엔드에서 어떤식으로 사용해야 하는지에 대해서는 깊게 생각해보지 않아서 한번 예시를 생각해보고 좀 더 잘 쓰기 위해서는 어떤 점을 주의해야할 것인지에 대해서 정리해보려고 한다. 참고는 코드스피츠 강의와 유인동 강사님의 함수형 프로그래밍을 어느정도 보면서 참고하였다.먼저 iterator와 generator를 간략하게 설명한 뒤에 나중에 프론트엔드에서의 사용예시를 설명해보려고 한다.
먼저 interface라는 고유명사를 머리 속에 넣고 생각해보자. javascript에서의 interface란 사양에 맞는 값과 연결된 속성키의 세트이며 어떤 객체라도 인터페이스의 정의를 충족시키며 하나의 객체는 여러개의 인터페이스를 충족하는 것을 말한다.
{
test(str){ return true; }
//test라는 key ,값은 문자열 ,반환결과는 boolean
}
ES6+에서 정의된 방식으로 test라는 키에 str이란 값을 주입 가능하게 되었고
그 값에 상관없이 true 를 반환한다. 이와 같은 interface 프로토콜을 통해서
만들어진 것이 iterator interface 이다.
1. next라는 키를 가진다.
2. 값으로 인자를 받지않고 IteratorResultObject를 반환하는 함수가 온다
3. IteratorResultObject는 value 와 done 이라는 키를 가지고 있다.
4. 이 중 done 은 계속 반복할 수 있을지 없을지에 따라 bool값을 반환한다.
// iterator interface(object)
{ data:[1,2,3,4]
next(){
return {
done:this.data.length==0,
value:this.data.pop();
}
}
}
그리고 iterable 이란 것도 설명이 필요하다.
의사코드로는
1. Symbol.iterator라는 키를 가진다.
2. 값으로 인자를 받지 않고 iterator object를 반환하는 함수가 온다.
// iterator object
[Symbol.iterator](){
return{
next(){
return { done:1,value:false;}
}
}
}
인터페이스 말고도 프로토콜로도 정리가 되어있으니 아래의 공식문서를 참고하면 좋을 것 같다.
Iteration_protocols and iterator protocol
이와 같은 iterable interface 그리고 iterator interface을 우리는 어떤 방식으로 사용하는 것일까?
아까전의 iterator interface를 다시 살펴보자
// iterator interface(object)
{ data:[1,2,3,4]
next(){
return {
done:this.data.length==0,
value:this.data.pop();
}
}
}
위의 iterator interface 예시는 반복자체는 하지 않지만 외부에서 반복을 next로서 실행이 가능하다. 그리고 iterator interface 내부에 이미 반복에 필요한 조건과 실행을 미리 준비하고 있다. 어려운 외부상황과 관계없이 내부에서 조건을 관리하고 있기에 좀 더 효율적이고 정확하게 코드를 파악할 수 있는 힌트가 된다고 생각한다. 그렇게 생각한 이유를 알기 위해서 먼저 직접 반복처리기를 보면서 이야기해보자
const loop=(iter,f)=>{
if(typeof iter[Symbol.iterator]=='function'){
iter=iter[Symbol.iterator]();
}else return;
if(typeof iter.next!='function') return;
do{
const v=iter.next();
if(v.done) return;
f(v.value);
}while(true);
}
const iter={
arr:[1,2,3,4],
[Symbol.iterator](){return this; },
next(){
return{
done:this.arr.length==0,
value:this.arr.pop()
}
}
}
아래와 같은 iter와 같은 iterable interface 를 만들어두고 loop라는 함수를 이용한다면 iter에 있는 arr 이 자동적으로 while문에 의해서 실행된다. 외부의 변경이 없어도 내부에서 arr의 데이터를 파악하고 실행하기에 오류가 최소화되고 규격에 맞는 틀을 잘 만들수 있기에 좀 더 효율적이고 정확하게 코드를 파악하는 힌트가 된다고 생각하였다.
먼저 아까전 설명하였던 iterable interface를 배경으로 우리는 현재 사용하고 있는 여러가지 기능을 우리도 모르게 사용하고 있다
1. Array destructuring(배열해체)
const [a,...b]=iter
console.log(a)
// 4
console.log(b)
// [3,2,1]
위와 같은 방식의 해체방식은 알게 모르게 사용하고 있다. const [a,...b]=iter 라는 배열을 선언하게 되면 우리는 iter의 next를 두번 실행하게 된다. a라는 배열을 선언하면 this.arr.pop을 한번 실행한 것과 같고 ...b 는 그 이후의 값을 여러번 실행한 것과 같다. 이방식은 javascript 표준 방식이기에 일반 배열을 선언하면 자동으로 iterator interface이 prototype과 같이 통용되기에 우리가 사용하고 있지만 기본적인 원리는 iterable interface를 통한 Array destructuring 이라고 할 수 있다.
const a=[...iter]
console.log(a)
// [4,3,2,1]
자동으로 iter라는 iterable이 pop을 여러번 실행하여 펼쳐지는 것을 뜻한다.
react에서 매우 자주 사용하는 방식이고 현재는 모르면 안되는 방식이기도 하다.
for(const v of iter){
console.log(v);
}
// 4,3,2,1
For of 도 결국 iterator 가 아니라면 실행되지 않는 방식이다. 우리가 가끔씩 만나는 에러의 정체도 결국 이 방식을 잘 이해하지 못해서 생기는 TypeError: 'x' is not iterable 도 결국 여기서 걸려서 생기는 문제라고 생각하면 된다.
아까전의 반복문을 다시 살펴보면 조금 문제가 있다는 것을 알게된다. 바로 iter에 무한한 배열과 같은 값이 들어오면 영원히 false 값이 나오지 않고 blocking 상태가 되는 상황에 직면하게 된다. 그렇기에 while 문에 조건을 다는 것은 필수적이다. 주석처럼 max와 같은 값을 주고 최대로 루프를 도는 것을 관리해야한다.
const loop=(iter,f,max)=>{
if(typeof iter[Symbol.iterator]=='function'){
iter=iter[Symbol.iterator]();
}else return;
if(typeof iter.next!='function') return;
do{
const v=iter.next();
if(v.done) return;
f(v.value);
// if(max>0) max--
// }while(max);
}while(true);
}
그렇지만 이런식으로 symbol.iterator 를 정의하고 반복을 만드는 것은 매우 불편한 일이라고 할 수 있다. 그것을 해결하는 방식이 generator이다.
const N2=class{
constructor(max){
this.max=max;
}
[Symbol.iterator](){
let cur=0,max=this.max;
return {
done:false,
next(){
if(cur>max){
this.done=true;
}else{
this.value=cur*cur
cur++
}
return this;
}
}
}
}
N2는 제곱을 요소로 가지는 클래스이다. 이 클래스는 console.log([...new N2(4)])
와 같이 실행하면 4가의 요소를 가진 배열을 만들어준다. 그리고 각각 개별 요소에 대해서도 for of 도 동작하게 만들어낼수 있다. 하지만 이런 함수는 매우 많은 고민과 지식을 요구한다. 그렇기에 나온 것이 generator 이다
const squareGenerator=function *(max){
let cursor=0;
while(cur<max){
yield cur*cur;
cur++
}
}
위의 함수는 generator 함수의 생성 프로토콜이다. generator 함수는 yield는 next와 같은 함수를 실행시킨다라고 생각하면 이해하기가 조금 쉬워진다. 그리고 실행될때 아까전 next에서 실행된 것과 같이 done과 value를 return 해주고 suspend 된다. 이 방식은 coroutine 이라고 할수 있다. 관련 내용은 url도 참고한다면 좀 더 이해가 될 것이라고 생각한다.
프론트엔드에 좀 더 직접적인 내용은 사용예시 2를 통해서 정리해보려고 한다.