Node.js 스터디 5주차
Node.js 교과서 6장 익스프레스 웹 서버 만들기
6.1절 익스프레스 프로젝트 시작하기
✅ Express 모듈 : Http 모듈의 요청과 응답 객체에 기능 및 메서드 추가해서 HTTP모듈보다 더 편리하게 사용이 가능
npm i express 명령어를 통해 Express를 설치할 수 있다.
const express = require('express');
const app = express(); //express 객체 생성
app.set('port', process.env.PORT || 3000); //포트 번호 설정
app.get('/', (req, res) => { // '/'에 get요청 올 때 어떤 처리를 할지 작성
res.send('Hello, Express'); // http모듈의 res.write, res.end대신 res.send사용
}); //Get외에도 POST, PUT, DELETE, PATCH등 다른 메서드 사용 가능
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
npm start로 실행하면 콘솔창엔 위 사진처럼 나온다.
아래는 localhost:3000 접속시 나오는 화면이다. res.send에 써놓은 문자열 Hello, Express가 나오는걸 확인할 수 있다.
만약 HTML파일로 응답하고 싶다면 path모듈로 위치를 지정한 다음 응답으로 html파일을 보내주면 된다.
* 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'); // path 모듈
const app = express();
app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '/index.html')) //sendFile로 메서드 변경
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
- 위처럼 html파일의 내용이 나오는 것을 확인할 수 있다.
6.2 자주 사용하는 미들웨어
익스프레스 사이트에 가보면 미들웨어 관련 설명이 나와있다.
미들웨어가 요청과 응답의 중간에 위치해있어서 미들웨어라 부르는데 라우터, 에러 핸들러, 다음 미들웨어 호출 등 익스프레스의 핵심이라고 말할 수 있다. app.use(미들웨어)의 형태로 사용한다.
...
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'), '번 포트에서 대기 중');
});
* 경로를 안넣는 경우 모든 요청에서 실행, 주소 넣는 경우 해당 요청에서만 실행
* next를 호출해야 다음 미들웨어로 넘어간다.
* 대부분의 에러 처리 미들웨어는 특수경우 제외하고는 맨 아래에 두는 것이 좋다.
app.use(미들웨어) | 모든 요청에서 미들웨어 실행 |
app.use('/abc', 미들웨어) | abc로 시작하는 요청에서 해당 미들웨어 실행 |
app.post('/abc', 미들웨어) | abc로 시작하는 POST요청에서 미들웨어 실행 |
✅ dotenv : env 파일을 읽어서 process.env 로 만든다 .env에는 주로 key파일들을 담아둔다. (db 비밀번호 등)
npm i morgan cookie-parser express-session dotenv
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')); // combined, common, short, tiny등의 인수 가능
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',
}));
...
COOKIE_SECRET=cookiesecret
✅ body-parser : req.body객체로 만들어주는 미들웨어이다. 추가적으로 이미지 등 multipart data가 있는 경우는 multer라는 모듈을 사용하면 된다.
app.use(express.json());
app.use(express.urlencoded({ extended : false });
익스프레스 4.16.0버전부터 body-parser 미들웨어의 일부 기능이 익스프레스에 내장이 되어있다. 그렇지만 Raw, Text 형식의 데이터를 추가로 해석할 수 있어서 직접 설치하는 경우도 있다.
✅ cookie-parser : 요청에 동봉된 쿠키를 해석해서 req.cookies 객체로 생성한다.
app.use(cookieParser(비밀키));
쿠키를 생성하려는 경우에는 res.cookie, res.clearCookie등의 메서드 사용한다.
res.cookie('name', 'zerocho', { // res.cookies(키, 값, 옵션 형식)
expires : new Date(Date.now() + 900000),
httpOnly : true,
secure : true,
});
res.clearCookie('name', 'zerocho', { httpOnly : true, secure : true }); //옵션도 일치해야 쿠키 지워짐
✅ express-session : 세션 관리용 미들웨어, 특정 사용자를 위한 데이터 임시적으로 저장해두거나 로그인 등을 위한 세션 구현시 사용한다. 사용자별로 req.session객체에 유지. express-session은 세션 관리시 클라이언트에 쿠키 보냄.
app.use(session({
resave : false, //수정 사항 안생겨도 세션 다시 저장할지 설정
saveUninitialized : false, //처음부터 세션 생성할지 설정
secret : process.env.COOKIE_SECRET,
cookie : {
httpOnly : true,
secure : false,
},
name : 'session-cookie',
}));
✅ 미들웨어의 특성 활용하기
- 미들웨어는 req, res, next를 매개변수로 가지는 함수(에러처리 미들웨어만 예외적으로 err, req,res, next)
- app.use, app.get, app.post등으로 장착
- next() 안에 인수 넣기 가능('route' : 다음 라우터로, error : 에러 핸들러로)
app.use((req, res, next) => {
req.data = '데이터 넣기';
next();
}, (req, res, next) => {
console.log(req.data);
next();
});
- 요청이 끝날 때까지만 데이터를 유지하려면 위 코드처럼 req객체에 데이터 넣어두면 된다.
- app.set의 경우 익스프레스 전역적으로 사용하기 때문에, 사용자 개개인 값 넣어두는 것은 req객체를 사용한다.
✅ 미들웨어 사용 패턴
- 미들웨어 안에 미들웨어 넣는 방식
app.use(morgan('dev'));
app.use((rqe, 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);
}
});
✅ multer : 이미지, 동영상 등 여러 파일을 멀티파트 형식(multipart/form-data)으로 업로드 시 사용
" npm i multer" 을 통해 multer 패키지 설치
<form id = "form" action = "/upload" method = "post" enctype="multipart/form-data">
<input type = "file" name = "image" />
<input type = "text" name = "title" />
<button type="submit">업로드</button>
</form>
multer 패키지의 기본 설정은 아래와 같다. 단 아래 코드는 서버 내에 uploads폴더가 있어야 한다.
const multer = require('multer');
const upload = multer({
storage : multer.diskStorage({
destination(req, file, done) { //destination : 어디에
done(null, 'uploads/');
},
filename(req, file, done) { // filename : 어떤 이름으로 저장
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname, ext) + Date.now() + ext);
},
}),
limits : { fileSize : 5 * 1024 * 1024 },
});
없는 경우 fs 모듈의 mkdirSync 메소드로 서버 시작할 때 생성해줄수도 있다.
const fs = require('fs');
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads폴더 생성합니다.');
fs.mkdirSync('uploads');
}
- 파일 하나만 업로드 하는 경우에는 single미들웨어 사용
app.post('/upload', upload.single('image'), (req, res) => {
console.log(req.file, req.body);
res.send('OK');
});
- 여러개 업로드 하는 경우
<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>
- multiple을 input태그 안에 작성
app.post('/upload', upload.array('many'), (req, res) => {
console.log(req.files, req.body);
res.send('OK');
});
- single대신 array로 미들웨어 교체한다. upload결과도 req.files라는 배열로 바뀐것을 코드에서 확인할 수 있다.
- 파일이 여러개지만 태그나 폼 데이터 키가 다른 경우
<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');
},
);
- fileds의 매개변수로 input 태그의 name을 각각 적는다.
- 그 외 특수경우로 파일 업로드 안하고 멀티파트 형식으로 업로드 하는 경우에 none미들웨어 사용
<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');
});
✅ multer의 미들웨어를 정리하면 아래와 같다.
single | 이미지 하나인 경우, req.file로 나머지는 req.body로 |
array | 이미지 여러개를 req.files로 나머지는 req.body로 |
fields | |
none | 모든 정보를 req.body로 |
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');
},
);
6.3 Router 객체로 라우팅 분리하기
app.get같은 메서드 부분이 라우터 부분이다. routes폴더를 생성하고 그 안에 index.js, user.js 작성
- index.js
const express = require('express');
const path = require('path');
const router = express.Router();
router.get('/', (req, res) => {
res.send('Hello, Express');
});
module.exports = router;
- user.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.send('Hello, User');
});
module.exports = router;
- app.js
라우터를 연결하고 404 응답 미들웨어도 작성
const indexRouter = require('./routes');
const userRouter = require('./routes/user');
// 라우터 연결
app.use('/', indexRouter);
app.use('/user', userRouter);
// 404리턴 미들웨어
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다. `);
error.status = 404;
next(error);
})
위 이미지처럼 경로를 다르게 접속하니 각각에 해당하는 응답을 받을 수 있다.
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) { //users/12등 요청도 처리
console.log(req.params, req.query);
});
* /user/like와 같은 라우터는 라우터 매개변수 사용하는 라우터보단 위에 위치해야 한다.
* 주소는 같으나 메서드가 다른 경우, 아래의 코드처럼 처리가 가능하다.
router.route('/abc')
.get((req, res) => {
res.send('GET /abc');
})
.post((req, res) => {
res.send('POST /abc');
});
6.4 req, res 객체 살펴보기
✅ Http모듈의 req, res객체가 확장된 것이 Express객체
✅ req 객체
req.app | req 객체를 통해 app객체에 접근할 수 있음 |
req.body | body-parser미들웨어가 만드는 요청의 본문 해석 |
req.cookies | cookie-parser 미들웨어가 만드는 요청의 쿠키 해석 |
req.ip | 요청의 ip주소 담겨있음 |
req.params | 라우트 매개변수에 대한 정보가 담긴 객체 |
req.query | 쿼리 스트링에 대한 정보가 담김 |
req.signedCookies | 서명된 cookie는 이곳에 담긴다. |
req.get(헤더이름) | 헤더의 값을 가져오고 싶을 때 사용하는 메서드 |
✅ res 객체
res.app | res객체를 통해 app객체에 접근할 수 있음 |
res.cookie(키, 값, 옵션) | 쿠키를 설정하는 메서드 |
res.clearCookie(키, 값,옵션) | 쿠키를 제거하는 메서드 |
res.end() | 데이터 없이 응답 보내는 경우 |
res.json(JSON) | json형식의 응답을 보냄 |
res.redirect(주소) | 리다이렉트할 주소와 함께 응답을 보냄 |
res.render(뷰, 데이터) | 템플릿 엔진 렌더링해서 응답시 사용 |
res.send(데이터) | 데이터와 함께 응답을 보냄. |
res.sendFile(경로) | 경로에 위치한 파일을 응답 |
res.set(헤더, 값) | 응답의 헤더를 설정 |
res.status(코드) | 응답시의 HTTP상태 코드 지정 |
아래의 예시처럼 사용할 수 있다.
res
.status(201)
.cookie('test', 'test')
.redirect('/admin');
6.5절 템플릿 엔진 사용하기
✅ 퍼그
- 문법이 간단하지만 HTML과는 문법이 많이 달라 호불호가 있음
- npm i pug 로 설치
app.set('views', path.join(__dirname, 'views')); //템플릿 파일들이 위치한 폴더 지정
app.set('view engine', 'pug');
- layout.pug
doctype html
html
head
title=title
link(rel='stylehseet', href='/style.css')
body
block content
- index.pug
extends layout
block content
h1 = title
p Welcome to #{title}
-error.pug
extends layout
block content
h1 = message
h2 = error.status
pre #{error.stackS}
✅ 넌적스
- HTML문법을 그대로 사용하되, 추가로 자바스크립트 문법 사용가능
- 파이썬 템플릿 엔진 Twig와 문법이 유사
- npm i nunjucks로 설치
const nunjucks = require('nunjucks');
...
nunjucks.configure('views', {
express: app, // express속성에 app객체 연결
watch : true, //html변경시 템플릿 엔진 다시 렌더링
});
✅ 퍼그, 넌적스 문법 비교
퍼그 특성 정리 | 넌적스 특성 정리 |
<> 와 닫는 태그 없음 | HTML그대로 차용해서 있음 |
#login-button p.hidden.full class는 #으로, id 는 . 으로, div태그는 생략 가능 |
|
텍스트는 한칸 띄우고 입력 | |
| (파이프)로 여러줄 입력 | |
style, javascript등 입력시 . 기호 사용 | |
- const node = 'Node.js'처럼 -로 선언 가능 태그 = 속성으로도 사용 가능 이스케이프 사용 X시에는 != 사용 |
변수를 {{ }}로 감쌈 {% set 변수 = '값' %} 형태로 내부 변수 사용가능 이스케이프시 {{ 변수 | safe }} 사용 |
반복 가능한 변수인 경우 each 사용 | 특수구문은 { % %} 안에 사용, 반복문도 여기에 넣는다. for in과 endfor사이에! loop, index라는 특수변수 사용 가능 |
if, else, case, else if 등의 조건문 사용가능 | if, elif, else, endif 등을 {% %} 사이에 넣고 사용 case문은 사용 X |
include 파일이름 형태로 다른 퍼그나 html 넣기 가능 | {% include "파일경로" %} 형태로 사용가능 |
extends로 레이아웃 불러오기 가능(공통 마크업) | { % extends 경로 %} 형태로 사용가능 |
✅ 에러 처리 미들웨어
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');
});
localhost:3000/abc에 접속하면 아래처럼 확인이 가능하다.
에러 코드 아래부분에 스택 트레이스가 나오기 때문에, 서버 폴더 구조 유추가 가능하다. 따라서 보안상의 이유로, 배포환경에서는 숨겨진다.