블로그 내 검색

2014. 11. 19.

EcmaScript 6 new Features

시작하며

아직 프로그래머들에겐 EcmaScript5 도 익숙하지 않는 판국에, EcmaScript6 버전 (이하 ES6)을 사용하는 노력들이 여기저기서 보인다.

몇몇의 오픈소스들은 이미 ES6 을 사용한 버전을 저장소에 올려두기도 했으며, ES6의 매력적인 일부분을 EC5에서 에뮬레이팅 하는 프레임워크도 나오고 있다
(Co, Koa 등등...)

예전에 EcmaScript5의 기능을 소개해본적 이 있는데, 이번에도 한번 정리해보려고 글을 쓰기 시작했다. 쉽지 않다...

실전 활용은 전혀 무리지만, 그래도 이런거구나 하는 마음으로 새로운 기능을 몇가지만 살펴보자

선언, 할당

변수 선언  방법이 다양해졌다
이러한 방법으로 변수 선언이 가능하다.
var [a, b, c] = [1, 2, 3];
console.log(a + c) // 4

var [ fullMatchNumber, firstNumber , middleNumber, lastNumber ] = 
    "010-8686-3131".match(/^(\d{2,3})-(\d{3,4})-(\d{4})$/);
console.log(middleNumber); // "8686"
객체 리터럴 선언 시 귀찮은 키와 값 매핑을 좀 더 간단히 해결할 수 있게 됐다
var honkong = "홍콩", china = "중국";
var country = { honkong, china };
console.log(country.china); // "중국"
객체의 키를 사용한 선언도 가능하다.
var Cat = {
    name: "Kitty",
    age: 3
};
var { name, age } = Cat;
console.log(name); // "Kitty"
서버에서 가져온 고객 데이터 중 세번째 고객의 첫번째 주문항목과 고객의 이름을 변수로 한번에 선언하고 싶다고 한다면
var fromServerData = [
    { 
        name: "pooh", 
        orderList: [ '신발', '구두', '김치'   ] 
    },
    { 
        name: "hunky", 
        orderList: [   '코트'   ] 
    },
    { 
        name: "pang", 
        orderList: [  '가글', '시계'  ] 
    },
    { 
        name: "jj", 
        orderList: [ '자전거', '셔츠' ] 
    }
]
다음과 같이 간단히 쓸 수 있다. (유용한지는 모르겠다...)
// 콤마를 사용해서 앞선 두 인덱스를 건너뛰고 그 뒤부터 변수를 선언해준다.
var [ , , { name: vipName, orderList: [firstOrderName] }] = fromServerData;
console.log(vipName) // "pang"
console.log(firstOrderName) // "가글"
물론 함수 인자에도 위에서 소개한 변수 선언법을 적용해볼 수 있다.
함수 인자도 결국 함수의 변수 선언이나 마찬가지니...
function introduce({ firstName, lastName }) {
    return lastName + ", " + firstName;
};
var ironMan = {
    firstName: "스타크",
    lastName: "토니"
}
console.log(introduce(ironMan)) // "토니, 스타크"

함수 기본값

예전에는 함수로 전달되는 인자에는 기본값을 줄 수 없어서 항상 함수 바디에서 인자에 값이 있는지 판단하고, 없다면 대체하는 코드가 필수로 들어가야 했지만, ES6에서는 함수 인자부분에 기본값을 설정할 수 있다~!!
function Rectangle(width=100, height=100) {
    this.width = width;
    this.height = height;
};
console.log(new Rectangle().width) // 100

가변 변수 (...rest)

자바등에서 즐겨 쓰이는 가변변수 가 더욱 유용하게 추가되었다.
function vargs(a, ...b) { 
    console.log(Array.isArray(b)); // 자바와 같이 가변 부분은 배열이다.
    b.push(a);
    return b;
}

var ret = vargs(1,2,3,4,5);
console.log(ret); // "2,3,4,5,1"
배열의 요소로 활용할 수도 있는 등, 유연하다.
var a = [1,2];
var b = [3,4, ...a];
console.log(b) // 3,4,1,2

let

자바스크립트는 언어 자체가 블럭 스코프가 존재하지 않는다.
하지만 가끔 블럭 스코프 등으로 스코프를 제한해야 할 때가 있는데, 그 때는 익명 함수를 사용하여 (여러 문제가 있는) 문제를 우회했지만 ES6에서는 굉장히 간단해졌다.
// ES5. 함수 스코프란 이렇다...
var ret = [];
for(var i = 0; i < 10; i++) {
    var myIndex = i;
    ret.push(function(){ return myIndex; })
}
ret[0](); // 9
ret[1](); // 9
ret[2](); // 9
ret[9](); // 9
함수 스코프와 그에 따른 클로저로 인해 배열의 모든 함수는 같은 스코프 체인을 생성하게 되어 다들 같은 마지막 루프의 인덱스 값만 반환하는 상황이 나온다.

클로저의 예제로 많이 나오는 예제라 몇몇사람은 친숙할 것이다.

물론 이것을 해결하는 방법도 함수 스코프를 사용하는 방법이다.
var ret = [];
for(var i = 0; i < 10; i++) {
    
    // Scope 를 억지로 생성, 정말 억지스럽다...
    (function() {
        var myIndex = i;
        ret.push(function(){ return myIndex; });
    })();
}
ret[0](); // 0
ret[1](); // 1
ret[2](); // 2
ret[9](); // 9
let 키워드를 사용하면 저런 삽질이 필요없다. 스코프가 블럭으로 제한되게 된다.
// ES6
var ret = [];
for(var i = 0; i < 10; i++) {

    // var 대신 let 으로 변수를 선언한다
    let myIndex = i;
    ret.push(function(){ return myIndex; })
}
ret[0](); // 0
ret[1](); // 1
ret[2](); // 2
ret[9](); // 9

FOR-OF 

for-of 로 루프가 간단해졌다. (정말?)
var arr = [ 1, 2, 3, 4, 5, 6 ];
[ for(x of arr) console.log (x) ] // 6번 루프를 돌며 배열 요소를 하나씩 출력한다.
for - of 구문은 실행 결과로 루프당 실행한 표현식의 결과를 반환하는데, 위의 예제의 경우 console.log 함수는 아무것도 반환하지 않으므로 undefined 로 채워진 길이 6의 배열을 반환하게 될 것이다.

조금 응용하여 다음과 같이 조건부로 반환하게 할 수 있다. 3 이상의 숫자만을 배열로 반환시켜보자
var arr = [ 1, 2, 3, 4, 5, 6 ];
var filtered = [ for(x of arr) if(x > 3) x ];
console.log(filtered); // 4, 5, 6
다중 루프도 해볼 수 있다.
var arr1 = ["a", "b" ], arr2 = [ "A", "B" ];
var ret = [ for (i of arr1) for (j of arr2) i+j ];

console.log(ret); // "aA, aB, bA, bB"
[] 괄호로 내부의 for - of 를 감싸주면 결과가 다르다.

내부 루프의 결과가 배열([]) 로 전달되기 때문이다.
var ret = [ for (i of arr1) [ for (j of arr2) i+j ] ];

console.log(ret); // "[[aA, aB], [bA, bB]]"

클래스

스펙 상 class 키워드로 클래스를 생성해볼 수 있으나 아직 FireFox, Chrome 에 탑재된 엔진에서도 클래스 지원은 하고 있지 않다. (Feature Not Yet Supported) 규약에 쓰인 내용을 간추려 코드로 써보면 이렇다.
class Car {

    // 생성자.
    constroctor(name) {
        this.name = name;
    }

    drive() {
        return "자동차를 운전합니다";
    }
}

class Truck extends Car {

    constroctor(name) {

        // 생성자 체이닝
        super(name);
    }
        
    drive() {
        // 부모의 drive 호출
        var d = super();
        return "무섭게 " + d;
    }
}

var truck = new Truck("포터");
truck.drive(); // "무섭게 자동차를 운전합니다"

Generator

제네레이터는 함수의 동작 중 순간순간을 정지하거나(suspend) 다시 동작(next)시킬 수 있는 순차 실행기 (Iterator) 를 반환하는 함수이다.

제네레이터 생성 문법에는 function *(){} 와 같이 asterisk(*) 를 써야 한다. 중단점에는 yield 키워드를 사용한다.
function* sequencer() {
    var i = 0;
    while(true) {
        yield i++;
    }
}
var gen = sequencer();
gen.next(); // Object { value: 0, done: false }  
gen.next(); // Object { value: 1, done: false } 
...
제네레이터 함수를 실행하면 이터레이터(Iterator)가 반환된다.

이터레이터의 next() 메서드를 실행할 때마다 함수가 실행되며, value와 done 프로퍼티를 가지는 객체를 반환한다. value 는 yield의 표현식이 할당되며 done 속성으로 next 가 가능한지 여부를 알 수 있다.

제네레이터는 비동기 작업과 결합할 경우 굉장히 강력한 능력을 낸다.
혹시 C# 을 해본 사람이라면 await 키워드를 사용한 비동기 프로그래밍과 약간(..) 비슷하다고 생각될 것이다.
function work(gen) {
    var ret;
    while(!ret.done) {
        ret = gen.next();
    }
    return ret.value;
}
function *asyncJob() {

   var input = yield getJSON("/some.json");
   return yield getJSON("/thing.json", input);
}
var asyncGen = work(asyncJob());
위의 구문이 별거 아닌 것처럼 느껴지지만, 만일 예외 처리가 섞인다면 이야기가 달라진다. 일반적인 비동기의 예외 처리는 콜백을 동반하기에 굉장히 어렵다. 하지만 제네레이터를 사용하게 되면 다음과 같이 동기식 코드를 사용하는 것처럼 단순해진다.
function *asyncJob() {
   
    // 단순히 그냥 try - catch.
    try {
        var input = yield getJSON("/some.json");
        return yield getJSON("/thing.json", input);
    }
    catch(ex) {
        return handleError(ex);
    }
}

이 외에도...

이 외에도 Promise가 정식으로 합류(?)했으며, Arrow Function, Module 등등 여러 새로운 기능들이 있지만 다 소개하기엔 블로그 스크롤과 읽는 사람의 인내심이 무한히 길어지고 한계가 올 것 같다. (글 쓰는 사람도 한계다...)

기회가 된다면 다음 포스팅에...

위의 예제들은 클래스를 제외하고는 전부 Firefox, Chrome  Console에서 실행해볼 수 있다.
크롬은 대신 주소창에 chrome://flags 을 타이핑해서 실험실에 들어간 뒤 "enable-javascript-harmony" 기능을 활성화시켜줘야 할 수 있다.

2014. 9. 15.

JavaScript Module Injector 만들기

JavaScript에서 DI를...

최근 AngularJS 에 관심이 많아서 여러모로 살펴보는 중인데, 그 중에서도 재미있게 본 것은 Dependecy Injection 을 JavaScript 레벨에서 지원해준다는 것이었다.

Java 등에서 쓰이는 Spring Framework에서는 ApplicationContext 에 빈을 등록해두면 특정 애노테이션을 확인하여 DI 해주는 방식으로 진행되지만 JavaScript에서는 Annotation 같은 것이 없고 (비슷하게 구현해볼 수는 있지만 낭비...) 다른 방법으로 구현해야 한다.

비결은 Function.prototype.toString에 있었다.

Function.toString

JavaScript의 함수는 toString을 할 경우 함수의 소스코드를 문자열로 반환한다.
function imFunction(you, say, ho) {
    console.log(you, say, ho);
}

document.getElementById('result').innerHTML = imFunction.toString();

jsFiddle
http://jsfiddle.net/javarouka/5wk4sofh/

여기서 중요한 것은 함수의 인자 목록도 문자열에 포함되어 있다는 것이다.
이걸 활용하면, DI를 흉내내볼 수 있다.

구현시작...

먼저 정규식이 필요하다
함수의 toString 결과를 함수의 이름, 인자, 몸체.이 셋으로 나눠볼 정규식을 만들어보자.

 (함수 몸체와 이름은 일단 쓸일이 없지만 후 확장을 위해 한번에 구해봤다..)
var FN_PARSE = /^function\s*(\S+)[^\(]*\(\s*([^\)]*)\)\s*\{([\W\w]+)\}$/m

위 정규식으로 match 할 경우 [toString 결과, 함수 이름, 함수 인자, 함수 몸체] 의 배열을 얻을 수 있다.
주의할 점이, 자바스크립트는 함수 인자 목록에도 주석을 사용할 수 있기에 자칫 주석으로 인자 이름을 잘못 가져올 수 있다.
주석을 제거하는 정규표현식도 준비한다.
var STRIP_COMMENT = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg

그렇다면 적당한 함수를 하나 준비해본다
function hello(man, to, women) {
    console.log(man + to + women);
}

파싱해보자.
var FN_PARSE = /^function\s*(\S+)[^\(]*\(\s*([^\)]*)\)\s*\{([\W\w]+)\}$/m,
    STRIP_COMMENT = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function hello(man, to, women) {
    console.log(man + to + women);
}
var parsed = hello.toString().match(FN_PARSE),
    fnName = parsed[1],
    fnArgs = parsed[2].replace(STRIP_COMMENT, '').split(',')
    fnBody = parsed[3];

잘 된다!

jsFiddle
http://jsfiddle.net/javarouka/ca1g4jf3/

모듈 레지스트리 및 인젝터 구현

그럼 남은일은 모듈을 등록할 레지스트리를 구현하는 일이다.
여기서는 간단히 이름 기반의 DI만 지원하는 것으로 하고, 키-값 객체로 관리하게 해보자.

일단 AMD 모듈이 아닌 일반적인 모듈로 구현해봤다.
(function(ctx) {

    var FN_PARSE = /^function\s*(\S+)[^\(]*\(\s*([^\)]*)\)\s*\{([\W\w]+)\}$/m,
        STRIP_COMMENT = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
        M = {};

    ctx.Injector = {

        // 의존성 모듈을 새로 등록한다.
        register: function(name, mo) {
            M[name] = mo;
        },

        // 함수를 받아 의존성을 주입한 뒤 즉시 실행한다.
        execute: function(fn, ctx) {
            return this.di(fn, ctx || this)();
        },

        // 함수를 받아 의존성을 주입한다.
        di: function(fn, ctx){

            // 함수를 정규식으로 분해한다.
            var parsed = fn.toString().match(FN_PARSE),
                fnName = parsed[1],
                args = parsed[2].replace(STRIP_COMMENT, '').split(','),
                body = parsed[3],
                i = 0, j,
                injected = [];

            // 인자의 이름으로 레지스트리에서 찾아 순서대로 적재
            for(j = args.length; i < j; i++) {
                injected[i] = M[args[i].trim()] || undefined;
            }

            // 래핑 함수를 반환한다.
            return function() {
                return fn.apply(ctx || null, injected);
            }
        }
    };
})(this);

di 함수에서 대해 조금 설명하면, 함수를 먼저 분석기로 쪼개서 배열을 얻은 뒤, 인자 배열을 돌면서 등록된 모듈과 매치하는 배열을 생성한 뒤 wrap 하여 반환하는 방식이다.

어디 잘 돌아가나 테스트.
var Coffee = {
    pour: function(some) {
        return "커피를 " + some + "에 따르고 ";
    }
}

var Milk = {
    pour: function(some) {
        return "우유를 " + some + "에 따르고 "
    }
}

Injector.register('coffee', Coffee);
Injector.register('milk', Milk);

function Cup(coffee, /**/milk) {
    var me = "머그컵";
    return Coffee.pour(me) + Milk.pour(me) + " 섞어";
}

var drink = Injector.di(Cup);

var coffeeMilk = drink(),
    directDrink = Injector.execute(Cup);

console.log(coffeeMilk); // 커피를 머그컵에 따르고 우유를 머그컵에 따르고 섞어 마신다
console.log(coffeeMilk == directDrink); // true

맛있는 커피우유가 만들어진 것 같다.

jsFiddle
http://jsfiddle.net/javarouka/dc28fxfg/

생각해볼 것들.


현재 구현이 포스팅하며 날림한거라 미비하거나 주의할 점이 몇가지 있다
  1. uglify 등 minify 할 경우 인자 이름이 보존이 안된다. mangle 옵션 등으로 인자이름을 보전해야 올바른 동작이 가능하다.
  2. 래핑 함수를 반환하는 관계로 스코프가 꼬일 수 있다.

2014. 9. 2.

2014. 8. 31.

Handlebars (for Java) 서버, 클라이언트 동시에 사용하기

웹 템플릿 엔진

웹 템플릿 엔진은 비슷한 구조의 View가 묶여지는 데이터에 의해 바뀔 수 있는 텍스트를 (보통은 HTML) 생성해주는 엔진을 가르키는 것이라고 볼 수 있다.

자바 개발자라면 JSP 가 아주 친숙한 템플릿 엔진일 것이다. 이 외에도 Apache Velocity, Jade, FreeMarker 등등 템플릿의 종류는 꽤 많다.

최근에는 서버에서 동작하는 템플릿 이외에도 클라이언트에서 동작하는 템플릿 엔진이 인기인데, 모바일 환경으로 넘어오며 더욱 중요해지고 있다.
  • 서버의 부담 감소
  • 적은 트래픽으로 컨텐츠 서비스
  • 클라이언트 캐시 사용 용이
  • 서버사이드와 클라이언트 사이드의 개발을 병렬로 진행할 수 있는 이점
이러한 점 때문에 앞으로도 인기는 꽤 많을듯 싶다. 암튼...


최근 클라이언트 사이드 프레임워크들은 자체 템플릿 엔진을 가지고 나오는 경우가 많으며, 거의 대부분의 서버사이드 템플릿 엔진은 클라이언트에서도 사용할 수 있도록 나오는 경우가 많다.
템플릿 엔진 관련 재미있는 사이트 ## 템플릿 엔진 셀렉터
이 중에서 최근 많이(정말...?) 사용되는 듯한 템플릿 엔진인 Handlebars 에 대해 좀 썰을 풀어볼 생각이다.

최근 회사 프로젝트(Java 기반이다) 에 적극적으로 적용하고 있는데 개인적으로는 꽤 만족하며 사용하고 있다. 처음 적용하면서는 꽤 어려움을 겪었는데 그 중 하나가 서버와 클라이언트에서 동시에 사용할 때 였다. 게다가 RequireJS 와 같이 사용할때 도 함정이 있었다.

서버와 클라이언트 동시 사용

템플릿 엔진들이 그렇듯이 바뀌는 동적 부분에 나름대로의 문법 치환자를 제공하고 그 부분에 데이터가 엮이며 치환되는 방식이다. 템플릿의 로직 지원여부에 따라 제어문이나 수식이 들어가기도 한다.

템플릿에 따라서는 아예 언어 레벨의 스크립틀릿 을 지원하기도 (JSP, PHP,  EJS  등) 하지만,  최근 추세는  logic-less 로 템플릿의 로직에 제약을 거는게 대부분이다.

Handlebars도 예외가 아니라서 스크립트릿따위 없다. 심지어 단순한 if condition, loop 구문도 제약이 굉장히 심한 편이다.

이야기가 좀 샜다.
암튼 본론으로 돌아가서...아래와 같은 템플릿이 있다고 가정한다.

<ul>
    {{#orders}}
        <li data-order-id="{{orderId}}">{{productName}}</li>
    {{/orders}}
</ul>

이 템플릿에 컨텍스트를 지정하기 전에 일단 컴파일이 필요하다.

Spring Framework 을 사용하는 서버 환경에서는 대부분 컴파일 과정을 프레임워크 레벨에서 처리하므로 데이터만 묶으면 되지만 Spring Framework 등의 Framework를 사용하지 않는 환경은 반드시 텍스트를 생성하기 위해 해당 환경에서 처리 가능한 형태로 변환하는 컴파일 작업이 사전에 수행되어야 한다.

템플릿 엔진의 작업은 다음과 같이 진행된다
  1. 템플릿 엔진이 템플릿을 읽는다
  2. 해당 환경에서 실행할 수 있게 템플릿을 컴파일한다.
  3. 컨텍스트를 지정해 텍스트로 변환한다
JavaCode 로 표현하면 다음과 같은 식이다.

TemplateLoader loader = ClassPathTemplateLoader();
loader.setPrefix("/views");
loader.setSuffix(".hbs");

Handlebars handlebars = new Handlebars(loader);

// /views/orderList.hbs
Template template = handlebars.compile("orderList");

List<orderdto> orderList = getOrderList();

// orderList를 Context 로 사용
System.out.println(template.apply(orderList));

/*
<ul>
    <li data-order-id="10031231">내주문</li>
    <li data-order-id="10031232">다른사람주문</li>
</ul>
*/


하지만 여기서 문제가 발생한다.

문제

서버와 클라이언트에서 동시에 Handlebars 를 사용하려고 할 경우 치환자의 문제가 생긴다
가령 이러한 템플릿을 두고 사용하려고 한다고 가정해보자

<-- 이 부분은 클라이언트에서 사용할 템플릿 -->
<script id="invoice-template" type="text/template" >
<table>
    {{#each this}}
        <tr>
            <td class="dateformat">{{createdAt}}</td>
            <td>{{createdBy}}</td>            
            <td>{{action}}</td>
            <td>{{returnDeliveryType}}</td>
            <td>{{editColumn}}</td>            
        </tr>
    {{/each}}
</table>
</script>

<-- 이 부분은 서버에서 컴파일 될 템플릿 -->
<div>
    <h1>{{reportName}}</h1>
    <p>{{author}}</p>
    <div id="relivery-report-table-area"></div>
</div>

<-- 클라이언트 템플릿을 사용해서 그려보자 -->
<script type="text/javascript" >

    var templateHtml = jQuery("#invoice-template").html();
    var template = Handlebars.compile(templateHtml);

    var invoiceList = Invoice.getDeliveryData();
    var invoiceListHtml = template(invoiceList);

    jQuery("#relivery-report-table-area").html(invoiceListHtml);

</script>

스크립트에서 클라이언트 템플릿을 사용하려고 하지만 서버에서 치환자들이 다 치환되어 버린 상태이기 때문에 템플릿 컴파일이 아무 의미가 없어진다. 혹은 이미 서버에서 주어진 컨텍스트에 클라이언트에서 사용하려 한 변수들이 없기에 NullPointerException 을 내고 있을 것이다.

서버와 클라이언트가 같은 치환자 문자열인 {{expression}} 을 쓰기 때문이다.

도움! ..이 아니고 Helper precomplie / embedded

둘의 제일 큰 차이는 서버에서 클라이언트에서 사용할 수 있게 컴파일을 미리 해두느냐 하지 않느냐의 차이이다.

먼저 precompile Helper 를 써보자.
위의 코드중 JavaScript 에서 사용할 템플릿을 별도 파일로 분리하자.

그리고

<script type="text/javascript">{{precompile 분리한 파일 경로 문자열}}</script>

과 같이 지정한다.
주의할 점은 스크립트 태그로 반드시 감싸야 하며, 타입은 text/javascript 로 지정해야 한다.

<-- 
    이 부분은 클라이언트에서 사용할 템플릿.
    precompile Helper 를 사용했다.
-->
<script type="text/javascript">{{precompile "precompiles/invoiceNumbers"}}</script>

<-- 이 부분은 서버에서 컴파일 될 템플릿 -->
<div>
    <h1>{{reportName}}</h1>
    <p>{{author}}</p>
    <div id="relivery-report-table-area"></div>
</div>

<-- 클라이언트 템플릿을 사용해서 그려보자 -->
<script type="text/javascript" >

    // 컴파일 과정이 필요없이 핸들바의 키로 잡힌다.
    var template = Handlebars.templates['precompiles/invoiceNumbers.hbs'];
    
    var invoiceList = Invoice.getDeliveryData();
    var invoiceListHtml = template(invoiceList);
    jQuery("#relivery-report-table-area").html(invoiceListHtml);

</script>

위의 결과로 서버에서는 JavaScript 에서 진행 될 템플릿 컴파일의 소스를 생성해서 내려주며 결과는 다음과 비슷한 구조가 된다.

<-- 
    이 부분은 클라이언트에서 사용할 템플릿.
    precompile Helper 를 사용했다.
-->
<script type="text/javascript">
var template = Handlebars.template(function (Handlebars,depth0,helpers,partials,data) {
    this.compilerInfo = [4,'>= 1.0.0'];
    helpers = this.merge(helpers, Handlebars.helpers); data = data || {};
    var stack1, functionType="function", escapeExpression=this.escapeExpression,
        self=this, blockHelperMissing=helpers.blockHelperMissing;

    function program1(depth0,data) {

        var buffer = "", stack1, helper, options;
        buffer += "\r\n";
        if (helper = helpers.createdAt) {
            stack1 = helper.call(depth0, {hash:{},data:data});
        }
        else { helper = (depth0 && depth0.createdAt);
            stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper;
        }
        buffer += escapeExpression(stack1)
            + "\r\n";
        if (helper = helpers.createdBy) { stack1 = helper.call(depth0, {hash:{},data:data});
        }
        else { helper = (depth0 && depth0.createdBy);
            stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper;
        }
        buffer += escapeExpression(stack1)
            + "\r\n";
        if (helper = helpers.action) { stack1 = helper.call(depth0, {hash:{},data:data});
        }
        else { helper = (depth0 && depth0.action);
            stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper;
        }
        buffer += escapeExpression(stack1)
            + "\r\n";
        if (helper = helpers.returnDeliveryType) {
            stack1 = helper.call(depth0, {hash:{},data:data});
        }
        else {
            helper = (depth0 && depth0.returnDeliveryType);
            stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper;
        }
        buffer += escapeExpression(stack1)
            + "\r\n";
        if (helper = helpers.editColumn) {
            stack1 = helper.call(depth0, {hash:{},data:data});
        }
        else { helper = (depth0 && depth0.editColumn);
            stack1 = typeof helper === functionType ? helper.call(depth0, {hash:{},data:data}) : helper;
        }        
        return buffer;
    }
    stack1 = helpers.each.call(depth0, depth0,
        {hash:{},inverse:self.noop,fn:self.program(1, program1, data),data:data});
    if(stack1 || stack1 === 0) { return stack1; }
    else { return ''; }
});
var templates = Handlebars.templates = Handlebars.templates || {};
templates['precompiles/invoiceNumbers.hbs'] = template;
var partials = Handlebars.partials = Handlebars.partials || {};
partials['precompiles/invoiceNumbers.hbs'] = template;
</script>

<-- 이 부분은 서버에서 컴파일 될 템플릿 -->
<div>
    <h1>{{reportName}}</h1>
    <p>{{author}}</p>
    <div id="relivery-report-table-area"></div>
</div>

<-- 클라이언트 템플릿을 사용해서 그려보자 -->
<script type="text/javascript" >

    // 컴파일 과정이 필요없이 핸들바의 키로 잡힌다.
    var template = Handlebars.templates['precompiles/invoiceNumbers.hbs'];
    
    var invoiceList = Invoice.getDeliveryData();
    var invoiceListHtml = template(invoiceList);
    jQuery("#relivery-report-table-area").html(invoiceListHtml);

</script>

뭔가 복잡한 코드로 변경되어 서버가 브라우저에 응답했다.

코드 내용에 주의를 기울일 필요가 없다.
중요한 건 저 코드가 컴파일 과정을 실행하며, 결과가 Handlebars.template 속성의 키로 지정된다는 것과 사용할때 컴파일 과정을 건너뛰고 컨텍스트만 지정해주면 된다는 사실이다.

반면 embedded Helper 를 사용하면 다음과 같은 응답이 온다.

<-- 
    이 부분은 클라이언트에서 사용할 템플릿.
    embedded Helper 를 사용했다.
-->
<script type="text/javascript">
<table>
    {{#each this}}
        <tr>
            <td class="dateformat">{{createdAt}}</td>
            <td>{{createdBy}}</td>            
            <td>{{action}}</td>
            <td>{{returnDeliveryType}}</td>
            <td>{{editColumn}}</td>            
        </tr>
    {{/each}}
</table>
</script>

<-- 이 부분은 서버에서 컴파일 될 템플릿 -->
<div>
    <h1>{{reportName}}</h1>
    <p>{{author}}</p>
    <div id="relivery-report-table-area"></div>
</div>

<-- 클라이언트 템플릿을 사용해서 그려보자 -->
<script type="text/javascript" >

    // 컴파일 과정이 필요없이 핸들바의 키로 잡힌다.
    var template = Handlebars.templates['precompiles/invoiceNumbers.hbs'];
    
    var invoiceList = Invoice.getDeliveryData();
    var invoiceListHtml = template(invoiceList);
    jQuery("#relivery-report-table-area").html(invoiceListHtml);

</script>

별도로 분리한 파일이 그대로 변환없이 클라이언트까지 내려온다.

물론 embedded 를 사용할 일은 거의 없다고 볼 수 있다. 서버측 컴파일을 거쳐오면 클라이언트에서 컴파일을 할 필요가 없어지기에 좀더 빠르게 클라이언트에서 템플릿을 표현할 수 있으며 코드도 간단해진다.

클라이언트 템플릿 엔진이 Handlebars.js 가 아니거나 Handlebars.js 를 늦게 인클루드 할 경우에나 소용이 있을 듯 싶다.

그런데 또 하나 문제가 있다.

precompile 을 적용하게 되면 사용자의 브라우저가 해당 precompile된 블럭을 읽기 전 Handlebars.js 가 로딩되어야 한다.

이것에서 문제가 발생한다.

RequireJS와 함께일 경우

최근엔 JS 부분이 중요해짐에 따라 수많은 스크립트들의 의존성을 해결해주고 모듈화를 적용하는 경우가 많아졌다. 그중 하나가 AMD방식의 모듈 로딩이며 그 중에서도 RequireJS 가 자주 사용되는 듯 하다.

만일 precompile 과 RequireJS로 Handlebars 를 로딩하고 있다면 비동기 모듈 로딩 특성상 반드시 오류가 난다.

precompile은 전역에 이미 Handlebars 객체가 있다고 가정한 상태로 템플릿 컴파일 결과를 응답하며, 이 시점에 클라이언트는 아직 Handlebars 객체가 존재하지 않는다.

당연히 스크립트 오류가 발생한다.

물론 precompile 블럭이 오기 전에 미리 <script  src='/path/to/Handlebars.js'> 하면 되지만 RequireJS 환경에서 스크립트를 모듈로 불러오지 않는 건 그리 할만한 일이 아니다.

물론 해결책이 있다.

Handlebars Java  버전은 wrapper 속성으로 amd 방식의 컴파일도 지원한다. 헬퍼 옵션에 wrapper="amd" 속성을 주면 끝이다.
precompile 경로가 그대로 RequireJS 모듈 이름으로 등록되며 그 이름으로 모듈을 로딩하면 컴파일 된 템플릿을 사용할 수 있다.

<script type="text/javascript">{{precompile "precompiles/invoiceNumbers" wrapper="amd"}}</script>
이 경우 클라이언트에 내려오는 컴파일 코드의 내용이 다음과 같이 AMD 형식으로 래핑된다.

define('precompiles/invoiceNumbers.hbs', ['handlebars'], function(Handlebars) {

    // ... 컴파일 된 소스

    return template;

}

이제 amd 로 로딩하여 사용하면 된다!
require(["jquery", "order", ""], function($, OrderModule, invoiceTpl) {

    "use strict";

    var $area = $("div[data-orde-area]"),
        data = OrderModule.getOrderData();

    $area.html(invoiceTpl(data));

});

wrapper 속성은 amd 외에도 anonymous, none 이 있다.
anonymous 는 익명 함수로 컴파일 소스 실행부를 감싸는 방식이며, none 은 기본값으로 전역 스코프에서 컴파일 과정을 처리한다.

참고자료