실무에서 Handlebars 사용하기 (feat, express)

dev park·2020년 4월 25일
9
post-thumbnail

handlebars 는 화면을 표현 할 수 있는 템플릿 엔진이다. Express 진영에서 많이 사용하는 템플릿 엔진으로 handlebars , pug , ejs 가 있는데, handlebarsejs 중 선택을 해야 해서, 장 단점을 찾아봤다.

pug는 태그를 표현하는 방식이 너무 익숙하지 않아서 패스!

Handlebars의 장 단점

장점

  • Java, Django, Ruby, PHP, Scala 에서 사용 할 수 있다.
  • 문법이 깔끔하다.
  • Logic 과 Markup 을 깔끔하게 분리 할 수 있다. {{ }} 안에 있는 것이 Logic 이다.
  • partials 를 지원한다.
  • 컴파일 단계에서 코드가 해석이 되므로, 클라이언트에서 따로 코드를 해석하는 단계가 필요없다. 그러므로 로딩이 빠르다.
  • HTML 오픈 소스를 복사/붙여넣기 가 가능하다. (pug는 불가능, pug 형식에 맞게 다시 작성해야 함)

단점

  • auto-complete, syntax highlighting 등 과 같은 기능을 에디터에서 제공해 주지 않는다.
  • 크게 단점이라고 느껴지는 점이 없어 보인다.

EJS 장 단점

장점

  • Javascript 로직을 그대로 사용 할 수 있다.
  • 렌더링 전 후의 코드 차이가 없다.
  • Jade(pug) 보다 빠르다고 한다.
  • 에러를 확인하고 처리하는데 수월하다.
  • HTML 과 Javascript를 알고 있다는 전제 하에, 러닝 커브가 상당히 낮다.

단점

  • block 을 사용하기 위해서는 별도의 third-party-library 를 추가하여 사용해야 한다.
  • <% %> <%= %> 와 같은 문법이 HTML에 들어가 있어 코드를 읽기가 쉽지 않다.

<% %> <%= %> 를 사용해 if문, for문을 작성한 ejs를 보면 너무 난잡해 보임, Java, Django 등 다양한 언어에서도 사용할 수 있는 handlebar를 선택!

1. Express로 서버 띄우기

아래 명령어를 입력하여 express 를 설치해 주고, server.js 를 만들어 준다.

npm init -y
npm i express
touch server.js
const express = require("express");
const app = express();
    
app.listen(3000, () => {
   console.log(`Server is running on ${3000} port`);
});

위 코드를 작성하고 node server.js 를 실행하면 3000번 포트로 서버가 실행된다.

2. Handlebars 설치하기

아래와 같이 입력하여, handlebars 를 설치하고 express 엔진에 적용한다.

npm i express-handlebars

server.js

...
const exphbs = require("express-handlebars");
    
app.engine("handlebars", exphbs());
app.set("view engine", "handlebars");

view 엔진을 handlebars를 사용하겠다 정도로 이해하면 충분하다.

이제, handlebars가 읽을 수 있는 템플릿을 작성해야 하는데, 기본 layouts를 만들어주고, 그 안에 추가로 넣는 방식으로 진행된다.

아래와 같은 폴더 구조를 만들어 줍니다.

    ├─.
    ├── server.js
    └── views
        ├── home.handlebars
        └── layouts
            └── main.handlebars

views/layouts/main.handlebars

<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8">
    <title>Basic Handlebars</title>
  </head>

  <body>

    {{{body}}}

  </body>

</html>

{{{ }}} 입니다. {{ }} 아닙니다!

views/home.handlebars

<h1>Hello, handlebars</h1>

이렇게 입력하고, 요청했을 때 home.handlebars 를 렌더링 해줍니다.

server.js

...
app.get("/", (req, res) => {
  res.status(200).render("home");
});

그리고 http://localhost:3000 으로 접속하게 되면 home.handlebars 에 작성한 내용을 확인 할 수 있다.

3. 서버 데이터 렌더링 하기

SSR(서버 사이드 렌더링) 이라면, 서버에서 전달 해주는 데이터를 렌더링 할 수 있어야 한다. render 메소드 두 번째 인자로 값을 전달 할 수 있다.

server.js

...
app.get("/", (req, res) => {
  res.status(200).render("home", {
    name: "parkoon",
  });
});

views/homde.handlebars

<h1>Hello, handlebars</h1>
<p>My name is {{name}}</p>

{{}} 두 개!

서버로 부터 전달 받은 값을 {{ }} 로 감싸 화면에 출력한다.

4. Handlebars 옵션 설정하기

express-handlebars 는 기본 레이아웃 설정부터 해서 다양한 옵션을 제공해 줍니다. 여기서 다룰 옵션은 아래와 같다.

  • defaultLayout: 기본 값으로 main 으로 되어 있으며, 설정된 값에 따라 main layout의 파일명을 설정 할 수 있다.
  • extname: 확장자를 설정할 수 있습니다. 기본은 handlebars 이다.
  • partialsDir: partial 이란 레이아웃을 채울 파일들을 의미하며, partial 의 위치를 설정 할 수 있습니다. 기본은 partials 이다.
  • layoutsDir: layout 파일 위치를 지정 합니다. 기본 값은 layouts 이다.

여기서 위치는 기본 값으로 가져가고, view 파일을 생성할 때마다 작성해 줘야 했던 handlebar 확장자를 hbs 로 변경 해보자.

server.js

app.engine(
  "hbs",
  exphbs({
    extname: "hbs",
  })
);
app.set("view engine", "hbs");

extname 뿐만 아니라 engine 의 첫 번째 인자, app.set의 두 번째 인자에 있던 handlebars 도 변경 해주어야 한다.

5. 조건에 따른 화면 처리

단순한 예로, isLoggedIn 값을 이용해서 로그인 한 사용자는 Logout을 보여주고, 로그인 하지 않은 사용자는 Logout을 보여주는 페이지를 만들어 보자.

{{#if}} {{else}} {{/if}}

먼저 서버에서 전달하는 코드다.

app.get("/", (req, res) => {
  res.status(200).render("home", {
    name: "parkoon",
    isLoggedIn: false,
  });
});

템플릿에서 받아서 처리하는 코드는 아래와 같다.

views/home.hbs

<h1>Hello, handlebars</h1>
<p>My name is {{name}}</p>

{{#if isLoggedIn}}
<a href="#">Logout</a>
{{else}}
<a href="#">Login</a>
{{/if}}

# 으로 시작해서 / 로 끝난다.

이번엔 isReady 값을 전달하여 준비 상태가 아니라면 Start 버튼을 보여주는 코드를 작성 해보자. 지금까지 배운대로 코드를 작성한다면, 아래와 같이 작성 할 것이다.

{{#if isReady}}
{{else}} <button>Start</button>
{{/if}}

이런 상황을 대비해, handlebars 에서는 unless 라는 헬퍼를 제공하고 있다.

{{#unless isReady}}
<button>Start</button>
{{/unless}}

동작 결과는 똑같을 것이다.

boolean 값 말고 string을 비교 할 수 없을까?

아쉽게도 내장된 handlebars 헬퍼를 이용해서 비교할 수 없다. 아래에서 커스텀 헬퍼를 다룰 때 이 문제를 해결해 보자.

6. 반복에 따른 화면 처리

템플릿에 users: ['parkoon', 'kimyang', 'choikoon', 'leeyang'] 을 전달하고 이 값들을 출력해보자.

{{#each}} {{/each}}

server.js

app.get("/", (req, res) => {
  res.status(200).render("home", {
    name: "parkoon",
    isLoggedIn: true,
    isReady: false,
    users: ["parkoon", "kimyang", "choikoon", "leeyang"],
  });
});

views/home.hbs

{{#each users}}
<span>{{this}}</span>
{{/each}}

순회가 가능한 값을 each 옆에 입력하고 this 로 접근하면 된다. 배열 뿐만 아니라, 객체도 each 로 순회 할 수 있으며, this 로 접근하면 객체의 value 값이 출력된다.

키 값에 접근하고 싶은데 어떻게 할 수 있을까?

이 때는 객체를 as 로 앨리어싱 처리는 해주면 된다.

views/home.hbs

{{#each user as |value key|}}
<span>{{value}}</span>
<span>{key}}</span>
{{/each}}

|value key| 의 값은 정의되어 있는 값이 아니라, 상황에 맞게 변경해서 써도 된다. e.g. |foo bar|

7. with / as 헬퍼 사용하기

우리는 객체를 접근 할 때 {{user.name}} 과 같이 사용 했다. 이 때 with 헬퍼를 사용하면 바로 name 으로 접근 할 수 있게 된다.

views/home.hbs

{{#with user}}
<span>{{name}}</span>
{{/with}}

그리고 as 헬퍼를 이용해 앨리어싱을 할 수 있다.

{{#with user as |u|}}
<span>{{u.name}}</span>
{{/with}}

8. Partial 사용하기

중복되는 부분을 partial 로 만들고 재사용 할 수 있게 제공하고 있다.

views 밑에 partials 폴더를 생성하고 그 안에 header.hbs 와 footer.hbs를 만들었다.

{{> header}} {{> footer}} 와 같이 앞에 > 로 partial을 불러온다.

views/partials/header.hbs

<header>
  <h1>{{title}}</h1>
  <nav>
    <li>Home</li>
    <li>Lecture</li>
    <li>Account</li>
  </nav>
</header>

views/partials/footer.hbs

<footer> 
  this is footer
</footer>

views/home.hbs

{{< header title="This is title" }}
          .
          .
          .
{{< footer}}

{{< header title="This is title" }} 과 같이 파라미터도 전달 할 수 있다!

이 정도면 handlebars 기본적인 화면을 구현할 수 있을 것이다.

지금부터는, 웹 페이지를 구현 했던 경험에 비춰 handlebars 에서는 어떻게 구현 할 수 있을지 알아본다.

9. 소소한 개발 팁

9.1 조건문에서 문자열 비교는 어떻게 할까?

위에서 언급했듯이, 내장된 헬퍼로는 구현 할 수 없다. 이 때 커스텀 헬퍼를 작성해서 사용한다.

username === parkoon 일 때 hello parkoon 을 띄워주세요.

server.js

    ...
    
    app.engine(
      "hbs",
      exphbs({
        extname: "hbs",
        helpers: {
          **ifUsernameParkoon**: function (arg1, arg2, options) {
            return arg1 === arg2 ? options.fn() : options.inverse();
          },
        },
      })
    );
    
    app.get("/", (req, res) => {
      res.status(200).render("home", {
        username: "parkoon",
      });
    });
    
    ...

서버에서 username 을 전달 해주고 있다. 그리고 helpers 라는 속성에 ifUsernameParkoon 이라는 함수를 만들었고 함수는 3의 파라미터(arg1, arg2, options)를 받고 있다.

views/home.hbs

{{#ifUsernameParkoon username "parkoon"}}
<h1>Hello, parkoon</h1>
{{/ifUsernameParkoon}}

{{#ifUsernameParkoon username "parkoon"}} 이렇게 ifUsernameParkoon 을 호출 하게되면 arg1 에는 username의 값이 arg에는 "parkoon"의 값이 들어간다.

isUsernameParkoon 에서는 arg1 과 arg2 를 비교하여 맞을 경우 fn 을 호출하고 맞지 않을 경우 inverse를 호출하고 있다.

fn은 커스텀 헬퍼가 감싸고 있는 부분을 출력해 주고, inverse는 출력하지 않는 역할은 한다.

그리고 헬퍼 함수 안에서는 this 를 접근 할 수 있는데, this 에는 렌더링 정보가 담겨 있다. 이 말은, username의 값이 담겨있다는 것이다. 따라서, 위 코드를 다음과 같이 수정 할 수 있다.

server.js

    app.engine(
      "hbs",
      exphbs({
        extname: "hbs",
        helpers: {
          ifUsernameParkoon: function (arg1, options) {
            return this.username === arg1 ? options.fn() : options.inverse();
          },
        },
      })
    );

views/home.hbs

    {{#ifUsernameParkoon "parkoon"}}
    <h1>Hello, parkoon</h1>
    {{/ifUsernameParkoon}}

이렇게 커스텀 함수를 이용해 입맛에 맞게 조건문을 구성할 수 있다.

9.2 css 모듈화

페이지에 따라 다른 css를 적용하고 싶습니다.

home.hbs 에는 body의 색을 #2980b9, about.hbs에는 #9b59b6 를 적용하고 싶다는 가정을 해보자. 전역에 <link rel="stylesheet" href="style.css"> 와 같이 적용 시키면 모두 동일한 색의 body를 갖게 될 것이다.

우리는 여기서, 편법을 사용한다. rendering 할 때 넘겨주는 데이터에 css 파일명을 넘겨 줄 것이다. public 폴더는 만들고 그 안에 css 폴더를 만들고 각 페이지 이름을 딴 css 파일을 만들어 줄 것이다.

    .
    └── public
         ├── css
    	 └── home.css
    	 └── about.css

home.css / about.css

    body {
      background-color: #9b59b6;
    }
    
    body {
      background-color: #2980b9;
    }

server.js

    app.get("/", (req, res) => {
      res.status(200).render("home", {
        style: "home",
      });
    });
    
    app.get("/about", (req, res) => {
      res.status(200).render("about", {
        style: "about",
      });
    });

views/layouts/main.hbs

    <head>
        ...
    	<link rel="stylesheet" href="css/{{style}}.css">
    	...
    </head>

이렇게 처리하면 각 페이지가 렌더링 될 때마다 다른 css를 적용할 수 있다.

CSS 가 적용이 안되고, 에러가 납니다.

위 문제는, handlebars와 무관한 문제로 서버에서 정적인 파일에 접근을 못할 때 발생하는 문제로 아래 코드는 추가해 줌으로써, 정적인 파일을 제공 할 수 있도록 해준다.

server.js

    ...
    const path = require("path");
    app.use(express.static(path.join(__dirname, "public")));
    ...

9.3 레이아웃을 main 말고 추가로 더 사용 할 수 있을까?

server.js

    app.get("/", (req, res) => {
      res.status(200).render("home", {
        style: "home",
        layout: "mobile",
      });
    });

layouts 폴더에 사용하고자 하는 레이아웃(mobile)을 생성하고, 위와 같이 렌더링 될 때 전달되는 값에 layout 속성을 추가하고 생성한 레이아웃(mobile)을 넣어 주면 된다.

0개의 댓글