express 웹 서버 만들기 -2(multer, 라우팅 분리, req, res, +전체 코드)

이번 포스팅에서는 1. multer, 2. 라우팅 분리(feat. Router 객체), 3. req, res 객체, +전체 코드를 다루겠다. express 웹 서버 만들기 -1에서 사용한 app.js 파일을 마저 사용하겠다.

책 Node.js 교과서(개정 2판) 책의 6장의 내용을 참고했다.
+모든 코드는 github주소에 있다.


1. multer

multer

  • 이미지, 동영상 등을 비롯한 여러 가지 파일들을 멀티파트 형식으로 업로드할 때 사용
    **멀티파트 형식: enctype이 multipart/form-data인 폼을 통해 업로드하는 데이터의 형식
  • 미들웨어의 종류

설치

npm i multer

multer의 동작은 아래에 [multer 전체 코드]의 주석으로 설명해두었다.

+파일을 하나만 업로드하는 방법

app.post('/upload', upload.single('image'), (req, res) => {
  console.log(req.file, req.body);
  res.send('ok');
});

Git [middleware/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>
  1. upload.single('image') 미들웨어에 의해 multer 설정에 따라 파일 upload
  2. 업로드 성공 시 결과는 req.file 객체 생성
  3. req.file 객체는 아래와 같음
{
 fieldname: 'img',
 originalname: 'nodejs.png',
 encoding: '7bit',
 ...
 filename: 'nodejs1514197844339.png',
 path: 'uploads\\nodejs1514197844339.png',
 size: 53357
}

+여러 파일을 업로드하는 방법1, 2

// 여러 파일을 업로드하는 방법
app.post('/upload', upload.array('many', (req,res) => {
    console.log(req.files, req.body);
    res.send('ok');
}))
// 여러 파일을 업로드하는 방법2
app.post('/upload', upload.fields([{name: 'image1'}, {name: 'image2'}]),(req, res) => {
    console.log(req.files, req.body);
    res.send('ok');
},
);

Git [middleware/multipart2.html]

<html>
<head></head>
<body> 
<!-- 여러 파일을 업로드하는 방법 -->
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="many" multiple/>
    <!-- 여러 파일을 업로드하는 방법2 -->
    <!-- <input type ="file" name="image1"/>
    <input type="file" name="image2"/> -->
    <input type="text" name="title" />
    <input type="text" name="title" />
    <button type="submit">업로드</button>
</form>
</body>
</html>

+파일을 업로드하지 않고 멀티파트 형식으로 업로드 방법

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

Git [middleware/multipart3.html]

<html>
<head></head>
<body>
<!-- 파일을 업로드하지 않고 멀티파트 형식으로 업로드 방법 -->
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
    <input type="text" name="title" />
    <button type="submit">업로드</button>
</form>
</body>
</html>

[multer 전체 코드]

Git [middleware/app.js] 中 일부

// multer
const multer = require('multer');
const fs = require('fs');

try {
    fs.readdirSync('uploads'); // sync로 upload 폴더 읽음
}catch(error){
    console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다');
    fs.mkdirSync('uploads'); // upload 폴더가 없으면 upload가 불가능하므로 폴더 생성
}
const upload = multer({ // multer의 인수로 설정을 넣음
    storage: multer.diskStorage({ // storage(저장 위치) 속성
        destination(req, file, done){ // 어디?(요청 정보, 업로드한 파일 정보, 함수) -> req, file 데이터를 가공 후 done으로 넘김
            done(null, 'uploads/'); // done(에러가 있으면 에러를 넣음, 실제 경로(or 파일 이름))
        },
        filename(req, file, done){ // 파일 이름
            const ext = path.extname(file.originalname);
            done(null, path.basename(file.originalname, ext) + Date.now() + ext); // 이름이 겹치지 않도록 이름에 현재 시간을 저장 
            // 결국, upload라는 폴더에 [파일명+현재시간.확장자]로 업로드
        },
    }),
    limits: {fileSize: 5 * 1024 * 1024}, // 업로드 제한 사항:{파일 크기: 5mb}
});
app.get('/upload', (req, res)=> {
    res.sendFile(path.join(__dirname, 'multipart2.html'));
});
app.post('/upload', upload.fields([{name: 'image1'},{name: 'image2'}]),
    (req, res) => {
        console.log(req.files, req.body);
        res.send('ok');
    },
);

// 파일을 하나만 업로드하는 방법 (multipart.html)
// app.post('/upload', upload.single('image'), (req, res) => {
//     console.log(req.file, req.body);
//     res.send('ok');
//   });

// 여러 파일을 업로드하는 방법 (multipart2.html)
// app.post('/upload', upload.array('many', (req,res) => {
//     console.log(req.files, req.body);
//     res.send('ok');
// }))

// 여러 파일을 업로드하는 방법2 (multipart2.html)
// app.post('/upload', upload.fields([{name: 'image1'}, {name: 'image2'}]),(req, res) => {
//     console.log(req.files, req.body);
//     res.send('ok');
// },
// );

// 파일을 업로드하지 않고 멀티파트 형식으로 업로드 방법 (multipart3.html)
// app.post('/upload', upload.none(), (req, res)=> {
//     console.log(req.body); // 파일을 업로드하지 않았으므로 req.body만 존재
//     res.send('ok');
// });

// req.session.name = 'delay100'; // 세션 등록
// req.sessionID; //세션 아이디 확인
// req.session.destroy(); // 세션 모두 제거

Git [middleware/multipart2.html]

<html>
<head></head>
<body> 
<!-- 여러 파일을 업로드하는 방법 -->
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
    <!-- <input type="file" name="many" multiple/> -->
    <!-- 여러 파일을 업로드하는 방법2 -->
    <input type ="file" name="image1"/>
    <input type="file" name="image2"/>
    <input type="text" name="title" />
    <button type="submit">업로드</button>
</form>
</body>
</html>

실행화면(웹 브라우저) - http://127.0.0.1:3000/upload

실행화면(console) - 업로드 후

[Object: null prototype] {
  image1: [
    {
      fieldname: 'image1',
      originalname: '표지 8.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: 'uploads/',
      filename: '표지 81642486558893.png',
      path: 'uploads\\표지 81642486558893.png',
      size: 1059878
    }
  ],
  image2: [
    {
      fieldname: 'image2',
      originalname: '표지 7.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: 'uploads/',
      filename: '표지 71642486559197.png',
      path: 'uploads\\표지 71642486559197.png',
      size: 1055004
    }
  ]
} [Object: null prototype] { title: 'ㅎㅎ' }
POST /upload 200 394.497 ms - 2

전체 파일 - uploads 폴더가 생성되며, 업로드한 파일의 이름이 현재 시간이 추가되어 서버에 저장됨


2. 라우팅 분리(feat. Router 객체)

// 라우터 분리
const indexRouter = require('./routes'); 
const userRouter = require('./routes/user');

// 라우터 경로 설정
app.use('/', indexRouter); // localhost:3000
app.use('/user', userRouter); // localhost:3000/user

...
app.use((req, res, next) => {
    res.status(404).send('Not Found'); // 일치하는 라우터가 없을 때 404 상태 코드를 응답
});

실행화면(웹 브라우저)

+라우터에 연결된 나머지 미들웨어를 건너뛰고 싶은 경우

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){ // :id는 /user/1, /user/123 등의 요청 처리(req.params.id로 조회 가능, 만약 :type이면 req.params.type으로 조회 가능, user, type 같은 변수는 마음대로 가능)
  console.log(req.params, req.query);
});

+주소에 쿼리스트링을 쓰는 경우
ex)
주소 요청
/users/123?limit=5&skip=10
req.params와 req.query객체 console

{ id: '123 } { limit: '5', skip: '10' }

+주소는 같지만 메서드가 다른 코드가 있을 때 하나로 줄이는 방법
줄이기 전

router.get('/abc', (req, res) => {
  res.send('GET /abc');
});
router.post('/abc', (req, res) => {
  res.send('POST /abc');
});

줄인 후

router.route('/abc')
 .get((req, res) => {
  res.send('GET /abc');
})
 .post((req, res) => {
  res.send('POST /abc');
});

3. req, res 객체

req, res는 메서드 체이닝을 지원하는 경우가 많음

메서드 체이닝 EXAMPLE )

res
 .status(201)
 .cookie('test', 'test')
 .redirect('/admin');
  • 자주쓰이는 req(요청) 객체

    -req.app: req 객체를 통해 app 객체에 접근할 수 있음(ex. req.app.get('port'))
    -req.body: body-parser 미들웨어가 만드는 요청의 본문을 해석한 객체
    -req.cookies: cookie-parser 미들웨어가 만드는 요청의 쿠키를 해석한 객체
    -req.ip: 요청의 ip 주소가 담겨있음
    -req.params: 라우트 매개변수에 대한 정보가 담긴 객체
    -req.query: 쿼리스트링에 대한 정보가 담긴 객체
    -req.signedCookies: 서명된 쿠키들이 req.cookies대신 이 곳에 담김
    -req.get(헤더 이름): 헤더의 값을 가져오고 싶을 때 사용하는 메서드

  • 자주쓰이는 res(응답) 객체

    -res.app: req.app처럼 res 객체를 통해 app 객체에 접근할 수 있음
    -res.cookie(키, 값, 옵션): 쿠키를 설정하는 메서드
    -res.clearCookie(키, 값, 옵션): 쿠키를 제거하는 메서드
    -res.end(): 데이터 없이 응답을 보냄
    -res.json(JSON): JSON 형식의 응답을 보냄
    -res.redirect(주소): 리다이렉트할 주소와 함께 응답을 보냄
    -res.render(뷰, 데이터): 템플릿 엔진을 렌더링해서 응답할 때 사용하는 메서드
    -res.sendFile(경로): 경로에 위치한 파일을 응답
    -res.set(헤더, 값): 응답의 헤더를 설정
    -res.status(코드): 응답 시의 HTTP 상태 코드를 지정


+전체 코드

Git [middleware/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);

// req, res, next들은 미들웨어 내부에 들어있음
// morgan
app.use(morgan('dev')); 
// static
app.use('/', express.static(path.join(__dirname, 'public'))); 
// body-parser
app.use(express.json());
app.use(express.urlencoded({extended: false})); // extended 옵션이 false면 노드의 querystring 모듈을 사용하여 쿼리스트링을 해석
                                                // extended 옵션이 true면 qs 모듈을 사용하여 쿼리스트링을 해석 - qs 모듈은 내장 모듈이 아닌 npm의 패키지(querystring 모듈의 기능을 좀 더 확장한 모듈임)
// cookie-parser
app.use(cookieParser(process.env.COOKIE_SECRET)); // 비밀키 할당, process.env.COOKIE_SECRET에 cookiesecret 값(키=값 형식)이 할당됨
// // cookie 생성(res.cookie)
//               키, 값, 옵션
// res.cookie('name', 'delay100', {
//     expires: new Date(Date.now() + 900000),
//     httpOnly: true,
//     secure: true,
// });
// // cookie 제거(res.clearCookie)
// res.clearCookie('name', 'delay100', {httpOnly: true, secure: true});

// express-session
// 인수: session에 대한 설정
app.use(session({
    resave:false, // resave : 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정
    saveUninitialized: false, // saveUninitialized : 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
    secret: process.env.COOKIE_SECRET,
    cookie: { // 세션 쿠키에 대한 설정
        httpOnly: true, // httpOnly: 클라이언트에서 쿠키를 확인하지 못하게 함
        secure: false, // secure: false는 https가 아닌 환경에서도 사용 가능 - 배포할 때는 true로 
                        // 코드에는 없지만, store: 데이터베이스에 연결해서 세션을 유지 
    },
    name: 'session-cookie',
}));

// 라우터 경로 설정
app.use('/', indexRouter); // localhost:3000
app.use('/user', userRouter); // localhost:3000/user

// multer
const multer = require('multer');
const fs = require('fs');

try {
    fs.readdirSync('uploads'); // sync로 upload 폴더 읽음
}catch(error){
    console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다');
    fs.mkdirSync('uploads'); // upload 폴더가 없으면 upload가 불가능하므로 폴더 생성
}
const upload = multer({ // multer의 인수로 설정을 넣음
    storage: multer.diskStorage({ // storage(저장 위치) 속성
        destination(req, file, done){ // 어디?(요청 정보, 업로드한 파일 정보, 함수) -> req, file 데이터를 가공 후 done으로 넘김
            done(null, 'uploads/'); // done(에러가 있으면 에러를 넣음, 실제 경로(or 파일 이름))
        },
        filename(req, file, done){ // 파일 이름
            const ext = path.extname(file.originalname);
            done(null, path.basename(file.originalname, ext) + Date.now() + ext); // 이름이 겹치지 않도록 이름에 현재 시간을 저장 
            // 결국, upload라는 폴더에 [파일명+현재시간.확장자]로 업로드
        },
    }),
    limits: {fileSize: 5 * 1024 * 1024}, // 업로드 제한 사항:{파일 크기: 5mb}
});
app.get('/upload', (req, res)=> {
    res.sendFile(path.join(__dirname, 'multipart2.html'));
});
app.post('/upload', upload.fields([{name: 'image1'},{name: 'image2'}]),
    (req, res) => {
        console.log(req.files, req.body);
        res.send('ok');
    },
);

// 파일을 하나만 업로드하는 방법 (multipart.html)
// app.post('/upload', upload.single('image'), (req, res) => {
//     console.log(req.file, req.body);
//     res.send('ok');
//   });

// 여러 파일을 업로드하는 방법 (multipart2.html)
// app.post('/upload', upload.array('many', (req,res) => {
//     console.log(req.files, req.body);
//     res.send('ok');
// }))

// 여러 파일을 업로드하는 방법2 (multipart2.html)
// app.post('/upload', upload.fields([{name: 'image1'}, {name: 'image2'}]),(req, res) => {
//     console.log(req.files, req.body);
//     res.send('ok');
// },
// );

// 파일을 업로드하지 않고 멀티파트 형식으로 업로드 방법 (multipart3.html)
// app.post('/upload', upload.none(), (req, res)=> {
//     console.log(req.body); // 파일을 업로드하지 않았으므로 req.body만 존재
//     res.send('ok');
// });

// req.session.name = 'delay100'; // 세션 등록
// req.sessionID; //세션 아이디 확인
// req.session.destroy(); // 세션 모두 제거

app.use((req, res, next) => {
    res.status(404).send('Not Found'); // 일치하는 라우터가 없을 때 404 상태 코드를 응답
});

app.use((req, res, next) => {
    console.log('모든 요청에 다 실행됩니다.');
    req.data = '데이터 넣기';
    next();
}, (req, res, next)=>{
    console.log(req.data);
    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'), '번 포트에서 대기 중');
});

Git [routes/index.js]

const express = require('express');

const router = express.Router();

// GET / 라우터
router.get('/', (req, res) =>{
    res.send('Hello, Express');
});

module.exports = router;

Git [middleware/routes/user.js]

const express = require('express');

const router = express.Router();

// GET /user 라우터
router.get('/', (req, res) =>{
    res.send('Hello, User');
});

module.exports = router;

Git [middleware/multipart2.html]

<html>
<head></head>
<body> 
<!-- 여러 파일을 업로드하는 방법 -->
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
    <!-- <input type="file" name="many" multiple/> -->
    <!-- 여러 파일을 업로드하는 방법2 -->
    <input type ="file" name="image1"/>
    <input type="file" name="image2"/>
    <input type="text" name="title" />
    <button type="submit">업로드</button>
</form>
</body>
</html>

Git [middleware/package.json]

{
  "name": "delay100_middleware",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app"
  },
  "author": "delay100",
  "license": "ISC",
  "dependencies": {
    "cookie-parser": "^1.4.6",
    "dotenv": "^14.1.0",
    "express": "^4.17.2",
    "express-session": "^1.17.2",
    "morgan": "^1.10.0",
    "multer": "^1.4.4"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

Git [middleware/.env]

COOKIE_SECRET=cookiesecret

좋은 웹페이지 즐겨찾기