Juni_Dev_log

(node.js) [Part.8] 뷰 템플릿 적용하기 - ejs 뷰 템플릿 사용하기 본문

Theorem (정리)/node.js

(node.js) [Part.8] 뷰 템플릿 적용하기 - ejs 뷰 템플릿 사용하기

Juni_K 2021. 1. 30. 16:18

최근에 만들어진 새로운 언어들은 대부분 MVC 패턴(Model-View-Controller 패턴)을 사용한다.

즉, 눈에 보이는 부분은 View / 뷰로 표현되는 데이터를 제공하는 것은 Model / 처리되는 과정을 담당하는 것은 Controller 로 구분하여 구성하면 구조를 더 쉽게 이해할 수 있다.

 

노드와 익스프레스도 지금까지 만든 각각의 기능을 뷰, 모델, 컨트롤러로 나눌 수 있다.

 

사용자 요청을 처리하는 라우팅 함수 ->  컨트롤러(Controller)
데이터베이스에 데이터를 저장하거나 조회하는 함수 -> 모델(Model)
사용자에게 결과를 보여 주기 위해서 만든 파일 -> 뷰(View)

 

그중에서, 뷰에 해당하는 부분을 살펴보면, 지금까지 사용자에게 결과를 응답으로 보낼 때 자바스크립트 코드를 직접 입력하는 방식을 사용했다.

그런데, 이 방식은 각각의 요청을 처리하는 함수마다 응답 코드를 문자열로 넣어줘야하므로 웹 문서를 코드 안에 입력하는 과정에서 오탈자가 생기기 쉽다. 따라서, 웹 문서의 기본적인 형태를 별도의 파일로 미리 만들어두고 사용하는 것이 좋다.

 

이제부터는 응답 웹 문서의 기본 형태를 뷰 템플릿 파일에 만들어 두고 사용한다.

뷰 템플릿을 사용하면, 웹 문서의 기본 형태는 뷰 템플릿으로 만들고 데이터베이스에서 조회한 데이터를 이 템플릿 안의 적당한 위치에 넣어 웹 문서를 만들게 된다.

이렇게 뷰 템플릿을 사용해 결과 웹 문서를 자동으로 생성한 후 응답을 보내는 역할을 하는 것뷰 엔진(View-Engine)이다.

 

익스프레스에서 뷰 엔진의 역할

 

- 웹 브라우저에서 보내온 요청은 웹 서버인 익스프레스에서 컨트롤러로 보낸다.

- 익스프레스에서는 특정 패스로 들어온 요청을 라우팅 함수에서 처리하므로 라우팅 함수를 컨트롤러라고 한다.

- 컨트롤러 안에서는 사용자 요청을 처리하기 위해 mongoose 스키마와 모델 객체를 이용해 데이터베이스를 조회하거나 데이터베이스에 저장한다.

- 이런 역할을 하는 것이 모델이며, 모델에서 처리한 결과는 뷰 엔진으로 전달된다.

 

뷰 엔진은 뷰 템플릿 파일에서 웹 문서의 기본 형태를 읽어 들여 사용자가 보게 될 최종 웹 문서를 만든 후 클라이언트에 응답을 보낸다.

여러가지 방식으로 뷰 템플릿을 만들 수 있는데, 익스프레스에서는 ejs, pug 등 여러가지 뷰 엔진을 지원한다.

그중에서도 ejs 템플릿을 사용해보도록 한다.

 

ejs 를 사용하면, 필요한 부분에만 변수를 삽입하거나 중간중간 자바스크립트 코드를 넣을수도 있다.

따라서 웹 페이지에 익숙한 웹 개발자에게 아주 쉬운 형식이다.

 

뷰 템플릿으로 로그인 웹 문서 만들기

DefaultExample을 복사해서 ViewExample 프로젝트를 만든다.

 

이 프로젝트 안에는 사용자 정보를 처리하는 함수들이 포함되어 있는데, 그중에서 로그인 기능을 처리한 후 응답하는 과정에 뷰 템플릿을 적용해보도록 한다.

 

먼저 app.js 를 열고 뷰엔진을 ejs로 지정한다.

#app.js

1
2
3
4
5
6
7
...
// 뷰 엔진 설정
app.set('views', __dirname + '/views');
app.set('view engine''ejs');
console.log('뷰 엔진이 ejs로 설정되었습니다.');
...
 
cs

 

- app 객체의 set 메소드는 속성을 설정하는 역할을 한다.

- views 속성 값으로 views 폴더를 지정한다. (views 폴더를 만들어야한다.)

- view engine 속성 값으로 ejs를 설정한다.

 

이제 login 함수를 살펴보자.

#user.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
...
var login = function(req, res) {
    console.log('user(user2.js) 모듈 안에 있는 login 호출됨.');
 
    // 요청 파라미터 확인
    var paramId = req.body.id || req.query.id;
    var paramPassword = req.body.password || req.query.password;
    
    console.log('요청 파라미터 : ' + paramId + ', ' + paramPassword);
    
    // 데이터베이스 객체 참조
    var database = req.app.get('database');
    
    // 데이터베이스 객체가 초기화된 경우, authUser 함수 호출하여 사용자 인증
    if (database.db) {
        authUser(database, paramId, paramPassword, function(err, docs) {
            // 에러 발생 시, 클라이언트로 에러 전송
            if (err) {
                console.error('사용자 로그인 중 에러 발생 : ' + err.stack);
                
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h2>사용자 로그인 중 에러 발생</h2>');
                res.write('<p>' + err.stack + '</p>');
                res.end();
                
                return;
            }
            
            // 조회된 레코드가 있으면 성공 응답 전송
            if (docs) {
                console.dir(docs);
 
                // 조회 결과에서 사용자 이름 확인
                var username = docs[0].name;
                
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h1>로그인 성공</h1>');
                res.write('<div><p>사용자 아이디 : ' + paramId + '</p></div>');
                res.write('<div><p>사용자 이름 : ' + username + '</p></div>');
                res.write("<br><br><a href='/public/login.html'>다시 로그인하기</a>");
                res.end();
            
            }
 
...
 
cs

사용자 인증이 성공했을 때 클라이언트에 응답 웹 문서를 보내기 위해서 여러 가지 태그를 입력한 것을 볼 수 있다.

사용자의 아이디와 이름이 변수에 들어 있으므로 + 기호로 다른 문자열과 함께 붙인 다음 응답 객체의 write() 메소드를 호출하여 응답을 보낸다.

이렇게 클라이언트에 응답을 보내기 위해 입력한 코드 중에서 HTML 태그 부분만 새로운 뷰 템플릿 파일로 만든다.

[views] 폴더에 login_success.ejs 파일을 만들고 코드를 입력한다.

 

#login_success.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>로그인 성공 페이지</title>
    </head>
    <body>
        <h1>로그인 성공</h1>
        <div><p>사용자 아이디 : <% = userid %></p></div>
        <div><p>사용자 이름 : <% = username %></p></div>
        <br><br><a href='/public/login.html'>다시 로그인하기</a>
    </body>
</html>
cs

중간에 <% %> 기호가 들어있는데, 이 기호는 자바스크립트 코드를 넣어주는 코드이다.

이 기호 중 앞에 있는 기호에 = 이 붙으면, 바로 뒤에 변수를 넣을 수 있으며, 그 변수의 값을 웹 문서에 출력할 수 있다.

 

뷰 엔진은 이 템플릿 파일을 읽어 들이고 userid와 username 변수의 값으로 해당 부분의 값을 대체한 후 그 결과를 만들어낸다. 이제 이 템플릿 파일응 이용해 응답 웹 문서를 만든 후, 클라이언트에게 응답을 보내도록 user.js 파일의 코드를 수정한다.

#user.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
...
// 조회된 레코드가 있으면 성공 응답 전송
            if (docs) {
                console.dir(docs);
 
                // 조회 결과에서 사용자 이름 확인
                var username = docs[0].name;
                
                // 뷰 템플릿을 사용하여 렌더링 후 전송
                var context = {userid:paramId, username:username};
                req.app.render('login_success', context, function(err, html){
                    if(err){
                        console.error('뷰 렌더링 중 오류 발생 : ' + err.stack);
                        
                        res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                        res.write('<h2>뷰 렌더링 중 오류 발생</h2>');
                        res.write('<p>'+ err.stack +'</p>');
                        res.end();
                        
                        return;
                    }
                    console.log('rendered : ' + html);
                    
                    res.end(html);
                });
            
            }
...
cs

 

익스프레스 서버 객체인 app에는 render() 메소드가 들어있다.

이 메소드를 호출하면 뷰 엔진이 템플릿 파일을 읽어 들인 후, 파라미터로 전달한 context 객체의 속성으로 들어 있는 값들을 적용하고 그 결과를 콜백함수로 돌려준다.

콜백 함수로 전달되는 html 파라미터에는 사용자가 보게 될 최종 웹 문서 코드가 들어가있다.

따라서, res.end() 메소드를 호출하면서 이 html 객체를 파라미터로 전달하면 클라이언트로 응답을 보내게 된다.

 

뷰 엔진이 템플릿 파일로 결과 웹 문서를 만드는 과정

- 뷰 엔진은 뷰 템플릿 파일을 로딩한 후, context 객체의 속성들을 사용해 결과 웹 문서를 만들어 내므로 이 context 객체에 userid 와 username 속성을 추가한다.

- context 객체를 전달받는 템플릿 파일에서는 <% = userid %> <% = username %> 코드를 추가하여 변수에 들어있는 문자열을 출력한다.

 

이제 ejs 모듈을 설치한다.

%npm install ejs --save

app.js 파일을 실행하고 login.html 파일을 연다. 작성하고 로그인하면 해당 웹 문서가 나온다.

 

★ ejs 를 설치했는데, 계속 ejs 모듈을 찾을 수 없다는 오류가 나왔을 때

app.engine('ejs', require('ejs').__express)

코드를 app.js 뷰 엔진 설정에 같이 설정해주면, "ejs 모듈의 참조 경로를 지정할 수 있다."

 

로그인 성공 페이지는 이전에 보았던 것과 같지만, 만들어진 과정은 다르다. 

즉, 이전에는 코드에 태그를 직접 입력한 웹 문서가 응답으로 보낸 것이었지만, 지금은 login_success.ejs 파일에 입력한 태그들이 표시된 것이다.

서버의 콘솔 창을 보면 render() 메소드를 호출했을 때, 뷰 템플릿으로부터 만들어진 결과 웹 문서의 코드를 확인할 수 있다.

 

결과 화면

뷰 템플릿으로 사용자 리스트 웹 문서 만들기

사용자 리스트 요청에 대한 응답으로 보여 줄 뷰 템플릿을 만들어보자.

user.js 파일 안에 들어있는 listuser 함수의 내용은 다음과 같다.

user.js 안에 있는 listuser 함수가 들어있는 부분을 참고하자.

#user.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
var listuser = function(req, res) {
    console.log('user(user2.js) 모듈 안에 있는 listuser 호출됨.');
 
    // 데이터베이스 객체 참조
    var database = req.app.get('database');
    
    // 데이터베이스 객체가 초기화된 경우, 모델 객체의 findAll 메소드 호출
    if (database.db) {
        // 1. 모든 사용자 검색
        database.UserModel.findAll(function(err, results) {
            // 에러 발생 시, 클라이언트로 에러 전송
            if (err) {
                console.error('사용자 리스트 조회 중 에러 발생 : ' + err.stack);
                
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h2>사용자 리스트 조회 중 에러 발생</h2>');
                res.write('<p>' + err.stack + '</p>');
                res.end();
                
                return;
            }
              
            if (results) {
                console.dir(results);
 
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h2>사용자 리스트</h2>');
                res.write('<div><ul>');
                
                for (var i = 0; i < results.length; i++) {
                    var curId = results[i]._doc.id;
                    var curName = results[i]._doc.name;
                    res.write('    <li>#' + i + ' : ' + curId + ', ' + curName + '</li>');
                }    
            
                res.write('</ul></div>');
                res.end();
            }
...
cs

 

데이터베이스에 조회한 사용자 리스트는 results 라는 배열 객체에 들어있다.

따라서 forEach 또는 for 문을 사용해 배열 요소를 확인할 수 있는데 여기에서는 for문을 사용하고 있다.

웹 문서를 만들어 내는 부분을 삭제하고 listuser.ejs 파일을 만든다.

#listuser.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>사용자 리스트 페이지</title>
    </head>
    <body>
        <h2>사용자 리스트</h2>
        <div>
            <ul>
                <% for (var i=0; i <results.length; i++){
                    var curId = results[i]._doc.id;
                    var curName = results[i]._doc.name;%>
                    <li>#<%= i %>- 아이디 : <%= curId %>, 이름 : <%= curName %></li>
                <% } %>
            </ul>
        </div>
        <br><br><a href="public/listuser.html">다시 요청하기</a>
    </body>
</html>
cs

- <% %> 코드 사이에 자바스크립트 코드를 넣을 수 있다. 

- <ul> 태그 사이로 for문 전체를 옮긴후 사용자 아이디와 사용자 이름을 출력해야할 부분을 구분하여 입력한다.

 

# user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
if (results) {
                console.dir(results);
 
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                
                // 뷰 템플릿을 이용하여 렌더링한 후 전송
                var context = {results:results};
                req.app.render('listuser',context,function(err,html){
                    if(err){
                        throw err;
                    }
                    res.end(html);
                });
            } 
...
cs

- 뷰 템플릿에 적용할 context 객체에는 사용자 리스트가 들어 있는 배열 객체를 results 속성 이름 그대로 넣어준다.

 

뷰 템플릿으로 사용자 추가 웹 문서 만들기

adduser.html 을 뷰 템플릿으로 만들어보자.

이번에는 여러 개의 뷰 템플릿 파일에서 공통으로 사용되는 일부 내용을 또 다른 뷰 템플릿 파일로 만들었다가 삽입해서 아용하는 방법을 알아보자.

 

웹 문서에 들어가는 태그 중 <head>태그는 대부분의 웹 문서에서 공통으로 사용되므로 별도의 ejs 파일로 만든 후 listuser.ejs 파일에서 읽어들여 함께 보여준다.

 

#user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
// 결과 객체 있으면 성공 응답 전송
            if (addedUser) {
                console.dir(addedUser);
 
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h2>사용자 추가 성공</h2>');
                res.end();
            } else {  // 결과 객체가 없으면 실패 응답 전송
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h2>사용자 추가  실패</h2>');
                res.end();
            }
...
cs

이것을 adduser.ejs 템플릿 파일로 만들어보자.

#adduser.ejs

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>헤드 부분 - ejs에서 inClude됨</title>
    </head>
    <body>
        <h2><%= title %></h2>
        
        <br><br><a href="/public/login.html">로그인으로 - ejs에서 inClude됨</a>
    </body>
</html>
cs

<head>태그 부분은 head.ejs 부분을 따로 만든다.

#head.ejs

1
2
3
4
<head>
    <meta charset="utf-8">
    <title>헤드 부분 - ejs에서 inClude됨</title>
</head>
cs

<a> 태그도 여러번 쓰이기에,  footer.ejs를 만들어본다.

#footer.ejs

1
<br><br><a href="/public/login.html">로그인으로 - ejs에서 inClude됨</a>
cs

 

이제 adduser.ejs 파일을 다음과 같이 수정하여 분리한 파일을 삽입한다.

#adduser.ejs

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
    <% include ./head.ejs %>
    <body>
        <h2><%= title %></h2>
        
        <% include ./footer.ejs %>
    </body>
</html>
cs

 

였지만... ejs 가 버전 업그레이드가 되면서 경로 설정하는 것이 바뀌었다.

# (최신버전) adduser.ejs

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
    <%-include('head.ejs')%>
    <body>
        <h2><%=title %></h2>
        
        <%-include('footer.ejs')%>
    </body>
</html>
 
 
cs

 

* Includes
Includes either have to be an absolute path, or, if not, are assumed as relative to the template with the include call. For example if you are including ./views/user/show.ejs from ./views/users.ejs you would use <%- include('user/show') %>.

You must specify the filename option for the template with the include call unless you are using renderFile().
You'll likely want to use the raw output tag (<%-) with your include to avoid double-escaping the HTML output.

<ul>  <% users.forEach(function(user){ %>    <%- include('user/show', {user: user}) %>  <% }); %></ul>

Includes are inserted at runtime, so you can use variables for the path in the include call (for example <%- include(somePath) %>).
Variables in your top-level data object are available to all your includes, but local variables need to be passed down.

NOTE: Include preprocessor directives (<% include user/show %>) are not supported in v3.0+.

www.npmjs.com/package/ejs/v/3.1.5

 

ejs

Embedded JavaScript templates

www.npmjs.com

 

- <% %> 사이에 include 키워드를 사용하면, 별도로 분리되어 있는 ejs 파일을 삽입할 수 있다. 이 때 파일 이름은 상대경로로 지정한다.

 

이제 뷰 템플릿으로 결과 웹 문서를 만들 수 있도록 user.ejs 파일을 열어 수정한다.

#user.ejs

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
var adduser = function(req, res) {
    console.log('user(user2.js) 모듈 안에 있는 adduser 호출됨.');
 
    var paramId = req.body.id || req.query.id;
    var paramPassword = req.body.password || req.query.password;
    var paramName = req.body.name || req.query.name;
    
    console.log('요청 파라미터 : ' + paramId + ', ' + paramPassword + ', ' + paramName);
    
    // 데이터베이스 객체 참조
    var database = req.app.get('database');
    
    // 데이터베이스 객체가 초기화된 경우, addUser 함수 호출하여 사용자 추가
    if (database.db) {
        addUser(database, paramId, paramPassword, paramName, function(err, addedUser) {
            // 동일한 id로 추가하려는 경우 에러 발생 - 클라이언트로 에러 전송
            if (err) {
                console.error('사용자 추가 중 에러 발생 : ' + err.stack);
                
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h2>사용자 추가 중 에러 발생</h2>');
                res.write('<p>' + err.stack + '</p>');
                res.end();
                
                return;
            }
            
            // 결과 객체 있으면 성공 응답 전송
            if (addedUser) {
                console.dir(addedUser);
 
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                
                // 뷰 템플릿을 이용하여 렌더링한 후 전송
                var context = {title:'사용자 추가 성공'};
                req.app.render('adduser',context,function(err,html){
                if(err){
                    console.error('뷰 렌더링 중 오류 발생 : ' + err.stack);
                    
                    res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                    res.write('<h2>뷰 렌더링 중 오류 발생</h2>');
                    res.write('<p>'+ err.stack +'</p>');
                    res.end();
                    
                    return;
                }
                console.log("rendered : " + html);
                
                res.end(html);
            })
            } else {  // 결과 객체가 없으면 실패 응답 전송
                res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
                res.write('<h2>사용자 추가  실패</h2>');
                res.end();
            }
        });
    } else {  // 데이터베이스 객체가 초기화되지 않은 경우 실패 응답 전송
        res.writeHead('200', {'Content-Type':'text/html;charset=utf8'});
        res.write('<h2>데이터베이스 연결 실패</h2>');
        res.end();
    }
    
};
cs

 

이제 뷰 템플릿 파일을 만들고 ejs 뷰 엔진으로 변환한 후, 응답으로 보내는 방법에 대해서 알아보았다.

Comments