본문 바로가기

ComputerScience/NodeJs

node - 6.2 자주 사용하는 미들웨어

728x90
 

더북(TheBook): Node.js 교과서 개정 2판

 

thebook.io

* 위 내용을 정리하였음

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.single('image'), (req, res) => {
  console.log(req.file);
  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'), '번 포트에서 대기 중');
});

6.2.1 morgan

- morgan을 사용하면 요청과 응답에 대한 정보를 콘솔에 기록한다.

app.use(morgan('dev'));

- 인수로 dev외에 combined, common, short, tiny 등을 사용할 수 있고 그에 따라 로그가 달라진다.

-[HTTP 메서드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트] 를 의미한다.

 

6.2.2 static

- 정적인 파일을 제공하는 라우터 역할을 한다.

- 기본으로 제공되기 때문에 따로 설치할 필요 없이 express객체에서 꺼내 쓴다.

app.use('요청 경로', express.static('실제 경로'));

app.use('/', express.static(path.join(__dirname, 'public')));

- 함수의 인수로 정적 파일들이 담긴 폴더(이름)를 지정한다.

- 위 예시는 public이라는 폴더에 정적 파일들이 "public/stylesheets/style.css" 이렇게 담겨 있는 것이다.

- public 폴더 안에 css, js, 이미지 파일들을 넣으면 http://localhost:3000/stylesheets/style.css처럼 주소로 브라우저에서 접근할 수 있는데

- static을 사용하면 요청주소가 '/'임에도 'public'으로 알아서 연결해준다. (서버의 구조를 숨길 수 있다, 보안)

- 이렇게 static으로 경로를 지정하면 정적 파일을 알아서 제공하므로 fs.readFile로 파일을 읽고 보내줄 필요가 없다.

- public 저 경로에 해당하는 파일이 없으면 내부적으로 next를 호출해서 다음 미들웨어를 수행한다. 반대로 원하는 파일을 찾으면 당연히 next를 호출하지 않는다.

 

6.2.3 body-parser

- 요청 본문에 있는 데이터를 해석해서 req.body 객체로 만들어준다.

- AJAX요청의 데이터를 주로 처리할때 사용한다.

- 익스프레스에 body-parser 미들웨어의 일부 기능이 내장되어 있다.

- 단 요청의 내용이 RAW(버퍼 데이터)거나 Text(문자)일때는 body-parser를 설치하여야 한다.

- 멀티파트(이미지, 동영상, 파일) 데이터는 처리하지 못한다. (multer모듈 사용)

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// raw, text 사용하려면
const bodyParser = require('body-parser'); 
app.use(bodyParser.raw());
app.use(bodyParser.text());

- 서버에 요청을 할때 데이터를 함께 전달한다면 그리고 그 데이터가 JSON 형식이라면 알아서 req.body에 들어간다.

- URL-encoded는 주소 형식으로 데이터를 보내는 방식이다.

- { extended: false } : 노드의 querystring 모듈을 사용하여 쿼리스트링을 해석

- { extended: true } :  qs 모듈을 사용하여 쿼리스트링을 해석. qs 모듈은 내장 모듈이 아니다. querystring 모듈의 기능을 좀 더 확장한 모듈이다.

- 이전 4.2절에서 POST와 PUT 요청의 본문을 전달받으려면 req.on('data')와 req.on('end')로 스트림을 사용했었는데 body-parser가 내부적으로 스트림을 처리해 req.body에 추가한다.

 

- JSON 형식으로 { name: 'zerocho', book: 'nodejs' }를 본문으로 보낸다면 req.body에 그대로 들어간다.

- URL-encoded 형식으로 name=zerocho&book=nodejs를 본문으로 보낸다면 req.body { name: 'zerocho', book: 'nodejs' }가 들어간다.

 

6.2.4 cookie-parser

- 요청에 동봉된 쿠키를 해석해 req.cookies 객체로 만든다.

app.use(cookieParser(비밀키));

- name=zerocho 쿠키를 보냈다면 req.cookies { name: 'zerocho' }가 됩니다. 유효 기간이 지난 쿠키는 알아서 걸러낸다.

- 비밀키를 인수로 넣어주면 쿠키가 내 서버가 만든 쿠키인지 검증할 수 있다.

- 비밀 키로 서명을 만들어 쿠키 값 뒤에 붙여주면 name=zerocho.sign과 같은 모양이 된다. 서명된 쿠키는 req.signedCookies 객체에 들어있다.

- 쿠키 생성/제거를 위해서는 아래 메서드를 사용한다.

res.cookie('name', 'zerocho', {
  expires: new Date(Date.now() + 900000),
  httpOnly: true,
  secure: true,
});
res.clearCookie('name', 'zerocho', { httpOnly: true, secure: true });

- 쿠키를 지우려면, 키와 값 외에 옵션도 정확히 일치해야한다.( expires, maxAge 옵션은 일치할 필요가 없음)

- 옵션중 signed 옵션을 true로 설정해서 쿠키 뒤에 서명이 붙도록 한다.

- 내 서버가 쿠키를 만들었다는 것을 검증할 수 있다.

- 서명을 위한 비밀 키는 cookieParser 미들웨어에 인수로 넣은 process.env.COOKIE_SECRET이 이 된다.

 

6.2.5 express-session

- 세션 관리용 미들웨어.

- 로그인 구현, 특정 사용자를 위한 데이터를 임시 저장 할때 유용.

- 세션은 사용자 별로 req.session 객체에 유지된다.

app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
  name: 'session-cookie',
}));

 

- express-session은 인수로 세션에 대한 설정을 받는다.

- resave : 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정

- saveUninitialized : 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정

- express-session은 세션 관리 시 클라이언트에 쿠키를 보낸다. 안전하게 쿠키에 서명을 하기 위해 secret값을 사용한다.

- name : 세션 쿠키의 이름

- cookie : 세션 쿠키에 대한 설정.

  • httpOnly를 true로 설정해 클라이언트에서 쿠키를 확인하지 못하도록 한다.
  • secure는 false로 해서 https가 아닌 환경에서도 사용할 수 있게 한다.
  • 배포 시에는 https를 적용하고 secure도 true로 설정하는 것이 좋다.
req.session.name = 'zerocho'; // 세션 등록
req.sessionID; // 세션 아이디 확인
req.session.destroy(); // 세션 모두 제거

- express-session으로 만들어진 req.session 객체에 값을 대입하거나 삭제해서 세션을 변경할 수 있다.

- express-session에서 서명한 쿠키 앞에는 s:이 붙는다. 실제로는 encodeURIComponent 함수가 실행되어 s%3A가 됩니다.

- s%3A의 뒷부분이 실제 암호화된 쿠키 내용이다. 앞에 s%3A가 붙은 경우, 이 쿠키가 express-session 미들웨어에 의해 암호화된 것이라고 생각하면 된다.

 

6.2.6 미들웨어 특성 활용하기

app.use((req, res, next) => {
  console.log('모든 요청에 다 실행됩니다.');
  next();
});

- 에러 처리 미들웨어를 제외하고는 req, res, next를 갖는 함수여야 한다.

- app.use, app.post, app.get 등 에 넣어서 사용한다.

app.use(
  morgan('dev'),
  express.static('/', path.join(__dirname, 'public')),
  express.json(),
  express.urlencoded({ extended: false }),
  cookieParser(process.env.COOKIE_SECRET),
);

- 이렇게 여러개의 미들웨어를 한번에 장착해도 된다.

- 미들웨어 내부에 next함수를 호출해야 다음 미들웨어로 넘어간다.

- static처럼 next를 호출하지 않는 미들웨어는 res.send, res.sendFile등으로 응답을 보내준다. 따라서 static 이후에는 다음 미들웨어가 실행되지 않는다.

- 미들웨어 장착 순서에 따라 동작이 달라질 수 있다.

- 따라서 next도 호출하지 않고 응답도 보내지 않으면 클라이언트는 응답을 받지 못해 하염없이 기다리게 된다.

- next에 'route'를 인자로 넣어주면 다음 라우터로 이동한다.

- 인자가 없으면 다음 미들웨어로 이동한다.

- 그 외의 경우는 에러 처리 미들웨어로 들어간다.

app.use((req, res, next) => {
  req.data = '데이터 넣기';
  next();
}, (req, res, next) => {
  console.log(req.data); // 데이터 받기
  next();
});

- 이렇게 미들웨어간에 데이터를 넘겨줄 수 있다.

- req(요청)가 끝나기 전까지 '데이터 넣기'가 유지된다.

- req 객체는 요청을 보낸 사용자 개개인에게 귀속되므로 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('combined')(req, res, next);
  } else {
    morgan('dev')(req, res, next);
  }
});

 

6.2.7 multer

- 이미지, 동영상 등의 파일들을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어이다.

- 멀티파트 형식이란 다음과 같이 enctype multipart/form-data인 <from>을 통해 업로드하는 데이터의 형식을 의미한다.

- 아래 .html파일이 있다면 multipart형식으로 데이터를 업로드할 수 있다.

<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="image" />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>

- 이렇게 form을 통해 업로드 되는 파일은 body-parser로 처리할 수 없기 때문에 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 },
});

- storage 속성에 어디에(destination) 어떤 이름으로(filename) 저장할지를 정해준다.

- req에는 요청에 대한 정보가, file에는 업로드한 파일에 대한 정보가 있다.

- done은 함수로 첫 번째 인수에는 에러가 있다면 에러를 넣고, 두 번째 인수에는 실제 경로나 파일 이름을 넣어준다. 즉 req, file의 데이터를 가공해서 done으로 넘기는 것이다.

- limits 속성에는 업로드에 대한 제한 사항을 설정할 수 있다. fileSize를 5MB로 제한하였다.

 

- 위의 코드 예시를 사용하기 위해서는 서버에 uploads 폴더가 꼭 존재해야 한다. 서버에 없다면 아래 코드로 꼭 만들도록 한다.

const fs = require('fs');

try {
  fs.readdirSync('uploads');
} catch (error) {
  console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
  fs.mkdirSync('uploads');
}

- 파일을 하나만 업로드하는 경우(multipart 경우)에는 single 미들웨어를 사용한다.

<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="image" />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>
app.post('/upload', upload.single('image'), (req, res) => { 
  console.log(req.file, req.body); 
  res.send('ok'); 
});

- single 미들웨어의 인수는 form데이터의 키나 input 태그의 name과 일치하게 넣으면 된다. 따라서 'image'라고 넣었다.

- 업로드 성공 시 결과는 req.file 객체 안에 들어 있다. 

- req.body에는 파일이 아닌 데이터인 title이 들어 있다.

- req.file 객체는 다음과 같이 생겼다.

{
  fieldname: 'img',
  originalname: 'nodejs.png',
  encoding: '7bit',
  mimetype: 'image/png',
  destination: 'uploads/',
  filename: 'nodejs1514197844339.png',
  path: 'uploads\\nodejs1514197844339.png',
  size: 53357
}

- 여러 파일을 업로드하는 경우 HTML의 input 태그에는 multiple을 쓰면 된다.

<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');
});

- 미들웨어에는 single대신 array를 쓴다.

- 업로드 결과도 req.file 대신 req.files 배열에 들어 있다.


- 파일을 여러 개 업로드하지만 input 태그나 폼 데이터의 키가 다른 경우에는 fields 미들웨어를 사용한다.

<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>

- fields 미들웨어의 인수로 input 태그의 name을 각각 적는다.

app.post('/upload',
  upload.fields([{ name: 'image1' }, { name: 'image2' }]),
  (req, res) => {
    console.log(req.files, req.body);
    res.send('ok');
  },
);

- 업로드 결과도 req.files.image1, req.files.image2에 각각 들어 있다.


- 특수한 경우지만, 파일을 업로드하지 않고도 멀티파트 형식으로 업로드하는 경우 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');
});

- 파일을 업로드하지 않았으므로 req.body만 존재한다.

728x90
반응형