๋์ ์ค
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
๋ณต์ฌ