Search
Duplicate
moon
sun

넌적스 Nunjucks

넌적스

💡
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
복사