[es6for3] for…of 대체

es6for3 시리즈

ES6는 새로운 언어철학을 제시하고 편리한 기능을 제공합니다. 하지만 크롬(52버전까지)조차 별도로 플래그를 활성화해야 만 쓸 수 있는 상황입니다.
이에 업계는 바벨 등의 번역기를 이용하여 ES5로 번역시키는 수를 사용하고 있습니다. 결국 바벨이 번역한 코드는 바벨라이브러리를 사용하는 ES5코드로 번역됩니다. ES6에 대한 깊은 이해는 ES6의 코드 방식을 하위 ES3.1로 번역하는걸 가능하게 하죠. ES6 기능에 대한 정확한 이해와 나만의 바벨을 만들기 위한 첫 단계로 ES6의 각 기능을 ES3.1로 번역해보죠.

이터러블의 값을 손쉽게 추출하기

실제로 이터레이터 인터페이스를 준수하면 next()를 호출할 때마다 value, done을 처리해야하니 굉장히 귀찮습니다. es6는 이터레이터에게 값을 얻는 단축표현을 풍부하게 제공하고 있습니다. 여기서는 간단히 3가지 방법을 살펴보겠습니다.

  1. 우선 해체식을 이용할 수 있습니다. 해체는 let, const의 선언과 함께 사용되는 경우를 예제로 많이 접하셨겠지만 실은 식의 일부입니다. 단지 배열해체는 괄호없이 사용할 수 있지만 객체 해체는 반드시 괄호가 필요합니다.
let a, b, c;

//ok
[,a] = arr;

//괄호로 감싸면 ok
({id:b, title:c} = obj);

이 중 배열 해체식은 실은 이터러블과 이터레이터에 대응합니다. 스펙문서에선 이터러블이라고 명시되어있지만 그럼에도 불구하고 구현물들은 이터레이터도 지원합니다(항상 이게 이상하다고 생각하고 있긴 합니다. getIterator의 작동에는 본인이 이터레이터면 그대로 반환한다라는 항목이 없는데 말이죠 ^^)

이터레이터에 직접 대응한다는 것은 다음의 코드로 쉽게 확인할 수 있습니다.

const [a] = [1,2][Symbol.iterator]();
console.log(a); //1

이러한 성질을 이용하면 이터러블이든 이터레이터든 제한된 값을 얻는 행위를 비롯하여 다음 단계의 값을 얻는 것 조차 해체식을 사용하는 편이 훨씬 수월합니다.

const iterator = [1,2,3,4,5][Symbol.iterator](), v;
while([v] = iterator, v !== undefined) console.log(v);

해체식 자체야 우변의 값을 그대로 반환하기 때문에 직접 조건으로 사용할 수는 없지만 해체로 얻은 v는 undefined직전까지 쓸모가 있는 식입니다.

  1. 펼침연산자를 이용해도 좀 편리해집니다. 이 방법은 무한의 원소를 갖는 이터레이터에겐 금물입니다만,
const iterator = [1,2,3,4,5][Symbol.iterator]();
const arr = [...iterator];

이 방법도 굉장히 손쉽게 이터레이터의 값을 추출할 수 있죠. 간단한 펼침연산자의 작동이 딱히 설명은 필요없을듯 합니다.

  1. 마지막으로 Array.from()을 이용하는 방법이 있습니다. Array.from 메소드도 인자로 이터러블이나 이터레이터를 받아서 배열로 바꾸는 기능을 제공함으로 손쉽게 값을 추출할 수 있습니다.
const iterator = [1,2,3,4,5][Symbol.iterator]();
const arr = Array.from(iterator);

for of에 대해

위에 설명한 세 가지 중에 해체식을 이용한 while루프 편법을 언어수준으로 내장한 것이 바로 for of 입니다. 우선 while문으로 블록스코프와 해체를 이용해 배열을 순회하는 경우를 다시 표현해보죠.

const arr = [1,2,3,4];
{
   const iterator = arr[Symbol.iterator]();
   let value;
   while([value] = iterator, value !== undefiend){
     console.log(value);
   }
}

굉장히 귀찮습니다. 이를 한 방에 블록스코프 생성과 함께 해결해주는 제어문이 for of입니다.

const arr = [1,2,3,4];
for(let value of arr){
  console.log(value);
}

굉장히 간결하게 표현됩니다. 어찌보면 for of의 변수 선언부분은 배열해체식을 생략한 형태에 가까운 것입니다(while문과 비교해보세요)
예를들어 다음과 같은 2차원 배열을 for of의 해체로 표현해보죠.

const arr = [['hika', 1], ['jidolstar', 2], ['munne', 3]];
for(let [name, id] of arr){
  console.log(name, id);
}

이렇게 원소에 대한 해체만 기술할 것입니다. 헌데 이를 while문으로 대체해보면 원소를 감싸는 배열해체까지 처리해야합니다(심지어 기본값도 ^^)

const arr = [['hika', 1], ['jidolstar', 2], ['munne', 3]];
{
   const iterator = arr[Symbol.iterator]();
   let name, id;
   while([[name, id] = []] = iterator, name !== undefiend){
     console.log(name, id);
   }
}

해서 for of가 이터레이터를 처리할 때 굉장히 고수준으로 귀찮은 것들을 은닉하고 다양한 처리를 내장하고 있다는 것을 알 수 있습니다.

ForOf 함수의 설계

일단 이걸 흉내내기 위한 계획은 다음과 같은 API를 제공하는 것입니다.

//기본 원소값을 그대로 받을 때
For.Of(iterator, function(v){
  //..여기서 할 일을 한다.
});

//해체식을 동반하는 경우
For('[name, id]').Of(iterator, function(vo){
  vo.name;
  vo.id;
});

거의 ES3.1에서 할 수 있는한 가깝게 표현해봤습니다. for나 of는 키워드이므로 대문자로 시작하는 For와 Of를 제작하면 됩니다. For는 근본적으로 호출하면 Of라는 메소드를 갖는 객체를 반환하는 함수라고 생각할 수 있습니다.

var obj = For('[name,id]');
typeof obj.Of == 'function'

For는 이런 느낌의 함수죠. 따라서 ‘Of’라는 메소드갖는 클래스를 정의할 필요가 있습니다. 또한 For는 인자로 해체문자열을 받으므로 이를 Of에게 전달할 책임이 있습니다.

var Of = function(dest){
  this.dest = dest; //해체문자열
};
Of.prototype.Of = function(iter, callback){//이터러블과 콜백함수
  var dest = this.dest;
  //iter객체를 순회하며 해체하여 f를 호출한다.
};

여기에 상응하는 For함수는 매우 간단하겠죠. Of클래스의 팩토리함수인 셈입니다.

var For = function(dest){
  return new Of(dest);
};

와꾸는 대충 나왔으니 Of 메소드에 대해서 구체적으로 계획을 생각해보죠. for of문의 of뒤에 나오는 이터러블객체는 정확하게는 다음과 같은 단계로 이터레이터를 추출하는 것으로 예상됩니다.

  1. 만약 해당 객체에 Symbol.iterator 메소드가 있으면 이를 호출한 객체로 대체한다.
  2. 이 객체에 next메소드가 있는지 확인한다.
  3. next메소드가 존재하면 이 객체를 순회할 대상으로 한다. 없으면 예외를 발생시킨다.

따라서 최초 인자로 받은 iter객체에 대해 동일하게 검증절차를 밟으면 됩니다. 그 다음엔 무한루프를 방지하기 위해 적절히 큰 값으로 루프를 돌면서 next를 호출한 값을 추출하여 콜백함수에 전달해주는 일이겠죠. 이때 기존에 구현한 해체함수 Dest를 활용하면 됩니다. 이상의 내용으로 Of메소드를 구현해보죠.

Of.prototype.Of = function(iter, callback){
  var dest = this.dest;

  //이터러블이면 이터레이터로 환원
  if(typeof iter[Symb.iterator] == 'function') iter = iter[Symb.iterator]();

  //next검사
  if(typeof iter.next != 'function') throw new TypeError();

  //무한루프를 방지하기 위해 적절히 제약을 가한다.
  var cnt = 100000, ㅍ;
  while(cnt--){

    //우선 next를 얻고
    v = iter.next();
    //done이면 중지
    if(v.done) break;
    //아니면 해체식 전달여부에 따라 해체하거나 그냥 원소를 콜백에 보낸다.
    callback(dest ? Dest(dest, v.value) : v.value);
  }
};

고작 요 정도입니다. 이제 마지막으로 남은 과제는 For.Of 형태처럼 For를 함수로 호출하지 않고 그냥 쓰는 경우인데 이건 의외로 간단합니다.

For.Of = Of.prototype.Of;

그냥 할당해주면 됩니다. 이러면 이 시점의 this는 For가 되므로 this.dest는 undefined로 처리되어 무리없이 진행됩니다.
이제 간단히 사용해볼 수 있습니다.

const arr = [['hika', 1], ['jidolstar', 2], ['munne', 3]];

//원래 for of루프
for(let [name, id] of arr){
  console.log(name, id);
}

//For.Of 이용
For('[name, id]').Of(arr, function(vo){
  console.log(vo.name, vo.id);
});

제법 비슷하게 처리됩니다.

결론

전체 코드를 정리하면 다음과 같습니다.

var For = (function(){
  var For = function(dest){return new Of(dest);};
  var Of = function(dest){this.dest = dest;};
  For.Of = Of.prototype.Of = function(iter, callback){
    var dest = this.dest, cnt = 100000, v;
    if(typeof iter[Symb.iterator] == 'function') iter = iter[Symb.iterator]();
    if(typeof iter.next != 'function') throw new TypeError();
    while(cnt--){
      v = iter.next();
      if(v.done) break;
      callback(dest ? Dest(dest, v.value) : v.value);
    }
  };
  return For;
})();

이미 복잡한 기능은 Dest에 숨겨져 있고 배열 등이 와도 이전 이터러블편에서 이미 배열과 문자열 등을 패치해두었기 때문에 잘 작동하게 됩니다. 하지만 여태 설명한 코드는 설명하면서 이해하기 쉽게 클래스화해서 설명했을뿐 싱글쓰레드 특성상 매번 Of객체를 만들 필요는 없습니다. 따라서 다음과 같이 정리될 것입니다.

var For = (function(){
	var Of = function(iter, f){
		var cnt = 100000, val;
		if(iter[Symb.iterator]) iter = iter[Symb.iterator]();
		while(cnt--){
			val = iter.next();
			if(val.done) break;
			f(dest ? Dest(dest, val.value) : val.value);
		}
		dest = prev;
	}, dest, prev, obj ={Of:Of},
	For = function(d){return prev = dest, dest = d, obj;};
	return For.Of = Of, For;
})();

For Of 안에서 다시 For Of를 사용하는 경우만 처리하면 되기 때문에 prev에 기존 dest를 넣어주면 됩니다. 동작하는 코드와 테스트는 여기에 있습니다.