Node Express에 handlebars 템플릿엔진 적용하기(레이아웃적용)

들어가기

Node Express환경에서 Pug와 EJS템플릿 엔진을 많이 사용하는 듯 하다. 내가 보고 있는 책에서도 Pug위주의 예제로 진행이 되었다.

하지만 막상 Express예제 프로젝트를 만들어 써보니 Pug이녀석 여간 까다로운 것이 아니다. 익숙해 진다면 굉장히 쉽게 태그를 작성할수는 있지만, 문제는 HTML문법을 너무 회손한다는 것이다.

즉, 인터넷에서 쉽게 구할수 있는 bootstrap 템플릿 같은것을 적용하기가 어렵다는 소리이다. 차선책으로 EJS를 써볼까 했으나, 이것도 영 HTML 친화적이지 못한것 같았다. 더군다나 jsp 스크립틀릿 같아서...

구글링을 해보니 handlebars라는 템플릿을 찾게 되었다. 얼마전에 Spring 웹프로젝트 예제를 만들때 thymeleaf와 고민했던 그 템플릿 엔진을 Node에서도 사용할수 있었다.

handlebars는 다른 템플릿 엔진에 비해 상대적으로 HTML을 거의 회손하지 않았고, 서버사이드뿐만 아니라 클라이언트 사이드에서도 단독으로 동작할수 있는 장점이 있다.

https://www.slant.co/versus/181/184/~handlebars-js_vs_ejs 위 사이트에서 Handlebars.js와 EJS를 비교하는 설문을 했는데 Handlebars가 더 많은 지지를 받고 있다.

이 글은 내가 예제로 Express 프로젝트에 Handlebars 템플릿 엔진을 적용하는 글이다. Handlerbars 템플릿 적용 말고도, mysql 시퀄라이저등 다른 설정도 포함한 글이다.

참고로 Node에서 express-handlebars라는 모듈을 사용한다.

기초 Express 프로젝트 생성(수동생성)

참고로 책을 보고 따라한 예제이며, Express cli를 사용하지 않고 수동으로 프로젝트를 생성했다.(템플릿 엔진을 선택하기 싫어서...) 개개인 입맛에 맞게 Express 프로젝트를 생성해도 된다.

프로젝트 폴더로 backend라는 폴더를 만들고, 다음 명령어로 node 프로젝트 생성

1
npm init -y

생성된 package.json를 아래처럼 수정

1
2
3
4
5
6
7
8
9
10
11
{
"name": "backend",
"version": "0.0.1",
"description": "bnad_of_coder express backend",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "hanumoka",
"license": "MIT"
}

mysqlDB와 sequleize를 사용하기 위해 모듈 설치

handlebars를 적용하는데 필요없다면 넘어가자.

1
2
3
npm i -g sequelize-cli
npm i sequelize mysql2
sequelize init

시퀄라이저 기본폴더 4개 생성됨 config, migrations, models, seeders

수동으로 views, routers, public, passport를 만들어준다.

backend 폴더에 app.js 파일을 생성한다.

app.js파일은 pakcage.json에서 설정한대로 Express의 엔트리 파일이 된다.

Express와 그 관련 모듈과, express-handlebars를 설치한다.

기타 모듈들을 추가한다. 템플릿 엔진은 handlebars를 사용한다. express-handlebars라는 모듈을 설치해야 한다.

1
2
3
4
5
6
7
8
npm i express
npm i cookie-parser
npm i express-session
npm i morgan
npm i connect-flash
npm i express-handlebars
npm i -g nodemon
npm i -D nodemon

nodemon모듈은 서버 코드에 수정이 발생하면 자동으로 재시작 해준다. 실행되는 콘솔에서 rs를 입력해서 수동으로 재시작 할 수 있다. nodemon은 개발용(배포용 아님)이다.

1
npm i dotenv

dotenv 모듈을 추가 설치하고, backend 폴더에 .env 파일을 만들고 아래 내용을 채운다. 이 것은 서버에서 쿠키, 세션용 비밀키 관리용이다.

1
COOKIE_SECRET=hanumokasecret

app.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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const express = require('express');
const hbs = require('express-handlebars'); // handlebars 템플릿 엔진
const cookieParser = require('cookie-parser');
const morgan = require('morgan');
const path = require('path');
const session = require('express-session');
const flash = require('connect-flash');
require('dotenv').config(); // 비밀키 관리


//TEST
const userTestRouter = require('./routes/test/user_test');

const app = express();

//** handlebars 핵심 설정 시작 **//
app.engine( 'hbs', hbs( {
extname: 'hbs',
defaultLayout: 'main', // 기본 레이아웃 파일 main.hbs 지정, 요청에 따라 레이아웃을 변경할수 있다.
layoutsDir: __dirname + '/views/layouts/', // 헨들바템플릿의 레이아웃 파일의 위치
partialsDir: __dirname + '/views/partials/' // 파티셜이란: 레이아웃을 채울 header.hbs, left.hbs, footer.hbs 파일의 위치
} ) );

app.set( 'view engine', 'hbs' ); // handlebars파일의 확장자를 hbs로 사용.

//** handlebars 핵심 설정 끝 **//

app.set('port', process.env.PORT || 8001);

app.use(morgan('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
}));
app.use(flash());

app.get('/', function (req, res) {
res.render('index');
});

//TEST ROUTER
app.use('/test', userTestRouter);

app.use((req, res, next) => {
const err = new Error('Not Found');
err.status = 404;
next(err);
});

app.use((err, req, res) => {
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.status(err.status || 500);
res.render('error');
});

app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기중');
});

Handlerbars관련 디렉토리와 파일들

위의 app.js에서 Handlebars의 핵심 소스를 추리면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
const hbs = require('express-handlebars');

app.engine( 'hbs', hbs( {
extname: 'hbs',
defaultLayout: 'main',
layoutsDir: __dirname + '/views/layouts/',
partialsDir: __dirname + '/views/partials/'
} ) );

app.set( 'view engine', 'hbs' );

그리고 주요 파일들은 아래와 같다. 파일들을 생성해주자.

디폴트 레이아웃 : views/layouts/main.hbs 레이아웃에 사용할 header : views/partials/header.hbs 레이아웃에서 사용할 footer : views/partials/footer.hbs 레이아웃에서 사용할 leaf : views/partials/left.hbs 인덱스 페이지 : views/index.hbs

내 폴더구조

예상하는 레이아웃 구조

중요한 것은 레이아웃을 담당하는 main.hbs일 것이다.

아래는 main.hbs 이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Example App</title>
</head>
<body>
...
{{> header }}
{{> left }}
...
{{{body}}}
...
{{> footer }}
</body>
</html>

body와 나머지의 무스타치 표현이 살작 다른것을 확인 할 수 있다. body는 라우터에서 바꿔줄 페이지의 컨텐츠가 된다.

나머지 header.hbs, footer.hbs, left.hbs와 index.hbs에는 대충 구별할수 있게 텍스트를 쳐주자.

예를 들어 header.hbs에는 아래처럼

1
<h1>헤더 입니다.</h1>

위 app.js에 기본 인덱스 라우터가 설정되어 있다.

1
2
3
app.get('/', function (req, res) {
res.render('index');
});

서버를 npm start로 실행하고 http://localhost:8001로 접근하면, 레이아웃이 적용된 index.hbs 파일이 보이는 것을 확인 할 수 있다.

http://avilos.codes/server/nodejs/node-js-express-and-handlebars/ 이 글을 보면, 라우팅 할때 레이아웃을 변경할수도 있다.

나는 부트스트랩4 템플릿 https://github.com/puikinsh/ElaAdmin를 적용하여 다음과 같이 화면이 나온다.

index.hbs

그리고 body페이지를 하나 추가하고 아래처럼, body가 바뀌는 것을 확인 할 수 있다.

test.hbs

예제끝!!!

참고자료

http://avilos.codes/server/nodejs/node-js-express-and-handlebars/

https://stackoverflow.com/questions/16385173/node-js-express-handlebars-js-partial-views

https://github.com/ericf/express-handlebars

핸들바 문법 https://programmingsummaries.tistory.com/381

부트스트렙4 어드민 테마 https://github.com/puikinsh/ElaAdmin

기타. Express favicon 처리(위 예제와 상관 없음)

파비콘이 없어서 404 뜨는 경우. https://www.npmjs.com/package/serve-favicon

1
npm install serve-favicon

1
2
3
4
5
6
7
8
9
10
var express = require('express')
var favicon = require('serve-favicon')
var path = require('path')

var app = express()
app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')))

// Add your routes here, etc.

app.listen(3000)