es6for3 시리즈
ES6는 새로운 언어철학을 제시하고 편리한 기능을 제공합니다. 하지만 크롬(52버전까지)조차 별도로 플래그를 활성화해야 만 쓸 수 있는 상황입니다.
이에 업계는 바벨 등의 번역기를 이용하여 ES5로 번역시키는 수를 사용하고 있습니다. 결국 바벨이 번역한 코드는 바벨라이브러리를 사용하는 ES5코드로 번역됩니다. ES6에 대한 깊은 이해는 ES6의 코드 방식을 하위 ES3.1로 번역하는걸 가능하게 하죠. ES6 기능에 대한 정확한 이해와 나만의 바벨을 만들기 위한 첫 단계로 ES6의 각 기능을 ES3.1로 번역해보죠.
리듀싱, 파싱, 해체 관계
제작하려는 해체시스템은 크게 리듀싱(reducing), 파싱(parsing), 해체(destructuring)로 처리됩니다.
- 리듀싱의 목적은 재귀적인 해석을 위해 문자열을 1차적으로 분리하여 배열과 오브젝트용 2차원 배열에 단계별로 저장하는 것이었습니다.
- 이제 이를 이용하여 파싱과정을 거치는데 파싱 후에는 파싱객체가 생성되고 이 객체를 이용하여 이후 다양한 값을 해석할 수 있게 됩니다.
- 마지막으로 해체는 런타임에 받아들일 대상객체를 파싱객체로 해석하여 값을 할당하는 행위입니다.
파싱과 해체가 분리되어있는 이유는 파싱객체은 한번만 만들면 되고 이를 이용하여 다수의 타겟을 해체하기 때문입니다.
리듀싱은 #1/2에서 이미 다뤘습니다. 이제 파싱객체를 생성하기 위한 파서와 값객체를 제작해보죠.
재귀적인 파서의 제작
해체시 너무 깊은 뎁스의 해체는 하지 않을거라는 전재를 깔았습니다. 왜냐면 너무 깊은 구조를 해체로 얻는다는 행위 자체가 인간에겐 좀 무리이기 이기 때문입니다. 해서 파서가 재귀적으로 작성되어도 큰 문제가 없을거란 판단하에 재귀형 파서로 제작하기로 했습니다.
우선 파서는
- 리듀싱된 문자열과 파싱결과를 담을 객체를 인자로 받아들이고
- 인자로 전달된 문자열로부터 원본 문자열을 복원한 뒤
- 이 문자열이 오브젝트나 배열인 경우 점진적으로 해석하고
- 기본값인 경우는 변수객체를 생성하여 반환합니다.
위의 과정에서 객체나 배열을 해석하다보면 해당 엘리먼트가 다시 객체이거나 배열인 경우가 발생하는데 이 때는 이를 담을 새로운 객체와 함께 파서를 재호출하여 위임하게 됩니다. 복잡한 설명은 이 정도로 하고 코드를 보죠.
var parser = function(str, result){ var origin, isObj; //1편에서 제작한 getData함수로부터 원본문자열을 얻음 origin = getData(str); //1. 원본문자열이 있으면 객체형이다! if(origin){ //오브젝트인지 객체인지 판별 isObj = v.charAt(0) == '{' ? 1 : 0; //2. 양쪽의 {}나 []를 제거하고 컴마로 나눈다. v.substring(1, v.length - 1).split(',').forEach(function(str, idx){ var parsed; str = str.trim(); //오브젝트인 경우 if(isObj){ //:이 있으면 k:v형태고 없으면 {k,k} 형태이므로 적절히 조치 p = str.indexOf(':'); str = p > -1 ? [str.substring(0, p), str.substr(p + 1)] : [v, v]; //3. 최종결과물은 [해체할키, 할당될 변수명] 형태 //4. 변수명측이 추가로 해체될 수 있기 때문에 재귀적으로 parser를 호출 //이를 result의 키명으로 넣어줌. if(parsed = parse(v[1], {})) result[str[0].trim()] = parsed; //5. 배열인 경우는 idx만 중요하므로 해당원소만 재귀처리후 할당 }else if(parsed = parse(str, {})) result[idx] = parsed; }); return result; }else{ //6. 아니면 기본형이니 변수객체를 생성하자. return str ? new Var(str) : undefined; } };
위의 흐름에서 최초 해체문자열이 다음과 같다고 생각해보죠.
{ items:[{title, link}], meta:{url,date,author} }
이는 리듀싱을 통해 다음과 같이 처리될 것입니다.
obj = [ ['{title, link}', '{url,date,author}'], ['{items:@arr_0_0@,meta:@obj_0_1@}'] ]; arr = [ ['[@obj_0_0@]'] ]; str = '@obj_1_0';
일단 이렇게 리듀싱되는게 이해되지 않으시면 다시 #1/2를 읽어보시길 추천해드립니다.
이제 파서를 호출하면 어떤일이 일어날지 추적해보죠.
var result = parser('@obj_1_0', {});
파싱결과를 담을 빈객체와 최종 리듀싱이 끝난 문자열을 보내 parser를 호출했습니다. 파서의 로직에 따라 위의 코드에 붙여진 번호대로 실행됩니다.
- getData로 값을 얻고(결과 = ‘{items:@arr_0_0@,meta:@obj_0_2@}’)
- 컴마로 분리한 요소를 순회한다.(분리된 요소 [‘items:@arr_0_0@’, ‘meta:@obj_0_2@’])
- 오브젝트 형이므로 :을 기준으로 분리하여 배열을 생성(각각 [‘items’, ‘@arr_0_0@’], [‘meta’, ‘@obj_0_2@’])
- 이를 다시 파서에게 새 객체와 함께 보내게 됨
이게 최초 일어나는 일입니다. 이제 재귀단계에 들어서면
parser('@arr_0_0@', {}) //1 parser('@obj_0_2@', {}) //2
4단계에서 이렇게 두번의 재귀호출을 처리하게 됩니다. 각각을 추적해보죠. 먼저 1번 호출을 보면 다시 코드 단계에 맞게 나온 결과만 표시해보겠습니다.
- origin = ‘[@obj_0_0@]’
- 순회 = [‘@obj_0_0@’]
- 다시 parser(‘@obj_0_0@’, {}) 재귀처리
이 단계에서 발생한 재귀는 1_1 로 명명하겠습니다. 1_1을 추적해보죠.
- origin = ‘{title, link}’
- 순회 = [‘title’, ‘link’]
- [‘title’, ‘title’], [‘link’, ‘link’] 로 분리
- 다시 parser(‘title’,{}), parser(‘link’,{}) 두개의 재귀처리
이제 막바지입니다. 위에서 도출된 두개의 재귀는 1_1_1, 1_1_2 로 명명하고 이중 1_1_1을 추적해보죠.
- origin = undefined
- 곧장 6으로 와서 str이 존재하므로 new Var(‘title’)을 반환
드디어 1_1_1에서 반환이 되기 시작합니다. 이를 수용한 1_1은
parsed = new Var(‘title’) 이 되고 result[‘title’] = new Var(‘title’) 이 됩니다.
이를 다시 수용하게될 1번 단계로 돌아가면
parsed = {title:new Var(‘title’)} 이 되어 result[0] = {title:new Var(‘title’)} 로 수렴하게 되죠.
이러한 재귀의 결과로 최종 결과물은 다음과 같이 처리됩니다.
result = { items:{ 0:{title:new Var('title'), link:new Var('link')} } meta:{ url:new Var('url'), date:new Var('date'), author:new Var('author') } };
변수를 대신할 변수객체
변수객체는 해체시 파싱객체를 순회하다가 형을 확인하여 값을 반환하기 위한 용도로 사용하는 마커입니다. 그다지 하는게 없으니 간단하게 작성합니다.
var Var = function(str){ this.str = str; }; Var.prototype.toString = function(){ return this.str; };
그야말로 해체시 instanceof Var를 위한 마커역할을 수행하고 있습니다. 이후 이는 다시 기본값을 처리하기 위한 기능을 부여하게 됩니다만, 우선은 넘어가죠.
해체
리듀싱과 파싱을 통해 결국 위에서 result에 해당되는 파싱객체를 손에 넣었습니다.
이 파싱객체를 이용하여 값을 해체한 뒤 해체된 결과를 객체에 담아 보내는 과정을 작성해보죠.
var destructuring = function(parsed, target, result){ var k, key, i, j; //파싱객체를 순회 for(k in parsed) if(parsed.hasOwnProperty(k)){ key = target[k]; //현재키의 값이 변수객체인 경우 if(key instanceof Var){ //target객체로부터 값을 얻는다. result[key] = target[k]; //아닌 경우 재귀적으로 넘겨버린다. }else if(key && typeof key == 'object') destructuring(key, v[k], result); } };
먼가 굉장히 짧은 편입니다.
- 우선 해체는 해체식이 주인공이므로 루프의 기준은 파싱객체입니다.
- 파싱객체의 각 키를 순회하며 변수객체인 경우는 타겟의 값을 넣어주면 되고
- 중첩된 경우는 다시 재귀로 호출하면 그만입니다.
이에 따라 어떤 타겟객체를 실체로 해체해가는 과정을 따라가보죠.
var target = {//대상객체 items:[ {title:'menu1', link:'1.html'} ], meta:{ url:'/menu/', date:'2016/08/03', author:'hika' } }; var parsed = { //파싱객체 items:{ 0:{title:new Var('title'), link:new Var('link')} } meta:{ url:new Var('url'), date:new Var('date'), author:new Var('author') } }; //해체결과 var result = destructuring(parsed, target, {});
최초 실행은 다음과 같이 됩니다.
- 우선 parsed를 for in 으로 순회하니까 items와 meta가 걸려나올 것입니다.
- 이 둘의 값이 다 Object로 Var의 인스턴스가 아니니 재귀호출을 일으킵니다.
2의 과정에서 나온 재귀를 각각 1(items), 2(meta) 로 나눠보죠. 이중 1번을 추적해봅시다.
destructuring( { //items에 해당되는 부분 0:{title:new Var('title'), link:new Var('link')} }, [ //target의 해당되는 부분 {title:'menu1', link:'1.html'} ], result //최초 받은 result그대로. );
이렇게 처리됩니다. 다시 for in을 돌게 되면 0에 대한 재귀가 발생하게 됩니다. 0번을 추적해보죠.
destructuring( //0에 해당되는 부분 {title:new Var('title'), link:new Var('link')}, //target의 해당되는 부분 {title:'menu1', link:'1.html'}, result //최초 받은 result그대로. );
이 단계에서의 for in은 title, link이고 이는 각각 Var의 인스턴스이므로 이때 해소됩니다.
result['title'] = 'menu1'; result['link'] = '1.html';
재귀지만 반환할 것은 없습니다. result에 계속 1차원적으로 키를 써가면 되는 것이 해체입니다.
이렇게 메타도 다 해체하고 나면 결과는 다음과 같아집니다.
result = { title:'menu1', link:'1.html', url:'/menu/', date:'2016/08/03', author:'hika' };
이상의 과정을 통해 result에는 해체된 결과가 키별로 들어가게 됩니다.
결론
1, 2편에 이어서 리듀싱, 파싱, 해체 과정을 부분으로 살펴봤습니다. 여태 기술한 코드는 기초 구현물로
- 기본값과
- …파싱
- 이터레이터에 대한 처리
가 들어있지 않았습니다. Iterator에 대한 폴리필은 Symbol을 폴리필해야하고 ForOf에 대한 폴리필도 연쇄적으로 요구하게 됩니다. 물론 이를 다 다룰 여정입니다만, 우선 그러한 요소가 있다고 가정한 상태의 전체 코드를 보겠습니다.
var Dest = (function(){ var DEST = Symb(), DEFAULT = Symb(), pool = {}, getVal, getData, parse, destructuring, Var, Dest, arr = [], obj = [], a, o, ad, od, at, ot, rObj = /\{[^\{\[\]\}]*\}/g, rArr = /\[[^\{\[\]\}]*\]/g, oR = function(v){return ot[o] = v, '@o_'+ od +'_' + (o++) + '@';}, aR = function(v){return at[a] = v, '@a_'+ ad +'_' + (a++) + '@';}, rO = /@o_[^@]+@/g, rA = /@a_[^@]+@/g, rR = /^@o_[^@]+@|@a_[^@]+@$/, rNum = /^[-]?[.0-9]+$/, rStr = /^('[^']*'|"[^"]*")$/, primi = {'true':true, 'false':false, 'null':null}; getData = function(d){ var target = d.search(rO) > -1 ? obj : d.search(rA) > -1 ? arr : 0; if(target) return d = d.substring(1, d.length - 1).split('_'), target[d[1]][d[2]]; return false; }; getVal = function(d){ //기본값 처리를 위한 값인식기 var target = d.search(rO) > -1 ? obj : d.search(rA) > -1 ? arr : 0; if(target) return d = d.substring(1, d.length - 1).split('_'), JSON.parse(target[d[1]][d[2]]); else if(d.search(rStr) > -1) return d.substring(1, d.length - 1); else if(d.search(rNum) > -1) return parseFloat(d); else if(d = primi[d]) return d; }; Dest = function(dest){ var loop, r = this[DEST] = {}; arr.length = obj.length = a = o = ad = od = 0, dest = dest.trim(); do{ loop = 0; if(dest.search(rObj) > -1) obj[od] = ot = [], dest = dest.replace(rObj, oR), od++, loop = 1; if(dest.search(rArr) > -1) arr[ad] = at = [], dest = dest.replace(rArr, aR), ad++, loop = 1; }while(loop); //전체 해체수준의 기본값처리 if(dest.indexOf('=') > -1) dest = dest.split('='), r[DEFAULT] = getVal(dest[1].trim()), dest = dest[0].trim(); if(dest.search(rR) == -1) throw 'invalid destructuring'; parse(dest, r); }; Dest.prototype.value = function(v){ var result = {}; destructuring(this[DEST], v === undefined ? this[DEST][DEFAULT] : v, result); return result; }); Var = function(k){ //변수객체별 기본값처리 var i = k.indexOf('='); if(i > -1) this[DEFAULT] = getVal(k.substr(i + 1).trim()), k = k.substring(0, i).trim(); this.k = k; }; Var.prototype.toString = function(){return this.k;}; parse = function(dest, r){ var v, isObj; dest = dest.trim(); if(v = getData(dest)){ isObj = v.charAt(0) == '{' ? 1 : 0; v.substring(1, v.length - 1).split(',').forEach(function(v, idx){ var p; v = v.trim(); if(isObj){ p = v.indexOf(':'); v = p > -1 ? [v.substring(0, p), v.substr(p + 1)] : [v, v]; if(p = parse(v[1], {})) r[v[0].trim()] = p; }else if(p = parse(v, {})) r[idx] = p; }); return r; }else return dest ? new Var(dest) : undefined; }; destructuring = function(target, v, result){ var k, key, iter, iterR, i, j; //이터러블해소 if(iter = v){ while(typeof iter[Symb.iterator] == 'function') iter = iter[Symb.iterator](); if(typeof iter.next == 'function') iterR = []; } for(k in target){ if(target.hasOwnProperty(k)){ key = target[k]; if(key instanceof Var){ //...해소 if(key.k.substr(0, 3) == '...'){ i = key.k.substr(3); if(iterR){//이터러블의 경우 처리 For.Of(iter, function(v){iterR.push(v);}); result[i] = iterR.slice(k); }else if(v instanceof Array) result[i] = v.slice(k); else throw 'invalid Array'; }else{ if(parseInt(k, 10) + '' == k){//배열인 경우 if(!(v instanceof Array) && iterR){ //이터러블처리 k = parseInt(k, 10); while(iterR.length - 1 < k){ j = iter.next(); if(j.done) break; iterR.push(j.value); } result[key] = iterR[k] === undefined ? key[DEFAULT] : iterR[k]; continue; } } result[key] = v[k] === undefined ? key[DEFAULT] : v[k]; } }else if(key && typeof key == 'object') destructuring(key, v[k], result); } } }; //Dest의 팩토리형 함수를 노출함 return function(dest, v){ return (pool[dest] || (pool[dest] = new Dest(dest))).value(v); }; })();
코드가 좀 긴 편이라 주석을 참고하시고 간단하게 보자면 외부에는 팩토리함수를 노출하고 내부에서는 Dest클래스를 이용하는 식입니다.
이제 사용을 간단히 할 수 있죠.
var result = Dest('[a,b,...c]', [1,2,3,4,5]); console.log(result.a, result.b, result.c); // 1, 2, [3,4,5]
구동되는 샘플과 단위 테스트는 여기에 있습니다.
recent comment