express server
learn-express 프로젝트를 하고 express 모듈을 이용한 server를 만들어보자.
npm i express
npm i -D nodemon
express와 편의를 위해 nodemon을 설치하자.
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'), '번 포트에서 대기중...');
});
express 모듈을 실행해 app 변수에 할당한다.
- app.set('port', 포트): 서버가 실행될 포트를 설정한다. process.env 객체에 PORT속성이 있다면 그 값을 사용하고, 없다면 3000 포트를 이용하게 한다.
- app.get(주소, 라우터): GET 요청이 올 때 어떤 동작을 할지 적는 부분이다. req는 요청에 관한 정보가 들어 있는 객체이고, res는 응답에 관한 정보가 들어 있는 객체이다.
테스트로 실행해보자.
npm start
(script에 start를 정의해 놔야 한다.)
sendFile을 사용하여 미리 작성한 html 파일을 보내보자.
<html>
<head>
<meta charset="UTF-8" />
<title>express server</title>
</head>
<body>
<h1>express</h1>
<p>learning!!</p>
</body>
</html>
index.html
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
미들웨어
미들웨어는 express의 핵심이다. 요청과 응답의 중간에 위치하여 미들웨어라고 부른다.
라우터와 에러 핸들러 또한 미들웨어의 일종이다. 미들웨어는 요청과 응답을 조작하여 기능을 추가하기도 하거, 나쁜 요청을 걸러내기도 한다.
app.use로 사용할 수 있다.
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('모든 요청에 다 실행');
});
app.get('/', (req, res, next) => {
console.log('GET / 요청에만 실행');
next();
}, (req, res) => {
throw new Error('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'), '번 포트에서 대기중...');
});
app.use에 매개변수가 req, res, next인 함수를 넣으면 된다. 미들웨어는 위에서부터 아래로 순서대로 실행되면서 요청과 응답 사이에 특별한 기능을 추가한다. next라는 매개변수는 다음 미들웨어로 넘어가는 함수이다.
에러 처리 미들웨어는 매개변수가 err, req, res, next로 네 개이다. 모든 매개변수를 사용하지 않더라도 매개변수가 반드시 네 개여야 한다. err에는 에러에 관한 정보가 담겨 있다.
다른 미들웨어도 추가하여 알아보자.
npm i morgan cookie-parser express-session dotenv
(dotenv는 process.env를 관리하기 위해 설치)
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('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'), '번 포트에서 대기중...');
});
dotenv 패키지는 .env 파일을 읽어서 process.env로 만든다. process.env를 별도의 파일로 관리하는 이유는 보안과 설정의 편의성 때문이다. 비밀 키들이 소스코드에 그대로 적어두면 소스 코드가 유출되었을 때 키도 같이 유출되기 때문이다.
morgan
morgan 연결 후 localhost:3000에 다시 접속해보면 기존 로그 외에 추가적인 로그를 볼 수 있다.
현재 콘솔에 나오는 에러 로그가 morgan에서 찍은 로그이다.
dev 외에 combined, common, short, tiny 등을 넣을 수 있다.
static
static 미들웨어는 정적인 파일들을 제공하는 라우터 역할을 한다. 기본적으로 제공되기 때문에 별도의 설치 없이 사용할 수 있다. 현재 public 폴더를 지정했다. public 폴더안에 css나 js, 이미지 파일을 넣어 두면 브라우저에서 접근할 수 있다.
body-parser
요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어이다. 보통 폼 데이터나 AJAX 요청의 데이터를 처리한다. 단 멀티파트(이미지, 동영상, 파일)데이터는 처리하지 못한다.
express 4.16.0 버전부터 body-parser 미들웨어의 기능 일부가 내장되었으므로 따로 설치할 필요는 없다.
하지만, 직접 설치해야 하는 경우도 있다. Raw, Text 형식의 데이터를 추가로 해석할 수 없다.
이런 경우에는 body-parser를 설치해 줘야 한다.
const bodyParser = require('body-parser');
app.use(bodyParser.raw());
app.use(bodyParser.text());
cookie-parser
cookie-parser는 요청에 동봉된 쿠키를 해석해 req.cookies 객체로 만든다.
해석된 쿠키들은 req.cookies 객체에 들어간다. 예를 들어 name=abc 쿠키를 보냈다면 req.cookies는
{ name: 'abc' }가 된다. 유효 기간이 지난 쿠키는 알아서 걸러낸다.
첫 번째 인수로 비밀 키를 넣어줄 수 있다. 서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내 서버가 만든 쿠키임을 검증할 수 있다. 쿠키는 클라이언트에서 위조하기 쉬우므로 비밀 키를 통해 만들어낸 서명을 쿠키 값 뒤에 붙인다. 서명이 붙으면 쿠키가 name=abc.sign과 같은 모양이 된다. 서명된 쿠키는 req.signedCookies 객체에 들어 있다.
cookie-parser가 쿠키를 생성할 때 쓰이는 것은 아니다. 쿠키를 생성/제거하기 위해서는 res.cookie, res.clearCookie 메소드를 사용해야 한다. res.cookie(키, 값, 옵션) 형식으로 사용한다.
exporess-session
세션 관리용 미들웨어이다. 로그인 등의 이유로 세션을 구현하거나 특정 사용자를 위한 데이터를 임시적으로 저장해둘때 매우 유용하다. 세션은 사용자별로 req.session 객체안에 유지된다.
express-session 1.5 버전 이전에는 내부적으로 cookie-parser를 사용하고 있어서 cookie-parser 미들웨어보다 뒤에 위치해야 했지만, 1.5 버전 이후에는 사용하지 않게 되어 순서가 상관없어졌다.
express-session은 인수로 세션에 대한 설정을 받는다. resave는 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정하는 것이고, saveUninitialized는 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정하는 것이다. 세션 쿠키의 이름은 name 옵션으로 설정한다.
cookie 옵션은 세션 쿠키에 대한 설정이다. maxAge, domain, path, expires, sameSite, httpOnly, secure 등 일반적인 쿠키 옵션이 모두 제공된다.
express-session으로 만들어진 req.session 객체에 값을 대입하거나 삭제해서 세션을 변경할 수 있다. 나중에 세션을 한 번에 삭제하려면 req.session.destory 메서드를 호출하면 된다. 현재 세션의 아이디는 req.sessionID로 확인할 수 있다.
세션을 강제로 저장하기 위해 req.session.save 메서드가 존재하지만, 일반적으로 요청이 끌날 때 자동으로 호출되므로 직접 scve 메서드를 호출할 일은 거의 없다.
미들웨어 특성 활용
미들웨어는 req, res, next를 매개변수로 가지는 함수로서 app.use, app.get, app.post 등으로 장착한다.
특정한 주소의 요청에만 미들웨어가 실행되게 하려면 첫 번째 인수로 주소를 넣으며 된다.
동시에 여러 개의 미들웨어를 장착할 수도 있으며 다음 미들웨어로 넘어가려면 next 함수를 호출해야 한다. 물론 내부적으로 next를 호출하는 미들웨어를 사용하면 굳이 할 필요는 없다.
하지만 미들웨어 장착 순서에 따라 어떤 미들웨어는 실행되지 않을 수도 있으니 이를 유의해야 한다.
next함수에도 물론 인수를 넣을 수 있다. 단 인수를 넣는다면 특수한 동작을 한다.
'route'라는 인수를 넣으면 다음 라우터로 error를 넣으면 에러 핸들러로 분기된다.
미들웨어 간에 데이터를 전달하는 방법으로는 req 객체에 데이터를 넣어두면 된다.
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('comdined')(req, res, next);
}else{
morgan('dev')(req, res, next);
}
});
multer
이미지, 동영상 등을 비롯한 여러 가지 파일들을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어이다.
멀티파트 형식이란 다음과 같이 enctype이 multipart/form-data인 폼을 통해 업로드하는 데이터의 형식을 의미한다.
<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이 있다면 멀티파트 형식으로 업로드할 수 있다.
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 함수의 인수로 설정을 넣는다. storage 송성에는 어디에(destination) 어떤 이름으로(filename) 저장할지 넣는다.
destination과 filename 함수의 req 매개변수에는 요청에 대한 정보가, file 객체에는 업로드한 파일에 대한 정보가 있다.
done 매개변수는 함수이다. 첫 번째 인수에는 에러가 있다면 에러를 넣고 두 번째 인수에는 실제 경로나 파일 이름을 넣어주면 된다.
현재 uploads라는 폴더에 [파일명+현재시간.확장자] 파일명으로 업로드하고 있다.
limits 속성에는 업로드에 대한 제한 사항을 설정할 수 있다.
위와 같은 설정을 사용하려면 서버에 uploads라는 폴더가 있어야 한다.
const fs = require('fs');
try{
fs.readdirSync('uploads');
} catch(err){
conosole.error(err);
fs.mkdirSync('uploads');
}
파일을 여러개 업로드할 때는 fields 미들웨어의 인수로 input 태그의 name을 각각 적으면 된다.
최종적인 html과 코드이다.
<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>
multipart.html
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(err){
console.error(err);
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.use((req, res, next) => {
console.log('모든 요청에 다 실행');
next();
});
app.get('/', (req, res, next) => {
console.log('GET / 요청에만 실행');
next();
}, (req, res) => {
throw new Error('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'), '번 포트에서 대기중...');
});
app.js