[es6] 연구노트 #2 – 명확한 반복처리

개요

s67에서는 ES2015를 주제로 스터디를 전개했습니다. 그 때의 내용을 정리하여 포스팅하는 시리즈입니다. ES2015에 대한 제 개인적인 이해와 정보를 정리해봅니다.

Iterable & Iterator

for 또는 while등의 문(statement)으로 구성된 반복은 실행될 수는 있지만 값이 될 수 없습니다. 값이 될 수 없다는 것은 그 행위를 실행할 수 있을 뿐이지 다음과 같이 할 수는 없다는 것입니다.

  • 인자로 전달하거나 상태를 기억해 둘 수 없다.
    1. 사전에 정의가 끝난 정적인 값에 대해서만 반복처리가 가능합니다.
    2. 반드시 반복 전에 반복할 대상에 대한 사전처리가 완료되어있어야 합니다.
    3. 따라서 반복처리될 대상에 대해 지연처리가 불가능하죠.
  • 지연실행의 필요성
    1. 연산을 통해 계산될 값은 미리 정의해둘 이유가 없습니다.
    2. 메모리에 정의한 적이 없어도 반복시점에 자원을 확보하여 반환할 수도 있죠.

요점은 이러한 반복행위에 대한 것으로 값으로 만드는 방법이 없는 것이 아닙니다. 대표적으로 디자인패탠의 커맨드패턴이나 이터레이터패턴을 이용하면 충분히 값으로 환원시킬 수 있습니다. 하지만 es2015에서의 진정한 가치는 Iterator프로토콜을 통한 표준적인 값 형태의 반복처리기 라는 것입니다.

반복의 기본과 Iterator Protocol

우선 반복이란 두 가지 요소로 구성되어있습니다. 반복할지말지를 결정한 상태와 반복시마다 실행할 부분인거죠. while문을 예로 들면 다음과 같습니다.

while(계속 반복할지의 상태){
   //반복 시마다 실행될 부분
}

Iterator는 이러한 반복 요소에 대한 표준적인 프로토콜을 정의합니다.

  1. 반복 시마다 실행될 부분 → next()메소드를 호출함
  2. 계속 반복할지 여부 → next로 반환된 객체의 done키에 있는 boolean값

예를 들어 문자열은 내부적으로 Iterable프로토콜을 준수하므로 Iterator를 반환합니다. 다음은 마치 배열처럼 문자열의 한자한자를 원소처럼 Iterator로 쓰는 예제입니다.

let [a, b, c] = 'abc'; //해체에 사용
let a = [...'abc']; //펼치기에 사용

//rest parameter에 사용
const test = (...arg)=>arg.join('-'); 
test(...'abcd'); //'a-b-c-d'

반복처리를 위임할 표준적인 값

위의 예에서 주목해야할 사실은 문자열이 Iterable이라는 표준 프로토콜을 통해 반복을 처리할 수 있는 형태의 값이라는 점입니다. 이제 반복이라는 행위를 값으로 캡슐화했으므로 동일한 프로토콜을 소비하는 측에서는 문자열인지 배열인지를 구분할 필요없이 프로토콜 베이스로 처리를 해갈 수 있습니다. 이를 통해 새로 도입된 펼치기, 해체, 나머지인자 등이 Iterator를 소비하게 되는 것입니다. 여기서도 가장 중요한 포인트는 이를 구현하는데 있어 표준이 생겼다는 점입니다. 반복처리를 위임하는데 있어 실질적인 표준적인 값의 타입은 Iterator Protocol타입이라 할 수 있겠죠.
내장 프로토콜 구현체를 넘어 간단히 Iterable을 하나 정의해보죠.

let n2 = {
  [Symbol.iterator](){ //Iterable구현
    var cursor = -1, max = 10;
    return {
      //초기값정의
      done:false, value:0,
      next(){
        if(cursor++ <= max){
          //커서의 여력이 있으면 제곱반환
          this.value = cursor * cursor;
        }else{
          //맥스에 도달하면 정리한다
          this.value = undefined; 
          this.done = true;
        }
        //Iterator호출은 컨텍스트가 유지됨!
        return this;
      }
    };
  }
};

간단히 0~10사이의 수에 대해 제곱을 반환하는 Iterable을 정의했습니다. 이를 간단히 사용해보면 다음과 같습니다.

console.log([...n2]);
//[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

무슨 일이 일어난거죠 ^^;

  1. n2객체는 표준 Iterable프로토콜을 준수합니다. 즉 표준적인 반복에 대한 값입니다.
  2. 펼치기(spread)연산자는 대상이 Iterable이거나 Iterator일 때 작동하는 연산자 입니다.
  3. 우선 n2의 [Symbol.iterator]가 호출되어 내부에 있는 Iterator객체가 반환되고
  4. done == true가 될 때까지 호출되어 펼칠 값을 계산해가게 됩니다.

표준 반복객체를 소비하는 다양한 언어의 내부요소뿐만 아니라 사용자가 반복처리를 소비하는 쪽을 제작할때도 표준에 맞춰 작성하면 됩니다. 예를들어 배열이나 Map, Set 등에는 forEach가 존재하지만 문자열에는 존재하지 않습니다. 문자열도 Iterable Protocol객체라는 점을 생각하면 의아한 레벨입니다. 위에서 제작했던 n2에도 forEach메소드는 누락되어있습니다.
따라서 Iterable 또는 Iterator 객체에 대해 forEach를 일괄로 수행하는 메소드를 생각해볼 수 있을 것입니다.

const forEach = (iter, f)=>{
  //1. 이미 본인게 있으면 그걸로 처리
  if('forEach' in iter) return iter.forEach(f);

  //2. Iterable인 경우 Iterator로 환원한다.
  if(Object.getOwnPropertySymbols(iter).includes(Symbol.terator)){
    iter = iter[Symbol.iterator]();
  }

  //3. Iterator인지 확인한다(상속받아도 괜찮음)
  if(!('next' in iter)) return;

  //4. 반복처리
  let index = 0;
  for(const v of iter) f(v, index++);
};

사용자가 반복처리기를 짤 때도 기존의 i, j와 같은 인덱스 기반이 아니라 Iterator나 Iterable에 대응하도록 작성하게 됩니다. 실제 사용해보죠.

//배열은 본인 메소드로 처리
forEach([1,2,3,4], console.log);

//문자열은 for of로 처리
forEach('abcde', console.log);

(사실 직접 작성한 forEach조차 내부에서 for of를 사용하기 때문에 내부적으로도 여전히 Iterable의 처리를 위임하고 있습니다^^)

반복타입의 값을 보다 쉽게 정의하기

es2015에서는 위와 같은 개념에서 출발한 표준 반복객체의 작성을 도와주는 언어적인 장치를 도입했습니다. 아무래도 모든 경우의 수에 대응하기 위한 Iterator프로토콜을 준수하는 객체를 매번 만드다는 것은 귀찮은 일이기 때문에 귀찮음으로 사용되지 않을 수 있습니다. 보다 손쉽게 반복객체를 작성하기 위한 언어의 새기능은 제네레이터입니다.
위에서 정의했던 n2를 제네레이터로 재정의해보죠.

const generator = function*(max){
  let cursor = -1;
  while(cursor++ < max) yield cursor * cursor;
};
console.log([...geneator(10)]);

같은 내용인데도 불구하고 현격하게 코드량이 줄어듭니다. 객체적인 관점에서는

  1. 컨텍스트상에서의 상태유지를 신경쓰고
  2. 매번 분리된 호출에 대응하는 방식의 알고리즘을 짜야하지만

제네레이터를 로컬어휘환경을 유지한체 yield를 통해 suspend를 처리하게 되므로

  1. 지역변수로 상태를 처리할 수 있고
  2. 연속된 호출에 대응하는 것이 아니라 내부의 로직이 yield시점을 조정하는 것으로

변경됩니다. 이 두가지 변경은 제네레이터 내부에서의 로직을 간단히 하고 상태유지의 개념을 제거하여 현격하게 코드의 양을 줄이게 됩니다.
참고로 실행 중인 EC에 suspend를 걸 수 있는 기능은 es2015를 지원하는 자바스크립트 엔진 차원의 기능이며 이를 활용한 다양한 패턴은 다른 연구노트에서 다룹니다.

결론

이번 노트에서는 명확한 반복처리에 대해서 생각해봤습니다. 반복처리는 전체 코드에서 저수준 구성요소입니다. 하지만 개발자마다의 고유한 스타일로 처리하던 반복문과 패턴은 코드를 어지럽게 만들고 상호 간의 협력을 어렵게 합니다. 저수준코드에 대한 표준안을 제시하고 동시에 이를 문이 아닌 값컨텍스트로 전환한 것은 es2015의 큰 장점이라 할 수 있습니다.
또한 반복객체 대한 언어차원의 표준을 프로토콜로 제시하여 사용자도 자유롭게 작성할 수 있게 하고 이를 소비하는 다양한 처리기도 기본으로 구현해뒀습니다.
es2015에서는 되도록이면 반복을 Iterator로 구현하도록 해야한다고 생각합니다.

제네레이터는 복잡한 내용을 많이 포함하므로 사전지식이 없으신 분들은 아래의 글을 참고하세요.

[es6] Generator #1
[es6for3] generator 대체 #1/2
[es6for3] generator 대체 #2/2

%d 블로거가 이것을 좋아합니다: