Node.js - MVC 패턴과 View Template Engine
Express에서 MVC 패턴의 개념을 이해하고, View를 동적으로 생성하기 위한 템플릿 엔진의 사용법을 학습합니다.
MVC 패턴과 View Template Engine
지금까지 우리는 res.send()
나 res.json()
을 사용하여 클라이언트에 텍스트나 JSON 데이터를 응답했습니다. 하지만 실제 웹 애플리케이션은 사용자가 볼 수 있는 완성된 HTML 페이지를 동적으로 생성하여 제공해야 합니다. 이 포스팅은 이를 효율적으로 처리하기 위한 MVC 패턴과 View 템플릿 엔진에 대해 소개합니다.
1. MVC 패턴의 이해
MVC는 Model-View-Controller의 약자로, 애플리케이션의 구조를 세 가지 역할로 구분하는 디자인 패턴입니다. 코드의 재사용성을 높이고, 비즈니스 로직과 사용자 인터페이스를 분리하여 유지보수를 용이하게 만드는 것이 목적입니다.
- Model: 데이터와 비즈니스 로직을 담당합니다. 데이터베이스와 상호작용하며 데이터를 가져오거나 저장, 수정, 삭제하는 역할을 수행합니다.
- View: 사용자에게 보여지는 UI(사용자 인터페이스)를 담당합니다. 컨트롤러로부터 받은 데이터를 사용하여 동적인 HTML 페이지를 생성합니다.
- Controller: 모델과 뷰 사이의 중재자 역할을 합니다. 클라이언트의 요청(Request)을 받아, 어떤 모델을 사용할지와 어떤 뷰를 보여줄지를 결정하고, 모델로부터 받은 데이터를 가공하여 뷰에 전달합니다.
우리가 이전 포스팅에서 controllers
폴더를 만들고 라우팅 함수들을 분리한 것이 바로 이 MVC 패턴의 Controller 부분을 구현한 것입니다. 오늘은 이 Controller가 View와 어떻게 상호작용하는지에 대해 집중적으로 알아볼 것입니다.
Controller와 View의 관계
- 클라이언트가 특정 URL로 요청을 보냅니다.
- Express 라우터는 해당 URL에 맞는 컨트롤러 함수를 호출합니다.
- 컨트롤러는 필요한 데이터를 모델로부터 가져옵니다. (이번 시간에는 모델 대신 간단한 JSON 데이터를 사용합니다.)
- 컨트롤러는
res.render()
함수를 호출하여, 사용할 뷰(View) 파일의 이름과 뷰에 전달할 데이터를 넘겨줍니다. - 뷰는 전달받은 데이터를 자신(HTML)의 정해진 위치에 동적으로 삽입하여 최종 HTML 페이지를 완성합니다.
- 완성된 HTML이 클라이언트에게 응답(Response)으로 전송됩니다.
2. View 템플릿 엔진
위 과정에서 5번 항목, 즉 “데이터를 HTML에 동적으로 삽입하여 최종 페이지를 완성”하는 역할을 수행하는 것이 바로 템플릿 엔진(Template Engine)입니다.
템플릿 엔진을 사용하면, 정적인 HTML 코드 안에 변수, 조건문, 반복문 등을 사용하여 동적으로 변하는 부분을 프로그래밍 방식으로 채워 넣을 수 있습니다.
Node.js의 주요 템플릿 엔진
Node.js 생태계에는 다양한 템플릿 엔진이 있으며, 각각의 문법과 특징이 다릅니다.
템플릿 엔진 | 특징 | 장점 | 단점 |
---|---|---|---|
EJS | Embedded JavaScript . HTML과 유사한 문법에 <% ... %> 태그를 사용하여 JavaScript 코드를 삽입. | 배우기 매우 쉽고, 기존 HTML 디자이너와의 협업이 용이함. | 기능이 비교적 단순함. |
Pug | (구 Jade) 들여쓰기 기반의 간결한 문법을 사용. HTML 태그를 직접 사용하지 않음. | 코드가 매우 간결하고 가독성이 높음. | HTML과 문법이 달라 별도의 학습이 필요함. |
Handlebars | `` 형태의 논리 없는(logic-less) 문법을 지향. | 뷰와 로직의 분리가 명확함. 다양한 헬퍼 함수 지원. | 간단한 조건/반복 외의 복잡한 로직 구현이 어려움. |
어떤 것을 선택해야 할까?
Google Trends 및 npm 다운로드 수에 따르면, EJS가 가장 널리 사용되고 있으며, 특히 초보자에게 인기가 많습니다. HTML 문법을 거의 그대로 사용하기 때문에 배우기 쉽고 직관적이라는 큰 장점이 있습니다.
따라서 이 포스팅에서는 EJS를 사용하여 View를 만드는 방법을 학습하겠습니다.
3. EJS를 이용한 View 구현 실습
이제 EJS를 사용하여 동적인 웹 페이지를 만드는 과정을 단계별로 실습해 보겠습니다.
1단계: EJS 설치 및 설정
먼저, ejs
패키지를 설치하고 Express 앱에 템플릿 엔진으로 설정합니다.
1) EJS 설치
1
$ yarn add ejs
2) app.js에 View Engine 설정
Express에게 우리가 사용할 템플릿 엔진이 ejs
이고, 템플릿 파일들은 views
폴더에 있다는 것을 알려주어야 합니다.
실습: /app.js
app.js
파일의3) Express 객체 생성 및 설정
부분에 다음 코드를 추가합니다.
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
/*----------------------------------------------------------
* 2) 필요한 모듈 로드
*----------------------------------------------------------*/
import logger from "./helpers/LogHelper.js";
import utilHelper from "./helpers/UtilHelper.js";
import routeHelper from './helpers/RouteHelper.js';
import express from "express"; // Express 모듈 가져오기
// highlight-start
import ejs from 'ejs'; // ejs 모듈 가져오기
// highlight-end
// ... (기존 코드) ...
/*-----------------------------------------------------------
* 3) Express 객체 생성 및 설정
*----------------------------------------------------------*/
// ... (기존 코드) ...
const app = express();
// ... (기존 코드) ...
// highlight-start
// 6) View 엔진 설정
// View 엔진이란 서버에서 동적으로 HTML 파일을 생성하기 위한 도구이다.
// 다양한 View 엔진이 있지만, 여기서는 ejs 모듈을 사용한다.
// ejs 모듈은 HTML 파일 내에 자바스크립트 코드를 삽입할 수 있게 해준다.
app.engine('html', ejs.renderFile);
app.set('view engine', 'html');
app.set('views', 'views');
// highlight-end
app.set('view engine', 'ejs')
: Express의 뷰 엔진으로ejs
를 사용하겠다고 설정합니다.app.set('views', 'views')
: 템플릿 파일들이 위치한 디렉토리를views
로 지정합니다.
💡 Tip: 뷰 파일 확장자로
.html
사용하기템플릿 파일의 확장자를
.ejs
가 아닌.html
로 사용하고 싶을 수도 있습니다. HTML 파일은 VSCode의 Emmet 기능이나 Prettier와 같은 코드 포매터의 지원을 더 잘 받을 수 있기 때문입니다.
.html
파일을 EJS 템플릿 엔진으로 렌더링하려면app.js
에 다음과 같이 설정을 추가해야 합니다.
1 2 3 4 5 6 7 8 // app.js import ejs from 'ejs'; // ... // .html 파일을 EJS 템플릿 엔진으로 렌더링하도록 설정 app.engine('html', ejs.renderFile); // 기본 뷰 엔진을 html로 변경 app.set('view engine', 'html');이 수업의 나머지 예제는 이 방식을 사용하여
.html
확장자를 기준으로 진행하겠습니다.
2단계: 기본 View 파일 생성 및 렌더링
Controller에서 View 파일을 렌더링하는 가장 기본적인 방법을 알아봅니다.
1) 컨트롤러 생성
먼저, MVC 패턴 실습을 위한 새로운 컨트롤러 파일을 생성합니다.
실습: /controllers/MvcController.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { GET } from '../helpers/RouteHelper.js';
import logger from '../helpers/LogHelper.js';
export const MvcController = () => {
/**
* res.render() 메서드를 사용하여 뷰 템플릿 렌더링
*
* res.render(view, locals)
* - view: 렌더링할 뷰 템플릿 파일의 이름 (확장자 제외)
* - locals: 템플릿에 전달할 데이터 객체 (JSON)
*/
GET('/page1')((req, res, next) => {
// 뷰에 전달할 데이터
const data = {
name: 'Express',
age: 20
};
// `views/index.html` 파일을 렌더링하면서 data 객체를 전달
res.render('index', data);
});
};
2) 뷰 파일 생성
views
폴더 안에 index.html
파일을 생성하고, Controller에서 전달받은 데이터를 출력합니다.
실습: /views/index.html
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
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>EJS Template</title>
</head>
<body>
<h1>EJS Template Example</h1>
<h2>기본 표현식</h2>
<!--
<%=...%>: 변수 값을 HTML에 출력 (HTML-escaped)
- HTML 태그는 문자열로 변환되어 출력됩니다.
-->
<p>Hello, <%=name%>! You are <%=age%> years old.</p>
<h2>Unescaped 표현식</h2>
<%
const mytag = '<strong>EJS</strong>';
%>
<!--
<%-...%>: 변수 값을 HTML에 그대로 출력 (Unescaped)
- HTML 태그가 그대로 렌더링됩니다. 보안에 유의해야 합니다.
-->
<p>Unescaped: <%-mytag%></p>
</body>
</html>
이제 서버를 실행하고 브라우저에서 http://localhost:8080/page1
로 접속하면, name
과 age
가 동적으로 채워진 HTML 페이지를 확인할 수 있습니다.
3단계: 조건문과 반복문
EJS에서는 <% ... %>
태그를 사용하여 JavaScript의 조건문(if
)과 반복문(for
, forEach
)을 그대로 사용할 수 있습니다.
1) 컨트롤러 수정
department.html
뷰를 렌더링하는 새로운 라우팅 함수를 추가합니다. 배열 데이터를 뷰에 전달합니다.
실습: /controllers/MvcController.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ... (기존 코드) ...
export const MvcController = () => {
// ... (기존 /page1 라우팅 함수) ...
/** 조건문과 반복문 */
GET('/page2')((req, res, next) => {
// 부서 목록 데이터
const departments = [
{ deptno: 101, dname: '컴퓨터공학과', loc: '1호관' },
{ deptno: 102, dname: '멀티미디어학과', loc: '2호관' },
{ deptno: 201, dname: '전자공학과', loc: '3호관' },
{ deptno: 202, dname: '기계공학과', loc: '4호관' }
];
// `views/department.html` 파일을 렌더링하면서 데이터를 전달
res.render('department', { depts: departments });
});
};
2) 뷰 파일 생성
views
폴더에 department.html
파일을 생성하고, forEach
와 if
를 사용하여 데이터를 테이블 형태로 출력합니다.
실습: /views/department.html
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
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Department List</title>
<style>
table { border-collapse: collapse; width: 50%; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: center; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>학과 목록</h1>
<table>
<thead>
<tr>
<th>학과번호</th>
<th>학과명</th>
<th>위치</th>
</tr>
</thead>
<tbody>
<% if (depts.length > 0) { %>
<% depts.forEach(dept => { %>
<tr>
<td><%= dept.deptno %></td>
<td><%= dept.dname %></td>
<td><%= dept.loc %></td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="3">데이터가 없습니다.</td>
</tr>
<% } %>
</tbody>
</table>
</body>
</html>
http://localhost:8080/page2
로 접속하면, departments
배열의 내용이 테이블 형태로 동적으로 생성된 것을 확인할 수 있습니다.
4단계: 공통 요소 재사용 (Partials)
여러 페이지에서 공통으로 사용되는 헤더(header), 푸터(footer), 네비게이션 바 등은 별도의 파일로 분리하여 재사용할 수 있습니다. 이러한 조각 파일을 파셜(Partial)이라고 합니다.
1) 파셜 파일 생성
공통 요소를 담을 partials
폴더를 views
폴더 안에 생성하고, header.html
와 footer.html
파일을 만듭니다.
실습: /views/partials/header.html
1
2
3
4
5
6
7
8
<header>
<h1>My EJS Website</h1>
<nav>
<a href="/page1">Page 1</a> |
<a href="/page2">Page 2</a>
</nav>
<hr/>
</header>
실습: /views/partials/footer.html
1
2
3
4
<footer>
<hr/>
<p>Copyright © 2025. All rights reserved.</p>
</footer>
2) 뷰 파일에 파셜 포함하기
index.html
와 department.html
파일의 상단과 하단에 <%- include('파일경로') %>
구문을 사용하여 파셜 파일을 포함시킵니다.
중요:
include
를 사용할 때는=
가 아닌-
를 사용한<%- ... %>
구문을 사용해야 합니다. 이는 파셜 파일의 HTML 내용이 이스케이프 처리되지 않고 그대로 렌더링되도록 하기 위함입니다.또한,
include
할 파일의 확장자까지 명시해 주어야 합니다.
실습: /views/index.html
(수정)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>EJS Template</title>
</head>
<body>
<%- include('partials/header.html') %>
<h2>기본 표현식</h2>
<p>Hello, <%= name %>! You are <%= age %> years old.</p>
<h2>Unescaped 표현식</h2>
<% const mytag = '<strong>EJS</strong>'; %>
<p>Unescaped: <%- mytag %></p>
<%- include('partials/footer.html') %>
</body>
</html>
실습: /views/department.html
(수정)
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
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Department List</title>
<style>
table { border-collapse: collapse; width: 50%; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: center; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<%- include('partials/header.html') %>
<h1>학과 목록</h1>
<table>
<!-- ... (테이블 내용은 동일) ... -->
<tbody>
<% if (depts.length > 0) { %>
<% depts.forEach(dept => { %>
<tr>
<td><%= dept.deptno %></td>
<td><%= dept.dname %></td>
<td><%= dept.loc %></td>
</tr>
<% }); %>
<% } else { %>
<tr>
<td colspan="3">데이터가 없습니다.</td>
</tr>
<% } %>
</tbody>
</table>
<%- include('partials/footer.html') %>
</body>
</html>
이제 다시 /page1
과 /page2
에 접속해 보면, 두 페이지 모두에 동일한 헤더와 푸터가 적용된 것을 확인할 수 있습니다. 이렇게 파셜을 사용하면 공통 요소의 수정이 필요할 때 해당 파셜 파일 하나만 수정하면 되므로 유지보수성이 크게 향상됩니다.