넌적스
Node.js 의 템플릿 엔진
Nunjucks는 템플릿 엔진 중 하나로, JavaScript를 기반으로 하는 강력하고 유연한 템플릿 엔진입니다. 주로 Node.js 환경에서 사용되며, Express.js와 함께 많이 사용됩니다.
확장자 : *.njk
주요 문법
문법 | 예시 | 설명 |
변수 출력 | {{ variableName }} | 변수의 값을 출력 |
조건문 | {% if condition %} ... {% endif %} | 조건에 따라 코드를 실행 |
반복문 | {% for item in itemList %} ... {% endfor %} | 배열이나 객체의 각 항목에 대해 반복 |
필터 | {{ variableName | filterName }} | 변수에 필터를 적용하여 출력 |
매크로 | {% macro myMacro(param) %} ... {% endmacro %} | 재사용 가능한 코드 블록 정의 |
매크로 호출 | {{ myMacro(param) }} | 정의된 매크로를 호출하여 사용 |
템플릿 상속 | {% extends layoutTemplate %} | 다른 템플릿을 확장하여 사용 |
블록 정의와 재정의 | {% block content %} ... {% endblock %} | 템플릿 상속 시 블록을 정의하고 재정의 |
주석 | {# 주석 내용 #} | 주석 처리 |
서버에서 뷰로 변수 전달 방법
1.
Nunjucks 설정 및 뷰 엔진 설정
2.
서버에서 뷰로 변수 전달
3.
Nunjucks 템플릿 파일 (example.njk)
Nunjucks 설정 및 뷰 엔진 설정
const express = require('express');
const nunjucks = require('nunjucks');
const app = express();
nunjucks.configure('views', {
autoescape: true,
express: app,
});
app.set('view engine', 'njk');
JavaScript
복사
서버에서 뷰로 변수 전달
app.get('/example', (req, res) => {
const data = {
title: 'Nunjucks Example',
message: 'Hello World',
itemList: ['Item 1', 'Item 2', 'Item 3'],
};
res.render('example', data);
});
JavaScript
복사
Nunjucks 템플릿 파일 (example.njk)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
</head>
<body>
<h1>{{ message }}</h1>
<ul>
{% for item in itemList %}
<li>{{ item }}</li>
{% endfor %}
</ul>
</body>
</html>
JavaScript
복사
게시판 예시코드
1.
라우터 파일 생성 및 설정
2.
라우터 정의
3.
njk 레이아웃 파일 작성
4.
style.css 연결
5.
index.njk 작성
6.
header, footer 추가
7.
로그인, 회원가입 화면 작성
8.
게시판 화면 작성
라우터 파일 생성 및 설정
1.
routes 폴더 생성
2.
index.js, board.js 파일 생성
3.
app.js 에 import 및 미들웨어 설정
routes 폴더 생성
index.js, board.js 파일 생성
app.js 에 import 및 미들웨어 설정
// nunjucks import
const nunjucks = require('nunjucks');
// 👩💻 라우터 모듈 import
const indexRouter = require('./routes/index');
const boardRouter = require('./routes/board');
JavaScript
복사
// Nunjucks 설정
nunjucks.configure('views', {
express: app,
watch: true,
});
app.set('view engine', 'njk');
// 👩💻 라우터 설정
app.use('/', indexRouter);
app.use('/board', boardRouter);
JavaScript
복사
라우터 정의
1.
index.js
2.
board.js
index.js
const express = require('express');
const router = express.Router();
// GET / 라우터
router.get('/', (req, res) => {
res.render('index', { title: 'Main' });
});
// 로그인
router.get('/login', (req, res) => {
res.render('login', { title: 'Login' });
});
// 회원가입
router.get('/join', (req, res) => {
res.render('join', { title: 'Join' });
});
module.exports = router;
JavaScript
복사
board.js
const express = require('express')
const router = express.Router()
let boardList = [
{ title : '제목1', writer : '작성자1', content : '내용1' },
{ title : '제목2', writer : '작성자2', content : '내용2' },
{ title : '제목3', writer : '작성자3', content : '내용3' },
]
// 👩💻 게시글 목록
router.get('/', (req, res) => {
res.render('board/list', {boardList} )
})
// 👩💻 게시글 등록
router.get('/insert', (req, res) => {
res.render('board/insert')
})
// 👩💻 게시글 등록
router.post('/', (req, res) => {
// 구조분해할당
const { title, writer, content } = req.body;
// const title = req.body.title
// const writer = req.body.writer
// const content = req.body.content
const newBoard = { title, writer, content };
boardList.push(newBoard);
res.redirect('/board');
});
// 👩💻 게시글 수정 페이지
router.get('/update/:id', (req, res) => {
console.log(`id : ${req.params.id}`);
let id = req.params.id
const board = boardList[id];
res.render('board/update', { board, id });
});
// 👩💻 게시글 수정
router.post('/update', (req, res) => {
const { id, title, writer, content } = req.body;
boardList[id] = { title, writer, content };
res.redirect(`/board/${id}`);
});
// 👩💻 게시글 삭제
router.post('/delete', (req, res) => {
const id = req.params.id;
boardList.splice(id, 1);
res.redirect('/board');
});
// 👩💻 게시글 읽기
// 요청 경로에 파라미터 매핑 방법 ➡ '/:파라미터명'
router.get('/:id', (req, res) => {
console.log(`id : ${req.params.id}`);
let id = req.params.id
let board = boardList[id]
res.render('board/read', {board, id})
})
module.exports = router;
JavaScript
복사
njk 레이아웃 파일 작성
1.
layout.njk
{# layout.njk #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My App{% endblock %}</title>
{# 여기에 CSS 연결 #}
</head>
<body>
<div id="content">
{% block content %}{% endblock %}
</div>
</body>
</html>
JavaScript
복사
style.css 연결
1.
public/css 폴더 생성
2.
style.css 파일 생성
3.
스타일 작성
public/css 폴더 생성
style.css 파일 생성
스타일 작성
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
ul {
list-style-type: none;
}
a {
text-decoration: none;
}
body {
font-family: Arial, sans-serif;
}
/* header */
header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #333;
color: #fff;
padding: 0 20px;
text-align: center;
height: 80px;
}
header h1 {
margin: 0;
}
header nav {
height: 100%;
}
header nav ul {
height: 100%;
}
header nav li {
display: inline-block;
height: 100%;
}
header nav a {
display: flex;
justify-content: center;
align-items: center;
color: #fff;
text-decoration: none;
min-width: 100px;
height: 100%;
}
header nav a:hover {
background-color: #fff;
color: #333;
}
/* footer */
footer {
background-color: #333;
color: #fff;
padding: 10px;
text-align: center;
}
.container {
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 300px;
margin: 100px auto;
}
.container-md {
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 480px;
margin: 100px auto;
}
.container-lg {
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
padding: 20px;
width: 1024px;
margin: 100px auto;
}
.container h2 {
text-align: center;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: bold;
}
.form-group input,
.form-group .btn {
display: inline-block;
font-size: 14px;
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.form-group .btn {
text-align: center;
background-color: #4caf50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 10px;
}
.form-group .btn:hover {
background-color: #45a049;
}
/* table */
.table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.table th {
background-color: #f2f2f2;
}
.table tr:hover {
background-color: #f5f5f5;
}
/* button */
.btn-box {
display: flex;
justify-content: space-between;
align-items: center;
margin: 20px 0;
}
.btn {
display: inline-block;
text-align: center;
font-size: 14px;
padding: 8px 18px;
box-sizing: border-box;
background-color: #4caf50;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
margin-bottom: 10px;
}
JavaScript
복사
layout.njk
{# layout.njk #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My App{% endblock %}</title>
<link rel="stylesheet" href="/css/style.css"> {# 여기에 CSS 연결 #}
</head>
<body>
<div id="content">
{% block content %}{% endblock %}
</div>
</body>
</html>
HTML
복사
index.njk 작성
{% extends 'layout.njk' %}
{% block content %}
<div class="container-lg">
<h1>{{ title }}</h1>
<h3>{{ title }} 화면입니다</h3>
</div>
{% endblock %}
HTML
복사
1.
extends layout: 현재 템플릿이 layout.njk라는 다른 템플릿을 확장한다는 의미입니다. 이는 기본 레이아웃을 정의하고, 다양한 페이지에서 이 레이아웃을 확장하여 사용할 수 있게 해줍니다.
2.
block content: 이 부분은 layout.njk에서 정의한 block content에 해당합니다. block content는 다른 템플릿에서 확장될 때 채워질 부분을 정의하는 역할을 합니다.
3.
div.container-lg: 이 부분은 특정 페이지의 콘텐츠를 감싸는 div 요소로, Bootstrap의 container-lg 클래스를 사용하여 컨텐츠의 너비를 조절하고 있습니다.
4.
h1= title: title 변수의 값을 출력하는 헤딩(제목) 요소입니다.
5.
h3 #{title} 화면입니다: title 변수의 값을 포함한 간단한 설명을 출력하는 또 다른 헤딩(부제목) 요소입니다.
header, footer 추가
header.njk
<header>
<h1>Nunjucks</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/login">Login</a></li>
<li><a href="/board">Board</a></li>
</ul>
</nav>
</header>
HTML
복사
footer.njk
<footer>
<p>Copyright © 2023 Pug</p>
</footer>
HTML
복사
layout.njk
{# layout.njk #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}My App{% endblock %}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
{% include "fragment/header.njk" %}
<div id="content">
{% block content %}{% endblock %}
</div>
{% include "fragment/footer.njk" %}
</body>
</html>
HTML
복사
로그인, 회원가입 화면 작성
login.njk
{% extends 'layout.njk' %}
{% block content %}
<div class="container">
<h2>Login</h2>
<form>
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<button class="btn" type="submit">Login</button>
<a class="btn" href="/join">Sign Up</a>
</div>
</form>
</div>
{% endblock %}
HTML
복사
join.njk
{% extends 'layout.njk' %}
{% block content %}
<div class="container">
<h2>Sign Up</h2>
<form>
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<button class="btn" type="submit">Sign Up</button>
</div>
</form>
</div>
{% endblock %}
HTML
복사
게시판 화면 작성
1.
board/list.njk (목록)
2.
board/insert.njk(글쓰기)
3.
board/read.njk (글읽기)
4.
board/update.njk(글수정)
board/list.njk
{% extends '../layout.njk' %}
{% block content %}
<div class="container-lg">
<div class="btn-box">
<h1>게시글 목록</h1>
<a class="btn" href="/board/insert">글쓰기</a>
</div>
<table class="table" border="1">
<thead>
<tr>
<th>No</th>
<th>Title</th>
<th>Writer</th>
</tr>
</thead>
<tbody>
{% for board in boardList %}
<tr>
<td width="80">{{ loop.index }}</td>
<td>
<a href="/board/{{ loop.index }}">{{ board.title }}</a>
</td>
<td width="200">{{ board.writer }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
HTML
복사
board/insert.njk
{% extends '../layout.njk' %}
{% block content %}
<div class="container-lg">
<h1>게시글 등록</h1>
<form action="/board" method="post">
<table class="table form-group" border="1">
<tr>
<th width="200">Title</th>
<td>
<input type="text" name="title">
</td>
</tr>
<tr>
<th width="200">Writer</th>
<td>
<input type="text" name="writer">
</td>
</tr>
<tr>
<th width="200">Content</th>
<td>
<input type="text" name="content">
</td>
</tr>
</table>
<div class="btn-box">
<a class="btn" href="/board">목록</a>
<button class="btn" type="submit">등록</button>
</div>
</form>
</div>
{% endblock %}
HTML
복사
board/read.njk
{% extends '../layout.njk' %}
{% block content %}
<div class="container-lg">
<h1>{{ board.title }}</h1>
<table class="table" border="1">
<tr>
<th width="200">No</th>
<td>{{ id }}</td>
</tr>
<tr>
<th width="200">Writer</th>
<td>{{ board.writer }}</td>
</tr>
<tr>
<th width="200">Content</th>
<td>{{ board.content }}</td>
</tr>
</table>
<div class="btn-box">
<a class="btn" href="/board">목록</a>
<a class="btn" href="/board/update/{{ id }}">수정</a>
</div>
</div>
{% endblock %}
HTML
복사
board/update.njk
{% extends '../layout.njk' %}
{% block content %}
<div class="container-lg">
<h1>게시글 수정</h1>
<form action="/board/update" method="post">
<input type="hidden" name="id" value="{{ id }}">
<table class="table form-group" border="1">
<tr>
<th width="200">No</th>
<td>{{ id }}</td>
</tr>
<tr>
<th width="200">Title</th>
<td>
<input type="text" name="title" value="{{ board.title }}">
</td>
</tr>
<tr>
<th width="200">Writer</th>
<td>
<input type="text" name="writer" value="{{ board.writer }}">
</td>
</tr>
<tr>
<th width="200">Content</th>
<td>
<input type="text" name="content" value="{{ board.content }}">
</td>
</tr>
</table>
<div class="btn-box">
<a class="btn" href="/board">목록</a>
<button class="btn" type="submit">수정</button>
</div>
</form>
</div>
{% endblock %}
HTML
복사
app.js 전체코드
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const nunjucks = require('nunjucks');
const path = require('path');
const app = express();
app.set('port', 3000);
// Nunjucks 설정
nunjucks.configure('views', {
express: app,
watch: true,
});
app.set('view engine', 'njk');
app.use(morgan('dev'));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(session({
resave: false,
saveUninitialized: false,
secret: 'session-secret',
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
// 라우터 모듈 import
const indexRouter = require('./routes/index');
const boardRouter = require('./routes/board');
// 라우터 설정
app.use('/', indexRouter);
app.use('/board', boardRouter);
// 404 오류 처리 미들웨어
app.use((req, res, next) => {
res.status(404).send('Not Found');
});
// 에러 처리 미들웨어
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send(err.message);
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
JavaScript
복사















