4장에서와 같이 웹 서버를 만들 때 코드가 복잡하고 확장성이 떨어지는 문제가 있다.
이를 불편함을 해소하고 편의 기능을 추가한 npm에서 제공하는 웹 서버 프레임워크가 있다.
익스프레스가 대표적이다.
직접 파일을 만들어도 되고 명령어를 콘솔에서 호출해도 된다.
version이나 description, author license는 자유롭게 수정 가능하다.
$ npm i express
$ npm i -D nodemon
// package.json
{
"name": "learn-express",
"version": "0.0.1",
"description": "익스프레스를 배우자",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "ojo",
"license": "MIT",
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}
nodemon app은 app.js를 nodemon으로 실행한다는 뜻이다.
nodemon은 코드가 수정될 때 자동으로 서버를 재시작하는 데몬으로 개발용으로만 사용하는 것을 권장한다.
서버 역할을 할 app.js는 아래와 같다.
// app.js
const express = require('express');
const app = express();
app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
res.send('Hello, Express');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
콘솔에 명령을 입력하면 http://localhost:3000으로 접속할 것이고 다음과 같은 페이지가 뜰 것이다.
$ npm start
// index.html
<html>
<head>
<meta charset="UTF-8" />
<title>익스프레스 서버</title>
</head>
<body>
<h1>익스프레스</h1>
<p>배워봅시다.</p>
</body>
</html>
// app.js
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
// res.send('Hello, Express');
res.sendFile(path.join(__dirname, '/index.html'));
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
// app.js
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 다 실행됩니다.');
next();
});
app.get('/', (req, res, next) => {
console.log('GET / 요청에서만 실행됩니다.');
next();
}, (req, res) => {
throw new Error('에러는 에러 처리 미들웨어로 갑니다.')
});
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'), '번 포트에서 대기 중');
});
실무에 자주 사용하는 패키지들로 아래와 같은 패키지들이 있다.
$ npm i morgan cookie-parser express-session dotenv
margan, cookie-parser, express-session은 미들웨어이고, dotenv는 process.env를 관리하기 위해 설치함.
// app.js
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config();
const app = express();
app.set('port', process.env.PORT || 3000);
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(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
app.use((req, res, next) => {
console.log('모든 요청에 다 실행됩니다.');
next();
});
app.get('/', (req, res, next) => {
console.log('GET / 요청에서만 실행됩니다.');
next();
}, (req, res) => {
throw new Error('에러는 에러 처리 미들웨어로 갑니다.')
});
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'), '번 포트에서 대기 중');
});
// .env
COOKIE_SECRET=cookiesecret
모든 요청에 다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 갑니다.
// 에러 스택 트레이스
GET / 500 21.878 ms - 50
app.use('요청 경로', express.static('실제 경로'));
app.use('/', express.static(path.join(__dirname, 'public')));
실제 서버의 폴더 경로에는 public이 들어있지만, 요청 주소에는 public이 들어 있지 않으므로 외부인이 서버의 구조를 쉽게 파악할 수 없게 된다. 이런 식으로 보안에 큰 도움을 준다.
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
$ npm i body-parser
const bodyParser = require('body-parser');
app.use(bodyParser.raw());
app.use(bodyParser.text());
app.use(cookieParser(비밀키));
res.cookie('name', 'ojo', {
expires: new Date(Date.now() + 900000),
httpOnly: true,
secure: true,
});
res.clearCookie('name', 'ojo', { httpOnly: true, secure: true });
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
req.session.name = 'ojo'; // 세션 등록
req.sessionID; // 세션 아이디 확인
req.session.destroy(); // 세션 모두 제거
express-session으로 만들어진 req.session 객체에 값을 대입하거나 삭제해서 세션을 변경할 수 있다. req.session.destroy 메서드를 호출해서 세션을 한 번에 삭제할 수 있고, req.sessionID로 현재 세션의 아이디를 확인할 수 있다.
미들웨어의 특성을 총정리해보자.
next 함수에 인수를 넣을 수도 있다. route라는 문자열을 넣으면 다음 라우터의 미들웨어로 바로 이동하고, 그 외의 인수를 넣는다면 바로 에러 처리 미들웨어로 이동한다. 이 인수는 에러 처리 미들웨어의 err 매개변수가 된다.
세션을 사용한다면 req.session 객체에 데이터를 넣어도 되지만 세션이 유지되는 동안 데이터도 계속 유지된다는 단점이 있다. 요청이 끝날 때까지만 데이터를 유지하고 싶다면 req 객체에 데이터를 넣어두면 된다. req.data를 통해 미들웨어 간에 데이터를 공유할 수 있다. 새로운 요청이 오면 req.data는 초기화된다.
app.use((req, res, next) => {
req.data = '데이터 넣기';
next();
}, (req, res, next) => {
console.log(req.data); // 데이터 받기
next();
});
미들웨어 안에 미들웨어를 넣는 방식이다.
app.use(morgan('dev'));
// 또는
app.use((req, res, next) => {
morgan('dev')(req, res, next);
});
위 코드는 같은 기능을 하는데 이 패턴이 유용한 이유는 기존 미들웨어의 기능을 확장할 수 있기 때문이다. 아래 코드와 같이 조건문에 따라 분기 처리를 할 수도 있다.
app.use((req, res, next) => {
if (process.env.NODE_ENV === 'production') {
morgan('combined')(req, res, next);
} else {
morgan('dev')(req, res, next);
}
});
// multipart.html
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="image" />
<input type="text" name="title" />
<button type="submit">업로드</button>
</form>
위와 같은 html이 있으면 멀티파트 형식으로 데이터를 업로드할 수 있다.
이런 폼으로 업로드하는 파일은 body-parser로 처리할 수 없고 직접 파싱(해석)하기도 어렵다.
$ npm i multer
const multer = require('multer');
const upload = multer({
storage: multer.diskStorage({
destination(req, file, done) {
done(null, 'uploads/');
},
filename(req, file, done) {
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname, ext) + Date.now() + ext);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
multer 함수의 인수로 설정을 넣는다.
destination과 filename으로 어디에 어떤 이름으로 저장할지를 갖고 있다.
req는 요청에 대한 정보, file에는 업로드한 파일에 대한 정보가 있다.
done의 첫 번째 인수에는 에러가 있다면 에러를 넣고, 두 번째 인수에는 실제 경로나 파일 이름을 넣는다. req나 file의 데이터를 가공해서 done으로 넘기는 형식이다.
위 코드는 uploads라는 폴더에 [파일명+현재시간.확장자] 파일명으로 업로드하고 있다.
업로드에 대한 제한 사항을 설정할 수 있다. 파일 사이즈를 5MB로 제한하였다.
위 설정을 실제로 활용하기 위해서는 서버에 uploads 폴더가 꼭 존재해야 한다.
const fs = require('fs');
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
설정이 끝나면 upload 변수가 생기고, 다양한 종류의 미들웨어가 들어있다.
app.post('/upload', upload.single('image'), (req, res) => {
console.log(req.file, req.body);
res.send('ok');
});
파일을 하나만 업로드하는 경우에 사용한다. (현재 multipart.html과 같은 경우)
업로드 결과는 req.file에 들어있다.
여러 파일을 업로드하는 경우, input 태그에 multiple을 쓴다.
// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="many" multiple />
<input type="text" name="title" />
<button type="submit">업로드</button>
</form>
app.post('/upload', upload.array('many'), (req, res) => {
console.log(req.files, req.body);
res.send('ok');
});
파일을 여러 개 업로드하는 경우에 사용한다.
업로드 결과는 req.files 배열에 들어있다.
파일을 여러 개 업로드하지만 input 태그나 폼 데이터의 키가 다른 경우에는 fields 미들웨어를 사용한다.
// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="image1" />
<input type="file" name="image2" />
<input type="text" name="title" />
<button type="submit">업로드</button>
</form>
app.post('/upload',
upload.fields([{ name: 'image1' }, { name: 'image2' }]),
(req, res) => {
console.log(req.files, req.body);
res.send('ok');
},
);
인수로 input 태그의 name을 각각 적는다.
업로드 결과는 req.files.image1, req.files.image2에 각각 들어 있다.
특수한 경우지만, 파일을 업로드하지 않고도 멀티파트 형식으로 업로드하는 경우가 있다.
// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
<input type="text" name="title" />
<button type="submit">업로드</button>
</form>
app.post('/upload', upload.none(), (req, res) => {
console.log(req.body);
res.send('ok');
});
파일을 업로드하지 않고 멀티파트 형식으로 업로드하는 경우에 사용한다.
파일을 업로드하지 않았으므로 req.body만 존재한다.
실제로 multer 예제를 실습하려면 아래와 같이 app.js와 multipart.html을 수정한다.
// app.js
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config();
const app = express();
app.set('port', process.env.PORT || 3000);
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(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
const multer = require('multer');
const fs = require('fs');
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage: multer.diskStorage({
destination(req, file, done) {
done(null, 'uploads/');
},
filename(req, file, done) {
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname, ext) + Date.now() + ext);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
app.get('/upload', (req, res) => {
res.sendFile(path.join(__dirname, 'multipart.html'));
});
app.post('/upload',
upload.fields([{ name: 'image1' }, { name: 'image2' }]),
(req, res) => {
console.log(req.files, req.body);
res.send('ok');
},
);
app.get('/', (req, res, next) => {
console.log('GET / 요청에서만 실행됩니다.');
next();
}, (req, res) => {
throw new Error('에러는 에러 처리 미들웨어로 갑니다.')
});
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'), '번 포트에서 대기 중');
});
// multipart.html
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="image1" />
<input type="file" name="image2" />
<input type="text" name="title" />
<button type="submit">업로드</button>
</form>
app.js에서 app.get 같은 메서드가 라우터이다.
라우터를 많이 연결하면 app.js가 매우 길어지므로 익스프레스에서는 라우터를 분리하는 방법을 제공한다.
routes폴더를 만들고 그 안에 index.js와 user.js를 작성한다.
// routes/index.js
const express = require('express');
const router = express.Router();
// GET / 라우터
router.get('/', (req, res) => {
res.send('Hello, Express');
});
module.exports = router;
// routes/user.js
const express = require('express');
const router = express.Router();
// GET /user 라우터
router.get('/', (req, res) => {
res.send('Hello, User');
});
module.exports = router;
// app.js
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config();
const indexRouter = require('./routes');
const userRouter = require('./routes/user');
const app = express();
app.set('port', process.env.PORT || 3000);
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(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
app.use('/', indexRouter);
app.use('/user', userRouter);
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'), '번 포트에서 대기 중');
});
router.get('/', function(req, res, next) {
next('route');
}, function(req, res, next) {
console.log('실행되지 않습니다');
next();
}, function(req, res, next) {
console.log('실행되지 않습니다');
next();
});
router.get('/', function(req, res) {
console.log('실행됩니다');
res.send('Hello, Express');
});
라우터에 연결된 나머지 미들웨어들을 무시하고 다음 라우터로 바로 건너뛸 수 있다.
router.get('/user/:id', function(req, res) {
console.log(req.params, req.query);
});
req.params.id를 조회하여 :id에 해당하는 주소를 넣을 수 있다.
:type이면 req.params.type으로 조회할 수 있다.
단, 일반 라우터보다 뒤에 위치해야 한다.
router.get('/user/:id', function(req, res) {
console.log('얘만 실행됩니다.');
});
router.get('/user/like', function(req, res) {
console.log('전혀 실행되지 않습니다.');
});
익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이므로 res.writeHead, res.write, res.end, res.send, res.sendFile 같은 메서드를 그대로 사용할 수 있다.
아래와 같이 메서드 체이닝을 활용하여 코드 양을 줄일 수 있다.
res
.status(201)
.cookie('test', 'test')
.redirect('/admin');
템플릿 엔진은 자바스크립트를 사용해서 HTML을 렌더링 할 수 있게 한다.
대표적인 템플릿 엔진인 퍼그(Pug)와 넌적스(Nunjucks)를 살펴보자.
$ npm i pug
// app.js
...
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(morgan('dev'));
...
퍼그 | HTML |
doctype html html head title= title link(rel='stylesheet', href='/stylesheets/style.css') |
<!DOCTYPE html> <html> <head> <title>익스프레스</title> <link rel="stylesheet" href="/style.css" /> </head> </html> |
#login-button .post-image span#highlight p.hidden.full |
<div id="login-button"></div> <div class="post-image"></div> <span id="highlight"></span> <p class="hidden full"></p> |
p Welcome to Express button(type='submit') 전송 |
<p>Welcome to Express</p> <button type="submit">전송</button> |
p | 안녕하세요. | 여러 줄을 입력합니다. br | 태그도 중간에 넣을 수 있습니다. |
<p> 안녕하세요. 여러 줄을 입력합니다. <br /> 태그도 중간에 넣을 수 있습니다. </p> |
style. h1 { font-size: 30px; } script. const message = 'Pug'; alert(message); |
<style> h1 { font-size: 30px; } </style> <script> const message = 'Pug'; alert(message); </script> |
퍼그는 자바스크립트 변수를 템플릿에 렌더링 할 수 있다. res.render 호출 시 보내는 변수를 퍼그가 처리한다.
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
이렇게 변수 객체를 넣는 대신, res.locals 객체를 이용해서 변수를 넣을 수도 있다.
이 방식은 다른 미들웨어에서도 res.locals 객체에 접근할 수 있다는 장점이 있다.
router.get('/', function(req, res, next) {
res.locals.title = 'Express';
res.render('index');
});
퍼그 | HTML |
h1= title p Welcome to #{title} button(class=title, type='submit') 전송 input(placeholder=title + ' 연습') |
<h1>Express</h1> <p>Welcome to Express</p> <button class="Express" type="submit">전송</button> <input placeholder="Express 연습" /> |
- const node = 'Node.js' - const js = 'Javascript' p # {node}와 # {js} |
<p>Node.js와 Javascript</p> |
p= '<strong>이스케이프</strong>' p!= '<strong>이스케이프하지 않음</strong>' |
<p><strong>이스케이프</strong></p> <p><strong>이스케이프하지 않음</strong></p> |
퍼그 | HTML |
ul each fruit in ['사과', '배', '오렌지', '바나나', '복숭아'] li= fruit |
<ul> <li>사과</li> <li>배</li> <li>오렌지</li> <li>바나나</li> <li>복숭아</li> </ul> |
ul each fruit, index in ['사과', '배', '오렌지', '바나나', '복숭아'] li= (index + 1) + '번째 ' + fruit |
<ul> <li>1번째 사과</li> <li>2번째 배</li> <li>3번째 오렌지</li> <li>4번째 바나나</li> <li>5번째 복숭아</li> </ul> |
퍼그 | HTML |
if isLoggedIn div 로그인 되었습니다. else div 로그인이 필요합니다. |
<!-- isLoggedIn이 true일 때 --> <div>로그인 되었습니다.</div> <!-- isLoggedIn이 false일 때 --> <div>로그인이 필요합니다.</div> |
case fruit when 'apple' p 사과입니다. when 'banana' p 바나나입니다. when 'orange' p 오렌지입니다. default p 사과도 바나나도 오렌지도 아닙니다. |
<!-- fruit이 apple일 때 --> <p>사과입니다.</p> <!-- fruit이 banana일 때 --> <p>바나나입니다.</p> <!-- fruit이 orange일 때 --> <p>오렌지입니다.</p> <!-- 기본값 --> <p>사과도 바나나도 오렌지도 아닙니다.</p> |
다른 퍼그나 HTML 파일을 넣을 수 있다.
퍼그 | HTML |
// header.pug header
// footer.puga(href='/') Home a(href='/about') About footer
// main.pugdiv 푸터입니다 include header
main h1 메인 파일 p 다른 파일을 include할 수 있습니다. include footer |
<header> <a href="/">Home</a> <a href="/about">About</a> </header> <main> <h1>메인 파일</h1> <p>다른 파일을 include할 수 있습니다.</p> </main> <footer> <div>푸터입니다.</div> </footer> |
레이아웃을 정할 수 있다.
퍼그 | HTML |
// layout.pug doctype html
// body.pughtml head title= title link(rel='stylesheet', href='/style.css') block style body header 헤더입니다. block content footer 푸터입니다. block script extends layout
block content main p 내용입니다. block script script(src="/main.js") |
<!DOCTYPE html> <html> <head> <title>Express</title> <link rel="stylesheet" href="/style.css" /> </head> <body> <header>헤더입니다.</header> <main> <p>내용입니다.</p> </main> <footer>푸터입니다.</footer> <script src="/main.js"></script> </body> </html> |
$ npm i nunjucks
// app.js
...
const path = require('path');
const nunjucks = require('nunjucks');
dotenv.config();
const indexRouter = require('./routes');
const userRouter = require('./routes/user');
const app = express();
app.set('port', process.env.PORT || 3000);
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.use(morgan('dev'));
...
configure의 첫 번째 인수로 views 폴더의 경로를 넣고, 두 번째 인수로는 옵션을 넣는다. exptress 속성에 app 객체를 연결하고 watch 옵션이 true이므로 HTML 파일이 변경될 때 템플릿 엔진을 다시 렌더링 한다.
퍼그와 달리 html 확장자를 그대로 사용해도 된다. 혹은 njk를 쓸 수 있다.
// routes/index.js
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
넌적스 | HTML |
<h1>{{title}}</h1> <p>Welcome to {{title}}</p> <button class="{{title}}" type="submit">전송</button> <input placeholder="{{title}} 연습" /> |
<h1>Express</h1> <p>Welcome to Express</p> <button class="Express" type="submit">전송</button> <input placeholder="Express 연습" /> |
{% set node = 'Node.js' %} {% set js = 'Javascript' %} <p>{{node}}와 {{js}}</p> |
<p>Node.js와 Javascript</p> |
<p>{{'<strong>이스케이프</strong>'}}</p> <p>{{'<strong>이스케이프하지 않음</strong>' | safe }}</p> |
<p><strong>이스케이프</strong></p> <p><strong>이스케이프하지 않음</strong></p> |
넌적스 | HTML |
<ul> {% set fruits = ['사과', '배', '오렌지', ' 바나나', '복숭아'] %} {% for item in fruits %} <li>{{item}}</li> {% endfor %} </ul> |
<ul> <li>사과</li> <li>배</li> <li>오렌지</li> <li>바나나</li> <li>복숭아</li> </ul> |
<ul> {% set fruits = ['사과', '배', '오렌지', ' 바나나', '복숭아'] %} {% for item in fruits %} <li>{{loop.index}}번째 {{item}}</li> {% endfor %} </ul> |
<ul> <li>1번째 사과</li> <li>2번째 배</li> <li>3번째 오렌지</li> <li>4번째 바나나</li> <li>5번째 복숭아</li> </ul> |
넌적스 | HTML |
{% if isLoggedIn %} <div>로그인 되었습니다.</div> {% else %} <div>로그인이 필요합니다.</div> {% endif %} |
<!-- isLoggedIn이 true일 때 --> <div>로그인 되었습니다.</div> <!-- isLoggedIn이 false일 때 --> <div>로그인이 필요합니다.</div> |
{% if fruit === 'apple' %} <p>사과입니다.</p> {% elif fruit === 'banana' %} <p>바나나입니다.</p> {% elif fruit === 'orange' %} <p>오렌지입니다.</p> {% else %} <p>사과도 바나나도 오렌지도 아닙니다.</p> {% endif %} |
<!-- fruit이 apple일 때 --> <p>사과입니다.</p> <!-- fruit이 banana일 때 --> <p>바나나입니다.</p> <!-- fruit이 orange일 때 --> <p>오렌지입니다.</p> <!-- 기본값 --> <p>사과도 바나나도 오렌지도 아닙니다.</p> |
<div>{{'참' if isLoggedIn}}</div> <div>{{'참' if isLoggedIn else '거짓'}}</div> |
<!-- isLoggedIn이 true일 때 --> <div>참</div> <!-- isLoggedIn이 false일 때 --> <div>거짓</div> |
넌적스 | HTML |
// header.html <header> <a href="/">Home</a> <a href="/about">About</a> </header> // footer.html <footer> <div>푸터입니다.</div> </footer> // main.html {% include "header.html" %} <main> <h1>메인 파일</h1> <p>다른 파일을 include할 수 있습니다.</p> </main> {% include "footer.html" %} |
<header> <a href="/">Home</a> <a href="/about">About</a> </header> <main> <h1>메인 파일</h1> <p>다른 파일을 include할 수 있습니다.</p> </main> <footer> <div>푸터입니다.</div> </footer> |
넌적스 | HTML |
// layout.html <!DOCTYPE html> <html> <head> <title>{{title}}</title> <link rel="stylesheet" href="/style.css" /> {% block style %} {% endblock %} </head> <body> <header>헤더입니다.</header> {% block content %} {% endblock %} <footer>푸터입니다.</footer> {% block script %} {% endblock %} </body> </html> // body.html {% extends 'layout.html' %} {% block content %} <main> <p>내용입니다.</p> </main> {% endblock %} {% block script %} <script src="/main.js"></script> {% endblock %} |
<!DOCTYPE html> <html> <head> <title>Express</title> <link rel="stylesheet" href="/style.css" /> </head> <body> <header>헤더입니다.</header> <main> <p>내용입니다.</p> </main> <footer>푸터입니다.</footer> <script src="/main.js"></script> </body> </html> |
나중에 익스프레스에서 res.render('body')를 사용해 같은 이름의 block 부분이 서로 합쳐지며 하나의 HTML로 렌더링 할 수 있다.
// app.js
...
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
...
에러 처리 미들웨어는 error라는 템플릿 파일을 렌더링 한다. 넌적스이므로 error는 error.html을 의미한다. 렌더링 시 res.locals.message와 res.locals.error에 넣어준 값을 함께 렌더링한다. 실제 404 에러가 발생했을 때, error.html의 페이지를 보이게 된다.
에러가 발생했을 때 에러 스택 트레이스가 보이는데 노출되면 보안에 취약할 수 있기 때문에 배포 환경에서는 숨겨야 한다.
[Node.js] 8장 몽고디비 (0) | 2021.12.01 |
---|---|
[Node.js] 7장 MySQL (0) | 2021.11.29 |
[Node.js] 5장 패키지 매니저 (0) | 2021.11.08 |
[Node.js] 4장 http 모듈로 서버 만들기 (0) | 2021.11.08 |
[Node.js] 3장 기능 알아보기(2) (0) | 2021.10.30 |