[es6for3] destructuring 대체 #1/2

es6for3 시리즈top

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

개요top

해체는 ES6도입된 편의기능입니다. 편의기능이긴 하지만 이를 통해 개념적으로 변수가 값에 매핑되는 것을 시각적으로 표현할 수 있고 반복문 등에서도 효과적으로 역할을 분리할 수 있습니다.
해체는 좌변이 배열해체식이거나 객체해체식이 되어 값에 해당되는 부분에 변수명을 넣으면 우변의 해당 위치의 값이 할당되는 형태입니다. 가장 쉽게는 다음과 같습니다.

let [a,b,c] = [1,2,3];
let {a,b,c} = {a:1, b:2, c:3};

위의 설명대로 좌변에서 평범한 배열이나 오브젝트 리터럴처럼 기술하되 값이 들어갈 자리에 변수가 들어가 있습니다. 이는 우변의 실 객체로부터 해당 위치의 값을 받는다는 의미입니다.
결국 해체는 “값자리에 변수를 넣는다” 라는 간단한 규칙만 기억하면 그리 외계어처럼 보이지 않겠죠 ^^;
여기에 몇 가지 추가 사항이 있습니다.

  1. 우변 값이 undefined라면 기본값이 할당된다.
  2. 해체에서 …연산을 사용하면 이후 모든 엘리먼트가 합쳐진 배열이 할당된다.
  3. 복합키에서 부모가 없는데 하위키를 참조하려 들면 throw가 난다.

위의 세가지를 코드로 표현해보죠.

//1. a에 undefined이므로 기본값 1이 되고, c는 없는 원소라 undefined이므로 기본값 3이 됨.
let [a = 1, b, c = 3] = [undefined, 2]; //a=1, b=2, c=3

//2. ...b이므로 남은 요소인 2, 3을 갖는 배열이 됨
let [a, ...b] = [1, 2, 3]; //a=1, b = [2,3]

//3. 우변에 data에 해당되는 값이 없으므로 undefined에 a키를 조회시도해 throw가 됨.
let {data:{a}, b} = {b:3}; //throw

이 정도면 기본은 익힌 셈입니다. 사실 저기엔 더욱 디테일이 숨어있습니다만(2번의 …b 만해도 원소가 하나도 없는 경우에도 반드시 빈 배열은 된다던가라는 식으로) 차근차근 구현해가면서 살펴보도록 하죠.

구현 방향top

해체는 굉장히 편리한 기능입니다만 기존 es5까지 이를 완벽히 흉내낼 방법은 없습니다. 해서 해체문법을 지원하여 해체가 완료된 객체를 반환하는 식으로 우회구현하는 방안을 생각해봤습니다.

var result = Dest('[a = 1, b, c = 3]', [undefined, 2]);
console.log(result.a, result.b, result.c);//1,2,3

이런 정도가 되겠습니다. 마침 직전에 다뤘던 템플릿에서 함수의 toString을 이용하는 방안도 강구해뒀으니 복합적인 해체시 도움을 받을 수 있을 겁니다.

var menus = Dest(function(){/*
{
  menus:[
      {title0, link0, color0, selected0},
      {title1, link1, color1, selected1},
      {title2, link2, color2, selected2}
  ]
}
*/, target);
for(i = 0; i < 3; i++){
  menu = document.getElementById('menu' + i);
  menu.innerHTML = menus['title' + i];
  menu.href = menus['link' + i];
  menu.style.color = menus['color' + i];
  menu.style.backgroundColor = menus['selected' + i];
}

완성된 해체처리기는 위와 같이 사용될 수 있을 것입니다. 또한 이후 구축할 ForOf에서도 역할을 잘 수행해내겠죠.

문자열 파싱전략top

일반적으로 문자열을 의미있는 집합으로 인식하는 과정은 굉장히 기계적이고 체계적입니다. 토크나이저를 구현하고 AST를 구축한뒤 구문트리를 소비해가며 처리하는 식입니다.
헌데 제 경우 이 전략을 자바스크립트에서 왠만하면 안쓰려고 합니다. AST는 단순한 파서일지라도 200줄이상이 넘어가고 복잡한 객체모델이 필요합니다. 루프를 최대한 억제하고 언어 내장기능으로 처리할 수 있다면 그 편이 스크립트언어에게는 유리하기 때문에 정규식으로 리듀싱했다가 풀어주는 방식으로 구현하겠습니다.
이 과정을 알아보기 위해 실제 파싱해야할 문자열이 어떤식으로 변경되었다가 풀어지는지를 살펴보죠.

우선 실제로 파싱할 문자열입니다.

{
   items:[{title, link}, {title, link}],
   meta:{url,date,author}
}

우선 리듀싱 전략이란 더이상 파싱할 객체가 없을 때까지 대체해가면서 변경하는 과정입니다. 우리가 재귀적으로 파싱해야하는 부분은 오브젝트리터럴과 배열리터럴이므로 이를 계속 안쪽부터 특수한 키로 대체해가면 됩니다.
일단 내부에 전혀 객체나 배열을 포함하지 않은 1차 대상을 찾아서 문자열로 대체하는데 이때 고려할 사항이 레벨이 얼마고 그 레벨 내에서 몇번째인가라는 점입니다.
레벨이란 한 번에 다시 풀어줄 대상끼리 묶은 것을 말합니다. 예를들어 중첩된 객체가 있을때 전혀 중첩되지 않은 객체들이 레벨1이 되고 하나를 중첩한 부모는 레벨2가 되는 식입니다. 같은 레벨1안에서도 여러개가 존재할 수 있으므로 그들을 구분하기 위한 아이디를 부여해야합니다.

각설하고 그렇게 첫번째 리듀싱을 수행해보죠.

{
   items:[@obj_0_0@, @obj_0_1@],
   meta:@obj_0_2@
}

위에서 내부에 배열이나 객체를 갖고 있지 않은 레벨0대상자인 객체 세개가 각각 @obj_0_0@, @obj_0_1@, @obj_0_2@로 치환된 것을 볼 수 있습니다. @obj는 이것이 오브젝트를 치환했음을 나타내고 0_0 은 레벨_ID를 나타냅니다. 이를 치환할때 여기에 상응하는 배열도 당연히 존재해야합니다.

var obj = [
  ['{title, link}', '{title, link}', '{url,date,author}']
];

치환과 동시에 레벨별 ID별 배열에 원본 문자열을 담아둠으로서 후에 펼칠때 사용할 수 있게 됩니다. 예를들어 ‘@obj_0_1@’ 로부터

function getData(key){
  //key = '@obj_0_1@'
  key = key.substring(1, key.length - 1); //양쪽 @제거
  key = key.split('_');
  switch(key[0]){
  case'obj':return obj[key[1]][key[2]];
  case'arr':return arr[key[1]][key[2]];
  }
}

이런 과정으로 원래 문자열을 찾아서 복원할 수 있을 것입니다.

일단 1차 리듀싱이 끝났으니 이어서 2차 리듀싱을 반복적으로 적용합니다.

{
   items:@arr_0_0@,
   meta:@obj_0_2@
}

이번 루프에서는 items의 배열부분만 대체되었습니다. 마지막으로 다시 리듀싱을 돌리면 최종적으로

@obj_1_0@

로 떨어지게 됩니다. 어떤가요 토크나이저보다 훨씬 재빨리 리듀싱과정을 정리하면서 구문트리 대신에 obj와 arr의 이차원배열을 생성하는 걸로 마무리 지을 수 있는 구조입니다.

리듀싱 구현top

위에서 설명한 전략을 구축하기 위해 간단히 알고리즘과 정규식을 작성해보죠.
우선 리듀싱단계에서의 문자열을 저장하기 위해 이차원배열이 두개 필요할테고, 레벨을 관리할 변수와 id를 관리할 변수가 필요할 것입니다.
또한 정규식과 이를 처리할 replace용 함수도 빼놓을 수 없죠.

reducer = function(str){
  var isEnd; //더이상 리듀스할게 없는지 있는지 판단
  var arr = [], obj = []; //중간산출물을 저장할 2차원배열
  var arrLv = 0, objLv = 0; //리듀싱레벨을 관리할 변수
  var arrID = 0, objID = 0; //레벨별 아이디를 관리할 변수
  
  //오브젝트인 {로 시작하되 내부에 [,{가 없는 경우
  var reObj = /\{[^\{\[\]\}]*\}/g;

  //배열 [로 시작하되 내부에 [,{가 없는 경우
  var reArr = /\[[^\{\[\]\}]*\]/g;
  
  do{
    isEnd = true; //일단 완료라 하고 중간에 변경이 일어나면 true로

    //object부터 처리
    if(str.search(reObj)){ 
      obj[objLv] = []; //해당레벨의 배열을 만듬
      str = str.replace(reObj, function(v){
        obj[objLv][objID] = v; //2차원배열에 원본을 넣어줌
        var result = ['@obj', objLv, objID].join('_'); //치환
        objID++; //id는 증가됨
        return result;
      });
      objLv++; //리듀싱이 끝나 lv가 상승함
      isEnd = false; //아직 끝이 아님 ^^
    }
    //array처리, 상동
    if(str.search(reArr)){ 
      arr[arrLv] = [];
      str = str.replace(reArr, function(v){
        arr[arrLv][arrID] = v;
        var result = ['@arr', arrLv, arrID].join('_');
        arrID++;
        return result;
      });
      arrLv++;
      isEnd = false;
    }
  }while(!isEnd);//끝이 아니라면 계속

  return str;//최종 리듀싱완료
};

헐퀴, 걍 리듀싱만 하는데도 글이 길어지고 있군요. 이제 파서함수를 작성할 차례입니다. ㄷㄷㄷ

결론top

한 번에 다 써버릴까 하다가 내용이 너무 길어지는 감이 있어 1,2부로 나누게 되었습니다. 다음 편에서는 파서의 의미와 작성을 다룬 뒤 실제 파서를 통해 얻게된 객체로 target을 매번 해체할 수 있게 합니다. 또한 구축을 위해 중간 산출물인 값객체나 기본값처리기, …처리기도 동시에 다루게 됩니다.