[Node.js-02] EJS(템플릿 엔진) & API 설계

Comely·2025년 3월 12일

Node.js

목록 보기
2/14

🎯 템플릿 엔진이 필요한 이유

문제: HTML 파일에 서버 데이터를 어떻게 넣을까?

해결: 템플릿 엔진 사용으로 서버사이드 렌더링 구현

🛠️ EJS 설치 및 설정

1. EJS 설치

npm install ejs

2. Express에 EJS 설정

// server.js 상단에 추가
app.set('view engine', 'ejs')

3. 폴더 구조 생성

프로젝트/
├── server.js
├── views/           ← EJS 파일들이 들어갈 폴더 (필수!)
│   ├── list.ejs
│   ├── nav.ejs
│   └── time.ejs
└── public/
    └── style.css

📄 EJS 파일 생성과 기본 사용법

list.ejs 파일 생성

<!-- 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>

EJS 파일 응답하기

app.get('/list', (요청, 응답) => {
    응답.render('list.ejs')  // sendFile이 아닌 render 사용!
})

📊 데이터를 EJS로 전송하기

1단계: 서버에서 데이터 전송

app.get('/list', async (요청, 응답) => {
    let result = await db.collection('post').find().toArray()
    응답.render('list.ejs', { 글목록: result })
    //                    { 변수명: 전송할데이터 }
})

2단계: EJS에서 데이터 출력

<!-- 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>

🔄 EJS 문법 완전 정리

기본 출력 문법

<!-- 1. 텍스트 출력 -->
<%= 변수명 %>

<!-- 2. HTML 렌더링 -->
<%- HTML포함변수 %>

<!-- 3. JavaScript 코드 실행 -->
<% JavaScript코드 %>

<%= %> vs <%- %> 차이점

<!-- 서버에서 전송된 데이터 -->
<!-- htmlContent = "<button>클릭</button>" -->

<!-- <%= %> 사용: 텍스트로 출력 -->
<%= htmlContent %>
<!-- 결과: <button>클릭</button> (텍스트) -->

<!-- <%- %> 사용: HTML로 렌더링 -->
<%- htmlContent %>
<!-- 결과: [클릭] (실제 버튼) -->

JavaScript 문법 활용

For 반복문

<!-- 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>

forEach 반복문

<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>
<% }) %>

🧩 Include를 활용한 모듈화

공통 네비게이션 분리

<!-- 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>

다른 파일에서 Include 사용

<!-- 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')
    })
})

time.ejs 파일

<!-- 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>

🌐 서버사이드 vs 클라이언트사이드 렌더링

서버사이드 렌더링 (우리가 하는 방식)

1. 사용자가 /list 요청
2. 서버에서 DB 데이터 조회
3. EJS로 HTML 완성
4. 완성된 HTML을 사용자에게 전송

장점:

  • SEO 최적화
  • 빠른 초기 로딩
  • JavaScript 비활성화 환경에서도 동작

단점:

  • 페이지 이동 시 전체 새로고침
  • 서버 부하 증가

클라이언트사이드 렌더링

1. 사용자가 /list 요청
2. 서버에서 빈 HTML + JavaScript 전송
3. JavaScript가 API로 데이터 요청
4. 브라우저에서 HTML 생성

장점:

  • 앱 같은 부드러운 사용자 경험
  • 서버 부하 감소

단점:

  • 초기 로딩 느림
  • SEO 어려움

🌐 HTTP 요청과 API 설계

HTTP 메서드 종류

메서드용도예시
GET데이터 조회게시물 목록 보기
POST데이터 생성새 게시물 작성
PUT데이터 전체 수정게시물 전체 내용 변경
PATCH데이터 부분 수정게시물 제목만 변경
DELETE데이터 삭제게시물 삭제

기본 API 예시

// 게시물 목록 조회
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 })
})

RESTful API 설계 원칙

1. Uniform Interface (일관된 인터페이스)

// ✅ 좋은 예: 일관된 패턴
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', ...)

2. 명사 중심의 URL 설계

// ✅ 좋은 예: 명사 사용
/api/posts          // 게시물들
/api/users          // 사용자들
/api/comments       // 댓글들

// ❌ 나쁜 예: 동사 사용
/api/getPosts
/api/createUser
/api/deleteComment

3. 계층적 URL 구조

// ✅ 좋은 예: 계층적 구조
/api/posts/123/comments        // 123번 게시물의 댓글들
/api/users/456/posts          // 456번 사용자의 게시물들
/api/categories/tech/posts    // 기술 카테고리의 게시물들

// ❌ 나쁜 예: 평면적 구조
/api/postComments?postId=123
/api/userPosts?userId=456

🔗 사용자 요청 방법

1. GET 요청 방법

브라우저 주소창

http://localhost:8080/list

HTML 링크

<a href="/list">게시물 목록 보기</a>
<a href="/post/123">123번 게시물 보기</a>

JavaScript Fetch

fetch('/api/posts')
    .then(response => response.json())
    .then(data => console.log(data))

2. POST 요청 방법 (다음 시간 예고)

<form action="/create" method="POST">
    <input name="title" placeholder="제목">
    <textarea name="content" placeholder="내용"></textarea>
    <button type="submit">작성</button>
</form>

🎨 고급 EJS 활용

데이터 가공 및 필터링

<!-- 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(글 => { %>
        <!-- 게시물 내용 -->
    <% }) %>
<% } %>

📝 요약 정리

핵심 개념

  1. EJS 설치: npm install ejs + app.set('view engine', 'ejs')
  2. 폴더 구조: views/ 폴더에 .ejs 파일 보관
  3. 데이터 전송: 응답.render('파일명.ejs', {변수명: 데이터})
  4. EJS 문법: <%= %> (출력), <%- %> (HTML), <% %> (JavaScript)
  5. 모듈화: <%- include('파일명.ejs') %>

다음 단계

  • HTML Form을 통한 POST 요청
  • 사용자 입력 받아 DB에 저장
  • 파일 업로드 기능
  • 세션과 로그인 시스템

EJS 템플릿 엔진으로 동적인 웹 페이지를 정리했습니다.

profile
App, Web Developer

0개의 댓글