브라우저가 요청을 보내면 서버가 완성된 HTML을 내려주는 방식이 서버사이드 렌더링(SSR) 이다. Express는 Template Engine을 통해 동적 데이터를 HTML에 주입한 뒤 응답으로 전송한다. React 같은 클라이언트 렌더링 없이도 동적 페이지를 만들 수 있는 가장 직접적인 방법이다.
클라이언트 요청
│
▼
Express Router
│
▼
Controller (데이터 조회)
│ { user, posts }
▼
res.render("index", data)
│
▼
Template Engine
(템플릿 파일 + 데이터 → HTML 문자열)
│
▼
완성된 HTML 응답
Express에 Template Engine을 등록하려면 app.set으로 두 가지를 지정한다.
import express from "express";
const app = express();
app.set("view engine", "ejs"); // 사용할 엔진 지정
app.set("views", "./views"); // 템플릿 파일 디렉토리
view engine: Express가 res.render 호출 시 사용할 템플릿 엔진. 패키지 이름과 일치해야 한다.
EJS(Embedded JavaScript) 는 HTML 안에 <% %> 태그로 JavaScript를 삽입하는 방식이다. 기존 HTML 구조를 그대로 유지해 학습 비용이 낮다.
npm install ejs
<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<body>
<h1>Hello, <%= user.name %></h1>
<ul>
<% posts.forEach(post => { %>
<li><%= post.title %></li>
<% }) %>
</ul>
</body>
</html>
| 태그 | 역할 |
|---|---|
<%= expr %> | 값 출력 (HTML 이스케이프) |
<%- expr %> | 값 출력 (이스케이프 없음) |
<% code %> | JavaScript 실행 (출력 없음) |
<%- include('partial') %> | 다른 템플릿 포함 |
Pug 는 들여쓰기 기반 문법으로 HTML 태그를 축약해 표현한다. 코드가 간결하지만 기존 HTML과 문법이 달라 익숙해지는 데 시간이 필요하다.
npm install pug
//- views/index.pug
doctype html
html
body
h1 Hello, #{user.name}
ul
each post in posts
li= post.title
라우터에서 res.render의 두 번째 인자로 객체를 넘기면 템플릿 안에서 해당 변수에 바로 접근할 수 있다.
app.get("/", async (req, res) => {
const user = await getUserById(req.session.userId);
const posts = await getRecentPosts();
res.render("index", { user, posts }); // 템플릿에 데이터 주입
});
공통 헤더·푸터를 매 페이지마다 반복하지 않으려면 레이아웃 상속을 사용한다. EJS에서는 ejs-mate 같은 패키지로, Pug에서는 extends 키워드로 구현한다.
//- views/layout.pug
html
head
title My App
body
block content //- 하위 템플릿이 채워 넣을 영역
//- views/home.pug
extends layout
block content
h1 홈 페이지
p 환영합니다.
block: Pug에서 상위 레이아웃이 정의한 교체 가능한 영역. 하위 템플릿이 같은 이름의 block으로 내용을 오버라이드한다.
| 항목 | EJS | Pug | Handlebars |
|---|---|---|---|
| 문법 | HTML + <% %> | 들여쓰기 | HTML + {{ }} |
| 학습 난이도 | 낮음 | 중간 | 낮음 |
| 로직 표현 | 자유 | 자유 | 제한적 |
| 레이아웃 | 별도 패키지 | 내장 | 내장 |
프로젝트 규모가 작고 팀에 HTML 친숙도가 높다면 EJS, 코드 간결함을 선호하면 Pug, 로직을 템플릿에서 배제하고 싶다면 Handlebars가 적합하다.