Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Assemble : Handlebars를 이용한 정적사이트 생성도구

최근에 프로젝트에서 프론트엔드를 서버 쪽 템플릿엔진에 연결하지 않고 HTML 기반으로만 동작하도록 만들어야 할 일이 있었다. 이건 마치 웹 프론트엔드를 모바일 앱처럼 클라이언트처럼 보고 서버에 API로만 붙어서 사용하겠다는 것과 같다.(이 부분에 대해서도 오래 고민한 내용이긴 하지만 그건 나중에...) HTML로 페이지를 구성하려니(CSS와 JavaScript를 안 쓴다는 의미가 아니다) 가장 큰 문제가 되는 것은 공통영역에 대한 처리 문제이다.

서버 측 템플릿엔진은 보통 인클루드나 레이아웃 기능이 있으므로 헤더나 푸터같은 공통 영역을 특정파일에서 처리할 수 있지만, HTML에는 인클루드 기능이 없으니 공통부분도 모든 페이지에 작성해야 한다. 페이지가 30페이지면 헤더도 30페이지에 똑같이 들어가고 수정하게 되면 30페이지에서 모두 수정을 해야 한다. 이는 텍스트를 검색해서 금세 바꿀 수 있는 거이긴 하지만 무척 귀찮은 부분이기도 하고 정리해서 관리하기 어려운 HTML에서는 특히나 유지보수 이슈가 발생할 가능성이 높다. 그래서 프론트엔드 환경에서 이러한 부분을 해결할 수 있는 도구를 찾아봐야 했고 필요한 요구사항은 다음과 같았다.

  1. 서버 측 템플릿언어처럼 공통부분을 한 파일에서 관리하고 조합할 수 있어야 한다.
  2. SPA(Single Page Application)이 아니므로 최종 결과가 각 HTML 파일로 만들어져야 한다.
  3. 방식에 따라 다르겠지만, 최종 HTML을 만드는 과정을 자동화할 수 있어야 한다.

그러던 중 찾아낸 것인 assemble였다.

Assemble

Assemblehandlebars를 템플릿언어로 사용하는 정적사이트 생성도구이다. 많은 프로젝트에서 이용 중인데 handlebars를 템플릿 엔진으로 사용하고 Grunt기반으로 동작해서 쉽게 페이지를 만들 수 있다. Assemble 홈페이지를 보면 Assemble과 관련된 다양한 도구들이 제공되고 있는데 이 글에서는 Assemble만 다룬다.

Assemble의 README에서 보듯이 Grunt를 기반으로 동작하고 있으므로 Assemble을 사용하려면 Grunt 설정이 되어 있어야 한다.(Grunt 0.4로 업그레이드하기 참고) Grunt를 사용해 보았다면 설치는 어렵지 않게 할 수 있을 것이다.

Assemble 설정

assemble을 설치하고 난 후(npm install assemble) Grunt에 설정해야 한다.

...

grunt.initConfig({
    assemble: {
        build: {
            options: {
                layoutdir: 'layouts',
                layout: 'default.hbs'
            },
            files: [
                { expand: true, cwd: 'pages', src: ['**/*.hbs'], dest: 'dist/' }
            ]
        }
    }
})
...

options에서 레이아웃을 설정한다. 기본 레이아웃 폴더를 layouts 폴더로 설정하고 그 아래 default.hbs를 기본 레이아웃으로 설정한다. 그 외 모든 페이지는 pages폴더 아래 두고 이 아래 있는 모든 hbs 파일을 레이아웃과 함께 빌드해서 dist폴더 아래 만든다. .hbs는 handlebsars 파일을 의미하고 pages 폴더 아래의 파일이 모두 dist아래로 1:1 매핑되서 HTML로 생성된다.

레이아웃


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <div id="container">
        <header id="header">This is header</header>

        <nav id="menus">side menu bar</nav>

        <div id="content">{{> body }}</div>
    </div>

    <footer id="footer">COPY RIGHT &copy; 2014. ALL RIGHT RESERVED. </footer>

    <script src="/js/jquery.js"></script>
    <script src="/js/lodash.js"></script>
</body>
</html>

레이아웃 파일이다. 서버 쪽 템플릿 언어를 많이 사용해 봤거나 handlebars를 써본 적이 있다면 쉽게 이해할 수 있는 구조다. 페이지의 공통 영역을 레이아웃에 위치시킨다. 변수의 사용은 {{}}``문법을 사용하고{{> body }}부분에 본문의 HTML 부분이 들어간다.(여기서는pages아래의.hbs`파일)

다음은 pages의 파일들이다.

---
title: 대문
---
<!-- pages/index.hbs -->
<h1>첫 페이지</h1>
<p>블라블라</p>
---
title: 로그인
---
<!-- pages/login.hbs -->
<h1>로그인 하세요</h1>
<input type="text" name="id">

각 페이지의 최상단에는 ---안에 YAML 문법으로 변수를 적을 수 있다. 여기서는 title 변수를 정의했는데 이 변수는 해당 페이지를 빌드할 때 사용하므로 레이아웃파일의 {{title}}부분에 들어가서 각 파일의 공통영역에 대한 변수를 각 페이지에서 제어할 수 있다. 필요한 변수들을 메뉴바나 필요한 스크립트를 위한 변수 등을 여기서 정의할 수 있다. Handlebars를 사용하므로 여기서 지원하는 {{#each}}, {{#if}}, {{#unless}}등의 문법도 당연히 사용할 수 있다.

이를 grunt assemble로 빌드하면 dist/index.htmldist/login.html파일이 생성된다.


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>대문</title>
</head>
<body>
    <div id="container">
        <header id="header">This is header</header>

        <nav id="menus">side menu bar</nav>

        <div id="content">



첫 페이지

블라블라

</div> </div> <footer id="footer">COPY RIGHT &copy; 2014. ALL RIGHT RESERVED. </footer> <script src="/js/jquery.js"></script> <script src="/js/lodash.js"></script> </body> </html>

빌드된 파일은 레이아웃과 페이지가 합쳐져서 하나의 HTML 파일로 만들어진다. 개발은 서버 측 언어처럼 나뉘어서 하지만 결과적으로는 정적인 HTML 파일을 생성할 수 있다.

앞에서는 Grunt 설정에서 레이아웃 파일을 지정했지만 실제로 웹사이트를 만드려면 하나의 레이아웃 파일로 모든 페이지를 처리할 수 있는 경우는 거의 없다. 레이아웃 파일을 아주 유연하게 작성하면 가능은 하지만 오히려 복잡도가 증가해서 그냥 여러 가지 레이아웃 파일을 만드는 것이 더 낫다. 각 페이지에서 상단에 YAML을 정의할 때 layout: popup.hbs과 같이 레이아웃을 지정하면 Grunt 설정의 기본 레이아웃 대신에 여기서 지정한 레이아웃 파일을 사용해서 빌드를 하게 된다.

파셜 인클루드

레이아웃으로 공통 부분을 따로 작성할 수 있지만 각 페이지에는 일부만 포함되는 공통 영역이 있다. 예를 들면 스크립트나 CSS 로딩 같은 부분은 보통 여러 레이아웃 파일에 공통으로 들어갈 수도 있고 로그인 영역이나 특정 메뉴에만 표시되는 부분은 전체적으로 항상 나오지는 않지만, 페이지 다수에서 등장할 수도 있다. 이런 부분을 모든 페이지에 항상 적어주는 것은 귀찮은 작업이므로 이 부분만 따로 공통화할 수 있으면 좋은데 이를 위해서 Partial Include를 지원한다.

{{> script}}와 같이 {{>}} 문법을 사용하면 해당 부분은 파셜로 처리할 수 있다. 위에서 본 레이아웃 파일을 다음과 같이 변경할 수 있다.


<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>{{ title }}</title>
</head>
<body>
    <div id="container">
        <header id="header">This is header</header>

        <nav id="menus">side menu bar</nav>

        <div id="content">{{> body }}</div>
    </div>

    <footer id="footer">COPY RIGHT &copy; 2014. ALL RIGHT RESERVED. </footer>

    {{> script}}

</body>
</html>

partials 폴더(Grunt 설정에서 지정했다.) 아래 다음과 같이 script.hbs파일을 생성하면 빌드할 때 파셜 디렉터리 아래에서 해당 변수명의 파일을 찾아서 자동으로 함께 빌드한다.

<!--partials/script.hbs-->
<script src="/js/jquery.js"></script>
<script src="/js/lodash.js"></script>

헬퍼 함수

이건 Handlebars가 템플릿의 기능을 확장하는 방법인데 템플릿 언어 특성상 데이터를 조작하거나 제어하는데 의도적인 제약을 두고 있다. 그래서 이를 훨씬 유연하게 사용하려면 헬퍼함수를 작성해서 이를 Handlebars 내에서 함수처럼 사용해야 한다. Assemble에서는 여러 가지 헬퍼함수를 handlebars-helpers에서 제공하고 있다. (이를 사용하려면 설치를 해야 한다.) 특수한 경우가 있어서 Handlebars의 문법으로 표현하기가 어렵다면 직접 헬퍼함수를 등록해서 사용할 수 있다.

Gruntfile.js파일에서 헬퍼함수를 로딩할 위치를 옵션으로 지정한다.

assemble: {
    options: {
        helpers: ['helpers/*.js']
    },
    build: {
      ...
    }
}

이제 필요한 헬퍼함수를 작성하면 된다. 여기서는 간단히 두 수의 합을 반환하는 헬퍼함수를 작성했다. 헬퍼함수는 Node.js 문법으로 module.exports.register = function(Handlebars, options, params) {};같은 모양이 되고 이 안에 각 헬퍼함수를 생성해서 등록하면 된다.

// helpers/sum.js
module.exports.register = function(Handlebars, options, params) {
    Handlebars.registerHelper('sum', function(a, b) {
        return a + b;
    });
};

헬퍼함수에서 sum이라는 헬퍼를 지정했으므로 hbs파일에서 {{sum 2 3}}와 같이 사용하면 이는 빌드 시에 5로 변경돼서 만들어진다. 로직이 필요한 조건문 등을 헬퍼함수를 사용하면 유연하게 대응할 수 있다.

Handlebars와의 충돌 처리

assemble에서 Handlebars를 사용하고 있으므로 Ajax 등을 통한 동적인 페이지 변경을 위해서 JavaScript에서 Handlebars를 사용하고 있다면 이 둘이 문법적으로 충돌하게 된다.

<script type="text/x-handlebars-template">
<ul>
    {{#each list}}
    <li>{{name}}</li>
    {{/each}}
</ul>

.hbs파일 안에 위와 같은 Handlebars 템플릿 코드를 넣어놨다면 실제로 이는 assemble이 처리하는 게 아니라 HTML에 포함된 JavaScript 에서 사용할 의도였지만 문법이 같기 때문에 Assemble이 빌드하면서 이를 한꺼번에 빌드해버리는 문제가 발생한다. 이를 회피하려면 프론트앤드용 Handlebars 문법은 다음과 같이 이스케이프 처리를 해주어야 한다.

<script type="text/x-handlebars-template">
<ul>
    \{{#each list}}
    <li>\{{name}}</li>
    \{{/each}}
</ul>

이스케이프 된 Handlebars 문법은 Assemble가 처리하지 않는다.

에필로그

HTML을 빌드해서 HTML 페이지로 만들거나 HTML만으로 정적인 웹프론트앤드를 운영하는 것은 흔한 요구사항은 아니라고 생각한다. 내가 HTML 퍼블리싱을 주 업무로 해 본 적은 없지만, 서버 쪽에서 만들어진 HTML 파일을 넘겨받아서 작업할 때의 제일 어려운 부분은 공통화에 대한 부분이었다. 앞에서 요구사항처럼 얘기했지만, HTML을 넘겨받을 때 헤더나 메뉴바, 푸터 등이 처음에는 모든 페이지에 잘 들어가 있지만 이러한 부분이 변경되기 시작하면 어느 페이지는 변경되고 어느 페이지는 변경안 돼서 항상 적용하는데 곤란을 겪는다. HTML을 작성하는 쪽에서도 이러한 유지보수에 노동력이 많이 들 것 같은데 Assemble 같은 도구를 사용하면 좋지 않을까 하는 생각이 들었다.

2014/09/22 23:59 2014/09/22 23:59