문제: HTML 파일에 서버 데이터를 어떻게 넣을까?
해결: 템플릿 엔진 사용으로 서버사이드 렌더링 구현
npm install ejs
// server.js 상단에 추가
app.set('view engine', 'ejs')
프로젝트/
├── server.js
├── views/ ← EJS 파일들이 들어갈 폴더 (필수!)
│ ├── list.ejs
│ ├── nav.ejs
│ └── time.ejs
└── public/
└── style.css
<!-- views/list.ejs -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시물 목록</title>
<style>
.grey-bg { background: #eee; }
.white-bg {
background: white;
margin: 20px;
border-radius: 5px;
}
.list-box {
padding: 10px;
border-bottom: 1px solid #eee;
}
.list-box h4 {
font-size: 16px;
margin: 5px;
}
.list-box p {
font-size: 13px;
margin: 5px;
color: grey;
}
</style>
</head>
<body class="grey-bg">
<div class="white-bg">
<div class="list-box">
<h4>글제목임</h4>
<p>글내용임</p>
</div>
<div class="list-box">
<h4>글제목임</h4>
<p>글내용임</p>
</div>
</div>
</body>
</html>
app.get('/list', (요청, 응답) => {
응답.render('list.ejs') // sendFile이 아닌 render 사용!
})
app.get('/list', async (요청, 응답) => {
let result = await db.collection('post').find().toArray()
응답.render('list.ejs', { 글목록: result })
// { 변수명: 전송할데이터 }
})
<!-- views/list.ejs -->
<body class="grey-bg">
<!-- 데이터 확인용 (개발 중에만 사용) -->
<%= JSON.stringify(글목록) %>
<div class="white-bg">
<div class="list-box">
<h4><%= 글목록[0].title %></h4>
<p><%= 글목록[0].content %></p>
</div>
<div class="list-box">
<h4><%= 글목록[1].title %></h4>
<p><%= 글목록[1].content %></p>
</div>
</div>
</body>
<!-- 1. 텍스트 출력 -->
<%= 변수명 %>
<!-- 2. HTML 렌더링 -->
<%- HTML포함변수 %>
<!-- 3. JavaScript 코드 실행 -->
<% JavaScript코드 %>
<!-- 서버에서 전송된 데이터 -->
<!-- htmlContent = "<button>클릭</button>" -->
<!-- <%= %> 사용: 텍스트로 출력 -->
<%= htmlContent %>
<!-- 결과: <button>클릭</button> (텍스트) -->
<!-- <%- %> 사용: HTML로 렌더링 -->
<%- htmlContent %>
<!-- 결과: [클릭] (실제 버튼) -->
<!-- views/list.ejs -->
<div class="white-bg">
<% for (let i = 0; i < 글목록.length; i++) { %>
<div class="list-box">
<h4><%= 글목록[i].title %></h4>
<p><%= 글목록[i].content %></p>
</div>
<% } %>
</div>
<div class="white-bg">
<% 글목록.forEach(function(글, index) { %>
<div class="list-box">
<h4><%= 글.title %></h4>
<p><%= 글.content %></p>
<small>글 번호: <%= index + 1 %></small>
</div>
<% }) %>
</div>
<% 글목록.forEach(function(글) { %>
<div class="list-box">
<h4>
<%= 글.title %>
<% if (글.views && 글.views > 100) { %>
<span class="hot-badge">🔥 HOT</span>
<% } %>
</h4>
<p><%= 글.content %></p>
<% if (글.author) { %>
<small>작성자: <%= 글.author %></small>
<% } else { %>
<small>익명</small>
<% } %>
</div>
<% }) %>
<!-- views/nav.ejs -->
<div class="nav" style="background: #333; padding: 15px;">
<a href="/" style="color: white; text-decoration: none; font-weight: bold;">
🍎 Apple Forum
</a>
<a href="/list" style="color: white; text-decoration: none; margin-left: 20px;">
게시물 목록
</a>
<a href="/write" style="color: white; text-decoration: none; margin-left: 20px;">
글쓰기
</a>
</div>
<!-- views/list.ejs -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시물 목록</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<%- include('nav.ejs') %> <!-- 네비게이션 삽입 -->
<div class="container">
<h1>게시물 목록</h1>
<!-- 게시물 목록 내용 -->
</div>
</body>
</html>
<!-- views/components/post-card.ejs -->
<div class="post-card">
<h3><%= post.title %></h3>
<p><%= post.content %></p>
<div class="post-meta">
<span>작성일: <%= new Date(post.date).toLocaleDateString() %></span>
<span>조회수: <%= post.views || 0 %></span>
</div>
</div>
app.get('/time', (요청, 응답) => {
let currentTime = new Date()
응답.render('time.ejs', {
시간: currentTime,
포맷된시간: currentTime.toLocaleString('ko-KR')
})
})
<!-- views/time.ejs -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>현재 시간</title>
<style>
.time-container {
text-align: center;
margin-top: 100px;
font-family: Arial, sans-serif;
}
.current-time {
font-size: 3rem;
color: #007bff;
margin: 20px 0;
}
.refresh-btn {
background: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
}
</style>
</head>
<body>
<%- include('nav.ejs') %>
<div class="time-container">
<h1>서버 현재 시간</h1>
<div class="current-time">
<%= 포맷된시간 %>
</div>
<p>서버 원시 시간: <%= 시간 %></p>
<button class="refresh-btn" onclick="location.reload()">
새로고침
</button>
</div>
</body>
</html>
1. 사용자가 /list 요청
2. 서버에서 DB 데이터 조회
3. EJS로 HTML 완성
4. 완성된 HTML을 사용자에게 전송
장점:
단점:
1. 사용자가 /list 요청
2. 서버에서 빈 HTML + JavaScript 전송
3. JavaScript가 API로 데이터 요청
4. 브라우저에서 HTML 생성
장점:
단점:
| 메서드 | 용도 | 예시 |
|---|---|---|
| GET | 데이터 조회 | 게시물 목록 보기 |
| POST | 데이터 생성 | 새 게시물 작성 |
| PUT | 데이터 전체 수정 | 게시물 전체 내용 변경 |
| PATCH | 데이터 부분 수정 | 게시물 제목만 변경 |
| DELETE | 데이터 삭제 | 게시물 삭제 |
// 게시물 목록 조회
app.get('/api/posts', async (요청, 응답) => {
let posts = await db.collection('post').find().toArray()
응답.json(posts)
})
// 특정 게시물 조회
app.get('/api/posts/:id', async (요청, 응답) => {
let post = await db.collection('post').findOne({
_id: new ObjectId(요청.params.id)
})
응답.json(post)
})
// 게시물 생성
app.post('/api/posts', async (요청, 응답) => {
let newPost = {
title: 요청.body.title,
content: 요청.body.content,
date: new Date()
}
await db.collection('post').insertOne(newPost)
응답.json({ success: true })
})
// ✅ 좋은 예: 일관된 패턴
app.get('/api/posts', ...) // 모든 게시물
app.get('/api/posts/:id', ...) // 특정 게시물
app.post('/api/posts', ...) // 게시물 생성
app.put('/api/posts/:id', ...) // 게시물 수정
app.delete('/api/posts/:id', ...) // 게시물 삭제
// ❌ 나쁜 예: 일관성 없음
app.get('/getAllPosts', ...)
app.get('/getPostById/:id', ...)
app.post('/createNewPost', ...)
// ✅ 좋은 예: 명사 사용
/api/posts // 게시물들
/api/users // 사용자들
/api/comments // 댓글들
// ❌ 나쁜 예: 동사 사용
/api/getPosts
/api/createUser
/api/deleteComment
// ✅ 좋은 예: 계층적 구조
/api/posts/123/comments // 123번 게시물의 댓글들
/api/users/456/posts // 456번 사용자의 게시물들
/api/categories/tech/posts // 기술 카테고리의 게시물들
// ❌ 나쁜 예: 평면적 구조
/api/postComments?postId=123
/api/userPosts?userId=456
http://localhost:8080/list
<a href="/list">게시물 목록 보기</a>
<a href="/post/123">123번 게시물 보기</a>
fetch('/api/posts')
.then(response => response.json())
.then(data => console.log(data))
<form action="/create" method="POST">
<input name="title" placeholder="제목">
<textarea name="content" placeholder="내용"></textarea>
<button type="submit">작성</button>
</form>
<!-- views/list.ejs -->
<div class="post-stats">
<p>전체 게시물: <%= 글목록.length %>개</p>
<p>최근 게시물:
<%
let recentPosts = 글목록.filter(post => {
let postDate = new Date(post.date)
let weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
return postDate > weekAgo
})
%>
<%= recentPosts.length %>개
</p>
</div>
<% 글목록.forEach((글, index) => { %>
<div class="list-box <%= index % 2 === 0 ? 'even' : 'odd' %>">
<h4><%= 글.title %></h4>
<p><%= 글.content.substring(0, 100) %>...</p>
<small><%= new Date(글.date).toLocaleDateString() %></small>
</div>
<% }) %>
app.get('/list', async (요청, 응답) => {
try {
let result = await db.collection('post').find().toArray()
응답.render('list.ejs', {
글목록: result,
에러: null
})
} catch (error) {
응답.render('list.ejs', {
글목록: [],
에러: '데이터를 불러올 수 없습니다.'
})
}
})
<!-- views/list.ejs -->
<% if (에러) { %>
<div class="error-message">
<p style="color: red;"><%= 에러 %></p>
</div>
<% } else if (글목록.length === 0) { %>
<div class="empty-message">
<p>게시물이 없습니다.</p>
</div>
<% } else { %>
<!-- 정상적인 게시물 목록 출력 -->
<% 글목록.forEach(글 => { %>
<!-- 게시물 내용 -->
<% }) %>
<% } %>
npm install ejs + app.set('view engine', 'ejs')views/ 폴더에 .ejs 파일 보관응답.render('파일명.ejs', {변수명: 데이터})<%= %> (출력), <%- %> (HTML), <% %> (JavaScript)<%- include('파일명.ejs') %>EJS 템플릿 엔진으로 동적인 웹 페이지를 정리했습니다.