
웹응용프로그래밍 수업에서 진행한 실습 과제입니다. 다음은 이번 실습에 사용되는 기술들의 개념입니다.
Node.js: 자바스크립트를 실행할 수 있는 런타임 환경, 비동기 I/O와 이벤트 기반, Spring이 대규모 어플리케이션에 적합하다면, 노드는 주로 빠르고 가벼운 어플리케이션 환경에 적합합니다.
Nunjucks: JavaScript로 작성된 서버 사이드 템플릿 엔진으로 HTML을 동적으로 생성합니다.
Nodemon: nodemon은 파일이 변경될 때마다 자동으로 서버를 재시작해주는 유용한 도구입니다.

디렉토리 구조는 위와 같습니다. public 하위에 css가 위치하고, views 폴더에 동적으로 html을 처리하기 위한 넌적스 파일들이 있습니다. 그리고 서버의 시작점인 app.js 파일 및 package.json 파일이 있습니다.
API 명세도 간단하게 작성해봤습니다.

우선 필요한 패키지들부터 먼저 설치해보겠습니다. 설치가 완료되면 node_modules 폴더와 package-lock.json이 생성됩니다.
npm i express nunjucks nodemon
const express = require('express');
const path = require('path');
const nunjucks = require('nunjucks');
const app = express();
const PORT = 3000;
// Nunjucks 설정
nunjucks.configure('views'/* 템플릿 파일 경로 */, {
autoescape: true,
express: app,
// 템플릿 파일이 변경되면 엔진을 재시작하도록 설정
watch: true,
});
// 정적 파일 경로를 'public'으로 설정 설정
app.use(express.static('public'));
// 폼 요청을 해석하기 위해 미들웨어 사용
// 폼 요청은 contact.njk 파일과 과제 설명 PDF 참고
app.use(express.urlencoded({extend : true}));
// 상품 데이터
const products = [
{ name: '상품 A', price: 10000, onSale: false },
{ name: '상품 B', price: 20000, onSale: true },
{ name: '상품 C', price: 15000, onSale: false },
{ name: '상품 D', price: 30000, onSale: true },
];
// 루트 라우트('/')
// / get 요청이 오면 index.njk로 라우트
// 이때, products 리스트를 인자로 주어야 함
app.get('/', (req,res)=>{
res.render('index.njk', {products});
});
// 상품 페이지 라우트('/products')
// /products get 요청이 오면 products.njk로 라우트
// 이때, products 리스트를 인자로 주어야 함
app.get('/products', (req, res) => {
res.render('products.njk', { products });
});
// 문의 페이지 라우트('/contact')
// /contact get 요청이 오면 contact.njk로 라우트
app.get('/contact', (req, res) => {
res.render('contact.njk');
});
// 문의 폼이 post되었을 경우 처리 라우트('/contact')
// 콘솔에 문의자의 name과 message를 출력
// 이후 contact_success.njk로 라우트
app.post('/contact', (req,res)=>{
const {name, message} = req.body;
console.log(`문의 - 이름 : ${name}, 메시지 : ${message}`);
res.render('contact_success.njk');
});
// 서버 시작
app.listen(PORT, () => {
console.log(`서버가 포트 ${PORT}에서 시작되었습니다.`);
});
nunjucks.configure 부분을 보면 watch:true로 하므로써, js 파일뿐만아니라 템플릿(njk) 파일이 변경되어도 엔진을 재시작하도록 하고 있습니다. 이는 엔진이 변경 사항을 감지하고, 새로운 버전의 템플릿을 렌더링하는 원리입니다.
기본적인 구조는 layout.njk 파일에 구현한 후에 동적으로 처리하는 부분을 block으로 정의합니다. 이후에 layout을 상속하여({% extends 'layout.njk' %}) block 내부에 원하는 내용을 담는 방식으로 오버라이딩합니다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>
{% block title %}상품 목록{% endblock %}
</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header>
<nav>
<a href="/">홈</a> |
<a href="/products">상품</a> |
<a href="/contact">문의</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© 2024 상품 목록 웹사이트</p>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- layout을 불러옴 -->
{% extends 'layout.njk' %}
<!-- title을 '홈 - 상품 목록'으로 변경 -->
{% block title%}홈 - 상품 목록{%endblock%}
<!-- content 블록 -->
{% block content %}
<h1>홈 페이지</h1>
<h2>상품 목록</h2>
<ul>
<!-- app.js로부터 받아온 products의 모든 상품에 대해 반복 -->
{% for product in products%}
<li>
<!-- 상품이 세일 중이라면 -->
{%if product.onSale ==true%}
<!-- '상품명 - 상품가격 원 (할인 중!)'으로 리스트 원소 생성 -->
<strong>{{product.name}}</strong> - {{product.price}} 원 (할인 중!)
<!-- 이때 상품명은 굵은 글씨체로 설정 -->
<!-- 아니라면 -->
{% else %}
<!-- '상품명 - 상품가격 원'으로 리스트 원소 생성 -->
{{product.name}} - {{product.price}} 원
<!-- 조건문 끝 -->
{%endif%}
</li>
<!-- 반복문 끝 -->
{% endfor %}
</ul>
<!-- content 블록 끝 -->
{% endblock %}
<!-- scripts 블록 -->
{% block scripts%}
<!-- 콘솔에 '홈 페이지가 로드되었습니다.'를 출력 -->
<script>
console.log('홈 페이지가 로드되었습니다.');
</script>
<!-- scripts 블록 끝 -->
{% endblock %}
<!-- layout을 불러옴 -->
{% extends 'layout.njk' %}
<!-- title을 '상품 페이지'로 변경 -->
{% block title%}상품 페이지{% endblock %}
<!-- content 블록 -->
{% block content%}
<h1>상품 상세 정보</h1>
<ul>
<!-- app.js로부터 받아온 products의 모든 상품에 대해 반복 -->
{% for product in products %}
<li>
<!-- '상품명 - 상품가격 원'으로 리스트 원소 생성 -->
{{ product.name }} - {{ product.price }} 원
<!-- 상품 가격이 20000원 이상이라면 -->
{% if product.price >= 20000 %}
<!-- '고가 상품'이라는 태그를 추가로 표시(styles.css의 .badge class 참고) -->
<span class="badge">고가 상품</span>
<!-- 조건문 끝 -->
{% endif %}
</li>
<!-- 반복문 끝 -->
{% endfor %}
</ul>
{% endblock %}
<!-- scripts 블록 -->
{% block scripts %}
<!-- 콘솔에 '상품 페이지가 로드되었습니다.'를 출력 -->
<script>
console.log('상품 페이지가 로드되었습니다.');
</script>
{% endblock %}
<!-- layout을 불러옴 -->
{% extends 'layout.njk' %}
<!-- title을 '문의 페이지'로 변경 -->
{% block title %}문의 페이지 {% endblock %}
<!-- content 블록 -->
{% block content %}
<h1>문의하기</h1>
<!-- /contact에 POST를 하는 form 생성 -->
<!-- 내용은 과제 PDF 참조 -->
<form action = "/contact" method ="POST">
<div>
<label for "name">이름: </label>
<input type="text" id="name" name="name" required>
</div>
<div>
<label for="message">메시지</label>
<textarea id="message" name="message" required></textarea>
</div>
<button type= "submit">보내기</button>
</form>
<!-- content 블록 끝 -->
{%endblock%}
<!-- scripts 블록 -->
{% block scripts%}
<!-- 콘솔에 '문의 페이지가 로드되었습니다.'를 출력 -->
<script>
console.log('문의 페이지가 로드되었습니다.');
</script>
<!-- scripts 블록 끝 -->
{%endblock%}
<!-- layout을 불러옴 -->
{% extends 'layout.njk' %}
<!-- title을 '문의 성공'으로 변경 -->
{% block title %}문의 성공{% endblock %}
<!-- content 블록 -->
{% block content %}
<h1>문의가 접수되었습니다</h1>
<p>귀하의 문의가 성공적으로 접수되었습니다. 감사합니다!</p>
<!-- content 블록 끝 -->
{% endblock %}
<!-- scripts 블록 -->
{% block scripts %}
<!-- 콘솔에 '문의 성공 페이지가 로드되었습니다.'를 출력 -->
<!-- scripts 블록 끝 -->
{% endblock %}
홈화면
상품 페이지
문의 페이지 GET

문의 페이지 POST



위와 같이 문의 접수도 잘 되어 터미널에 출력되는 것을 확인할 수 있습니다.ㅎㅎ