본문 바로가기
개발/Spring

(2) 스프링, isomorphic, 서버사이드 렌더링 - Handlebars

by Kingbbode 2016. 9. 25.

(1) 스프링, isomorphic, 서버사이드 렌더링

(2) 스프링, isomorphic, 서버사이드 렌더링 - Handlebars



Spring + Nashorn을 통해 isomorphic할

첫번째 클라이언트 템플릿 엔진은 handlebars!



1. handlebars란?


 Handlebar.js(이하 핸들바)는 자바스크립트의 템플릿 엔진 중 하나로 Mustache를 기반으로 구현한 템플릿 엔진입니다. Mustache는 콧수염모양의 {{ }} Bracket을 이용하여 data를 표현하는 것을 의미하며, 이를 이용하면 html페이지에서 HTML+Bracket의 구성으로 디자이너와 개발자가 협업할 때도 디자이너에게도 이해하기 쉬운구조로써 협업을 하는데도 도움이 됩니다. (참고 : 돛단배의 항해일지)


 다양한 frontend template engine 중에서 각광받고 있는 handlebars!

 handlebars의 다양한 장점에 매력을 느껴 handlebars를 선택하는 서비스들이 많습니다. 대표적으로 티몬(티몬 개발 블로그 참조)에서 handlebars의 장점을 개발블로그에 소개하기도 했으며, 최근까지 handlebar를 활용한 다양한 사례들이 생기고 있습니다. 

 

 또한 창천향로의 개발이야기의 창천향로님이 소개해주신 pre-compile을 사용한다면 하나의 js로 재컴파일없이 더욱 더 빠른 속도로 Handlebars를 사용할 수 있습니다! 이 프로젝트에서도 중요하게 사용될 부분이니 꼭 보시길 바랍니다.


 frontend template engine의 선택은 본인의 몫!



2. handlebars.java가 아니고 왜 Nashorn을 사용하는가?


 handlebars는 자바 포팅 버전인 handlebars.java를 통해 서버사이드 렌더링을 제공하고 있습니다.


 그러나 제가 Nashorn을 사용하려는 이유는 다음과 같습니다.


 Handlebars.java에서는 몇가지 문제가 재기되었습니다. 대표적으로 서버와 클라이언트에서 동시에 Handlebars를 사용하려고 할 경우 치환자의 문제입니다. 공식홈페이지에서는 pre-compile을 통해 이 문제를 해결하는 가이드를 제공합니다. 그리고 이 pre-compile 방식도 여러가지 문제가 재기되었습니다. 대표적으로 티몬의 개발 블로그에서 문제의 여지를 재기하며 새로운 방법을 제시했습니다. 

 

 제시된 해결 방법들은 프론트엔드 handlebars.js와는 거리가 먼, 서버사이드에서 렌더링하기 위한 코드들이 추가로 달라붙게 됩니다. 그리하여 pre-compile된 js만을 사용하고 싶었던 제가 생각한 isomorphic과 Handlebars.java는 거리가 멀다는 것을 알게 되었습니다.


 그래서 저는 pre-compile된 js만으로 isomorphic을 하기 위하여 Nashorn을 사용하게 되었습니다.



3. Nashorn으로 Spring에서 Handlebars isomorphic 하기!


사용될 예제 hbs입니다.


index.hbs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta charset="utf-8" />
    <title>Spring Nashorn isomorphic handlebars</title>
    <meta name="description" content="overview &amp; stats" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
</head>
<body>
<div>
    <p>Layout Text : {{ text}}</p>
</div>
<div id="layer">
    {{{layer}}}
</div>
<a href="javascript:;" id = "call">client call</a>
<script src="/webjars/jquery/3.1.0/jquery.min.js"></script>
<script src="/webjars/handlebars/4.0.5/handlebars.min.js"></script>
<script src="/js/templates.js"></script>
<script>
    var $layer = $('#layer');
    $('#call').click(function() {
        $.ajax({
            'url''/rest',
            'type''GET',
            'dataType''json',
            'success': function (data) {
                $layer.html(Handlebars.templates['layer'](data));
            }
        });
    });
</script>
</body>
</html>
cs


layer.hbs

1
<p>Layer Text : {{ text}}</p>
cs


 서버에서 text와 layer가 렌더링된 후 클라이언트에서 click에 의해 layer 부분만 다시 렌더링해보겠습니다. 서버와 클라이언트에서 동일하게 이 두개의 hbs만을 사용하게 됩니다.



Config


1
2
3
4
5
6
7
@Bean
public ViewResolver viewResolver() {
    ScriptTemplateViewResolver viewResolver = new ScriptTemplateViewResolver();
    viewResolver.setPrefix("/templates/");
    viewResolver.setSuffix(".hbs");
    return viewResolver;
}
cs


 View단은 모두 pre-compile된 js를 통해서 서버든 클라이언트든 렌더링되므로 사용될 ViewResolver를 통해 suffix를 hbs로 하여 viewResolver를 bean으로 등록합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Bean
public ScriptTemplateConfigurer configurer() {
    ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
    configurer.setEngine(scriptEngine());
    configurer.setRenderFunction("render");
    configurer.setSharedEngine(true);
    return configurer;
}
 
@Bean
public NashornScriptEngine scriptEngine() {
    NashornScriptEngine nashornScriptEngine = (NashornScriptEngine) new ScriptEngineManager().getEngineByName("nashorn");
    try {
        nashornScriptEngine.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/polyfill.js").getInputStream(), "UTF-8"));
        nashornScriptEngine.eval(new InputStreamReader(resourceLoader.getResource("classpath:META-INF/resources/webjars/handlebars/4.0.5/handlebars.min.js").getInputStream(), "UTF-8"));
        nashornScriptEngine.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/js/templates.js").getInputStream(), "UTF-8"));
        nashornScriptEngine.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/render.js").getInputStream(), "UTF-8"));
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    return nashornScriptEngine;
}
cs

  

 NashronScriptEngine은 하위 모듈 렌더링을 위해 Service단에서 사용될 것이기 때문에 DI할 수 있도록 Bean으로 따로 등록하고 ScriptTemplateConfigurer에 등록합니다.


render.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var templates = {};
function render(template, model, url) {
    var pattern=/templates\/(.*)\.hbs/;
    var hbs = pattern.exec(url)[1];
    if(Handlebars.templates[hbs]){
        return Handlebars.templates[hbs](toJsonObject(model));
    }else {
         var compiledTemplate;
         if (templates[url] === undefined) {
             compiledTemplate = Handlebars.compile(template);
             templates[url] = compiledTemplate;
         }
         else {
            compiledTemplate = templates[url];
         }
        return compiledTemplate(toJsonObject(model));
    }
}
 
function renderModule(hbs, model){
    if(Handlebars.templates[hbs]){
        return Handlebars.templates[hbs](toJsonObject(model));
    }else {
        var compiledTemplate;
        if (templates[url] === undefined) {
            compiledTemplate = Handlebars.compile(template);
            templates[url] = compiledTemplate;
        }
        else {
            compiledTemplate = templates[url];
        }
        return compiledTemplate(toJsonObject(model));
    }
}
 
// Create a real JSON object from the model Map

function toJsonObject(model) {
    var o = {};
    for (var k in model) {
        // Convert Iterable like List to real JSON array
        if (model[k] instanceof Java.type("java.lang.Iterable")) {
            o[k] = Java.from(model[k]);
        }
        else {
            o[k] = model[k];
        }
    }
    return o;
}
cs


 render function은 ScriptTemplateConfigurer에 등록된 function으로 3가지 파라미터를 제공합니다. 

 pre-compile을 사용할 것이 아니라면 제공된 template 파라미터를 compile하여 View를 구성하겠지만, pre-compile을 사용할 것이므로 url을 사용하여 pre-compile된 View를 찾아 렌더링합니다.

 grunt를 통해 compile 할 때 파일이름으로 pre-compile을 등록하였기때문에, 수정하면 되지만 귀차니즘으로 정규식을 사용하여 pre-compile view를 찾고 있습니다;;


 renderModule function은 하위 모듈 렌더링을 위한 function으로 마찬가지로 pre-compile된 view를 찾아 컴파일합니다. 불필요하게 같은 로직이 나누어져있다고 생각하신다면 grunt를 수정하여 render와 renderModule을 합쳐도 됩니다.



Service

  

 ScriptTemplateEngine에서 view가 렌더링되기 전에 하위 모듈을 렌더링하기 위한 Service 입니다.

 

HandlebarstemplateServiceIple.java

1
2
3
4
5
6
7
8
9
10
11
@Autowired
NashornScriptEngine scriptEngine;
 
@Override
public String render(HandleBarsTemplate handleBarsTemplate) {
    try {
        return (String) scriptEngine.invokeFunction("renderModule", handleBarsTemplate.getName(), handleBarsTemplate.getModel());
    } catch (Exception e) {
        return "";
    }
}
cs


 Bean으로 등록해둔 nashorn을 이용하여 render.js에 작성한 renderModule을 실행하는게 전부 입니다. 

 model과 hbs를 전달하기 편하게 하기 위해 HandlebarsTemplate pojo를 작성했습니다.


HandlebarsTemplate.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class HandleBarsTemplate {
 
    public HandleBarsTemplate(String name) {
        this.name = name;
        this.model = new HashMap<>();
    }
 
    private String name;
    private Map<String, Object> model;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Map<String, Object> getModel() {
        return model;
    }
 
    public void setModel(Map<String, Object> model) {
        this.model = model;
    }
 
    public HandleBarsTemplate put(String name, Object value) {
        this.model.put(name, value);
        return this;
    }
}
cs



Controller


HomeController.java

1
2
3
4
5
6
7
8
9
 @Autowired
HandlebarsTemplateService handlebarsTemplateService;
 
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Model model) throws Exception {
    model.addAttribute("text""server");
    model.addAttribute("layer", handlebarsTemplateService.render(new HandleBarsTemplate("layer").put("text""server")));
    return "index";
}
cs


 text와 nashorn을 통해 렌더링된 하위 layer를 모델에 추가한 후 "index"로 return 하였을 때 config에 등록된 ViewResolver와 ScriptTemplateConfigurer에 의해 index.hbs이 Nashorn을 통해 render function으로 수행됩니다.

 render function을 통해 pre-compile된 view를 return 하여 서버사이드를 통해 렌더링이 완성됩니다.


HomeRestController.java

1
2
3
4
5
6
@RequestMapping("/rest")
public Map home() throws Exception {
    Map data = new HashMap<>();
    data.put("text""client");
    return data;
}       

cs


 클라이언트에서 click을 통해 클라이언트 렌더링되는 과정은 여느 Handlebars.js와 같습니다. 





첫 페이지


Click 후 client render




설명이 부족하였다면 참고 바랍니다!

git : https://github.com/kingbbode/spring-nashorn-isomorphic/tree/handlebars



 Nashorn을 통하여 Handlebars를 isomorphic 해 보았습니다. 클라이언트와 같은 js를 서버에서도 그대로 사용하였기 때문에 Hanlebars.java보다 간단하게 서버와 클라이언트에서 같은 템플릿을 사용하여 렌더링할 수 있었습니다.  자바에서 자바스크립트를 사용할 수 있는 이점은 정말 다양할 것 입니다. 그러나 아직 자바의 스크립트 엔진이 검증이 확실히 이루어지기 않았습니다.  진행하면서 느낀 것이 생각보다 깔끔하고 괜찬은데 성능은 어느정도일까 하는 의문이 계속 들었습니다. 때문에 조만간 벤치 마크를을 어느정도하여 문서를 업데이트해보아야 겠습니다!

댓글