Node.js - Express (1): 미들웨어


이 포스트는 Express 에 대해 알아본다.

소스는 assu10/nodejs.git 에 있습니다.

  • Express 사용
  • 미들웨어
    • morgan
    • static
    • body-parser
    • cookie-parser
    • express-session
    • 미들웨어 내용 정리
    • multer
      • upload.single('img') - req.file 객에체 하나의 파일만 업로드
      • upload.array('imgs') - req.files 객체에 input 태그의 name 이 동일한 여러 개의 파일을 업로드
      • upload.fields([{ name: 'imagename1' }, { name: 'imagename2' }]) - req.files 객체에 input 태그의 name 이 다른 여러 개의 파일을 업로드
      • upload.none() - 파일 업로드 없이 텍스트 데이터만 multipart 형식으로 전송

npm 에는 서버를 제작하는 과정에서의 불편함을 해소하고 편의 기능을 추가한 Express 라는 웹 서버 프레임워크가 있다.

웹 서버 프레임워크에 Express 외에도 koa, hapi 같은 프레임워크가 있지만 아래 그래프를 보면 express 가 npm 패키지 다운로드 수가 월등히 높은 것을 알 수 있다.

https://www.npmtrends.com/express-vs-hapi-vs-koa


1. Express 사용

npm init --y 명령어로 package.json 을 생성한 후 express 와 nodemon 패키지를 설치한다.

> npm init --y

> npm i express
> npm i -D nodemon

package.json

{
  "name": "chap06",
  "version": "1.0.0",
  "description": "learn express",
  "main": "app.js",
  "scripts": {
    "start": "nodemon app"
  },
  "author": "assu",
  "license": "ISC",
  "devDependencies": {
    "eslint": "^8.3.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "nodemon": "^2.0.15",
    "prettier": "2.5.0"
  },
  "dependencies": {
    "express": "^4.17.1"
  }
}

scripts 부분에 start 속성 nodemon app 을 기입한 후 nodemon app 으로 서버를 실행한다. (nodemon 으로 app.js 를 실행한다는 의미)
서버 코드에 수정 사항이 생길때마다 매번 서버를 재시작하기 귀찮은데 nodemon 모듈로 서버를 자동으로 재시작하게 해준다.

nodemon 이 실행되는 콘솔에 rs 를 입력하여 수동으로 재시작할 수도 있다.

운영 환경에서는 서버 코드가 빈번하게 변경될 일이 없으므로 nodemon 은 개발용으로만 사용하는 것을 권장한다.

app.js

const express = require('express');

const app = express();
app.set('port', process.env.PORT || 3000);

app.get('/', (req, res) => {
  res.send('Hello~');
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트 대기중');
});

express 내부에 http 모듈이 내장되어 있으므로 서버의 역할을 할 수 있다.

express 에서는 res.write, res.end 대신 res.send 를 사용한다.
res.writeHead, res.write, res.end 등의 메서드는 http 모듈의 기능이고,
res.send, res.sendFile 은 express 가 추가한 메서드이다.

app.get 외에도 app.post, app.put, app.patch, app.delete, app.options 메서드가 존재한다.

npm start 로 서버를 실행해보자.

> npm start        

> chap06@1.0.0 start
> nodemon app

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
3000 번 포트 대기중
rs
[nodemon] starting `node app.js`
3000 번 포트 대기중

문자열 대신 파일로 응답하려면 res.sendFile 을 이용한다.

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~');
  console.log(__dirname);
  res.sendFile(path.join(__dirname, '/index.html'));
});

app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트 대기중');
});
/Users/assu/Developer/01_nodejs/mynode/chap06

2. 미들웨어

미들웨어는 express 의 핵심이다.
요청과 응답의 중간에 위치하여 미들웨어라고 부른다. 라우터와 에러 핸들러 또한 미들웨어의 일종이다.

미들웨어는 app.use 와 함께 사용된다. 예) app.use(미들웨어)

express 서버에 미들웨어를 연결해보자.

app.js

const express = require('express');

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'), '번 포트 대기중');
});

미들웨어는 app.use 에 매개 변수가 req, res, next 인 함수를 넣으면 된다.
미들웨어는 위에서 아래로 순서대로 실행되면서 요청과 응답 사이에 특별한 기능을 추가할 수 있다.

next 라는 세 번째 매개변수는 다음 미들웨어로 넘어가는 함수이다.
next 를 실행하지 않으면 다음 미들웨어가 실행되지 않는다.

주소를 첫 번째 매개변수로 넣지 않으면 미들웨어는 모든 요청에서 실행되고, 주소를 넣으면 해당 요청에서만 실행된다.

예) app.use(미들웨어) - 모든 요청에서 미들웨어 실행
app.use('/aa', 미들웨어) - aa 로 시작하는 요청에서 미들웨어 실행
app.post('/aa', 미들웨어) - aa 로 시작하는 POST 요청에서 미들웨어 실행

app.use 나 app.get 같은 라우터에 미들웨어를 여러 개 붙일 수도 있다. 위 코드에선 app.get 라우터에 미들웨어 2개가 연결되어 있다.

app.get(
  '/',
  (req, res, next) => {     // 첫 번째 미들웨어
    console.log('GET / 요청에서만 실행됨');
    next();
  },
  (req, res) => {           // 두 번째 미들웨어
    throw new Error('에러는 에러 처리 미들웨어로 보냄');
  },
);

에러 처리 미들웨어는 매개 변수가 err, req, res, next 로 4개이다.
모든 매개 변수를 사용하지 않아도 매개 변수는 반드시 4개 이어야 한다.
res.status 로 HTTP 상태 코드를 지정할 수 있는데 기본값은 200 이다.

에러 처리 미들웨어를 직접 연결하지 않아도 기본적으로 express 가 에러를 처리하지만 실무에서는 직접 에러 처리 미들웨어를 연결해주는 것이 좋다.

에러 처리 미들웨어는 특별한 경우가 아니면 가장 아래에 위치하도록 한다.

에러 처리 미들웨어에 대한 상세 내용은 Node.js - Express (2): 라우터, 템플릿 엔진4. 에러 처리 미들웨어 를 참고하세요.

localhost:3000 에 접속하면 콘솔에 아래와 같이 출력된다.

모든 요청에 다 실행됨
GET / 요청에서만 실행됨
Error: 에러는 에러 처리 미들웨어로 보냄
    at /Users/assu/Developer/01_nodejs/mynode/chap06/app.js:18:11
    at Layer.handle [as handle_request] (/Users/assu/Developer/01_nodejs/mynode/chap06/node_modules/express/lib/router/layer.js:95:5)
    at next (/Users/assu/Developer/01_nodejs/mynode/chap06/node_modules/express/lib/router/route.js:137:13)
    at /Users/assu/Developer/01_nodejs/mynode/chap06/app.js:15:5

실무에서 자주 사용하는 미들웨어 패키지들에 대해 알아보자.

> npm i morgan cookie-parser express-session dotenv

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', // 세션 쿠키명 (기본값은 connect.sid)
        }),
);

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

설치한 패키지들을 불러온 뒤 app.use 에 연결한다.
req, res, next 가 없는 이유는 미들웨어 내부에 이미 들어있기 때문이다. next 도 내부적으로 호출하기 때문에 다음 미들웨어로 넘어갈 수 있다.

dotenv 패키지는 .env 파일을 읽어서 process.env 로 만든다.
process.env.COOKIE_SECRET 에 cookiesecret 값이 할당된다.

process.env 를 별도의 파일로 관리하는 이유는 보안과 설정의 편의성 때문이다.
비밀키들을 소스 코드에 그대로 적어두면 소스 코드가 유철되었을 때 키도 같이 유출되는데 .env 같은 별도의 파일에 비밀 키를 적어두고 dotenv 패키지로 비밀 키를 로딩하는 방식으로 관리하면 소스 코드가 유출되더라도 .env 파일만 잘 관리하면 비밀 키는 지킬 수 있다.


2.1. morgan

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

morgan 연결 후 localhost:3000 으로 접속 시 기존에 나오던 로그 외에 추가적인 로그를 볼 수 있다.

3000 번 포트 대기중
모든 요청에 다 실행됨
GET / 요청에서만 실행됨
Error: 에러는 에러 처리 미들웨어로 보냄
// 에러 스택 트레이스 생략
GET / 500 8.151 ms - 46

콘솔의 GET / 500 8.151 ms - 46 는 morgan 미들웨어에서 나오는 것이다.
morgan 미들웨어는 요청과 응답에 대한 정보를 콘솔에 기록하여 요청과 응답을 한 눈에 볼 수 있어 편하다.

인수로 dev 외에 combined, common, short, tiny 등을 넣을 수 있고, 각 인수바다 로그가 달라진다.
주로 개발 환경에서는 dev, 배포 환경에서는 combined 를 사용한다.

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


2.2. static

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

static 미들웨어는 정적인 파일들을 제공하는 라우터 역할을 한다.
기본적으로 제공되기 때문에 따로 설치할 필요없이 express 객체 안에서 꺼내쓰면 된다.

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

예) app.use(‘/’, express.static(path.join(__dirname, ‘public’)));

예를 들어 public/css/style.css 에 css 파일이 있다면 http://localhost:3000/css/style.css 으로 접근이 가능하다.

서버 폴더 경로와 요청 경로가 다르기 때문에 외부인이 서버의 구조를 쉽게 파악할 수 없다.

또한 정적 파일을 알아서 제공해주기 때문에 fs.readFile 로 파일을 직접 읽어서 전송할 필요도 없다.
요청 경로에 해당 파일이 없으면 알아서 내부적으로 next 를 호출하고, 파일을 발견하면 응답으로 파일을 보내고 다음 미들웨어는 호출하지 않는다.


2.3. body-parser

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

body-parser 는 요청 본문에 있는 데이터를 해석하여 req.body 객체로 만들어주는 미들웨어이다.
단, multiPart (이미지, 동영상, 파일) 데이터는 처리하지 못한다. 이런 경우는 뒤에 나올 multer 모듈을 사용한다.

express 4.16.0 부터 body-parser 미들웨어의 일부 기능이 express 에 내장되어 따로 설치할 필요는 없지만 버퍼나 텍스트 형식의 데이터 처리시엔 따로 설치해서 사용해서 한다.

body-parser 는 JSON, URL-encoded 형식 외 Raw, Text 형식의 데이터도 추가로 해석할 수 있는데
Raw 는 요청의 본문이 버퍼 데이터인 경우, Text 는 텍스트 데이터일 경우이다.

> npm i body-parser
const bodyParser = require('body-parser');
app.use(bodyParser.raw());
app.use(bodyParser.text());

요청 데이터의 종류를 보면 아래와 같다.

  • JSON
    • JSON 형식의 데이터 전달 방식
  • URL-encoded
    • 주소 형식으로 데이터는 보내는 방식
    • 폼 전송은 URL-encoded 방식을 주로 사용
    • { extended: false }
      • false: 노드의 내장 모듈인 querystring 모듈을 사용하여 쿼리스트링 해석
      • true: npm 패키지인 qs 모듈을 사용하여 쿼리 스트링 해석

앞에서 POST, PUT 요청의 본문을 받을 때 req.on(‘data’), req.on(‘end’) 로 스트림을 받았는데 body-parser 를 사용하면 body-parser 가 내부적으로 스트림을 처리해 req.body 에 추가해준다.

예를 들어 JSON 형식으로 { name: ‘assu’, age: 30 } 으로 본문을 보내면 req.body 에 그대로 들어가고,
URL-encoded 형식인 name=assu&age=30 으로 본문을 보내면 req.body 에 { name: ‘assu’, age: 30 } 으로 들어간다.


app.use(cookieParser(process.env.COOKIE_SECRET));

cookie-parser 는 request 에 있는 쿠키를 해석하여 req.cookies 객체로 만든다.
Node.js - http 모듈로 서버 생성3. 쿠키와 세션 에 나온 parseCookies 함수와 비슷한 기능이다.

예를 들어 name=assu 쿠키가 있다면 req.cookie 는 { name: ‘assu’ } 가 된다.
유효기간이 지난 쿠키는 알아서 걸러낸다.

cookieParser 의 인수로 비밀키 를 넣어줄 수 있는데 서명된 쿠키가 있는 경우 제공한 비밀키를 통해 해당 쿠키가 내 서버에서 만든 쿠키임을 검증할 수 있다.

쿠키는 클라이언트에서 위조하기 쉽기 때문에 비밀키를 통해 만들어낸 서명을 쿠키값 뒤에 붙이는데 서명이 붙은 쿠키는 name=assu.sign 과 같은 모양이다.
서명된 쿠키는 req.cookie 대신 req.signedCookies 객체에 들어간다.

주의할 것은 cookie-parser 가 쿠키를 생성할 때 쓰는게 아니라 쿠키를 해석하여 req 객체에 넣어주는 역할이라는 것이다.

쿠키를 생성/제거할 때는 res.cookie, res.clearCookie 메서드를 사용한다.
res.cookie(key, value, option) 형식이다.

옵션은 Node.js - http 모듈로 서버 생성3. 쿠키와 세션 에 나온 쿠키 옵션과 동일하다.

  • 쿠키명=쿠키값
  • Expires=날짜
    • 기본값은 클라이언트가 종료될 때 까지임
  • Max-age=초
    • Expires 와 비슷하지만 날짜 대신 초를 입력, Expires 보다 우선함
  • Domain=도메인명
    • 쿠키가 전송될 도메인을 특정함, 기본값은 현재 도메인.
  • Path=URL
    • 쿠키가 전송될 URL 을 특정함, 기본값은 / 이고, 이 경우 모든 URL 에서 쿠키 전송 가능
  • Secure
    • true: HTTPS 일 경우에만 쿠키 전송
    • false: HTTPS 가 아닌 환경에서도 쿠키 전송
  • HttpOnly
    • 설정 시 자바스크립트에서 쿠키에 접근할 수 없음(즉, 클라이언트에서 쿠키 확인 불가), 쿠키 조작 방지를 위해 설정하는 것이 좋음

쿠키 생성 예시

const expires = new Date();
  // 쿠키 유효 시간을 현재시간 + 5분으로 설정
  expires.setMinutes(expires.getMinutes() + 5);

  res.cookie('name', 'assu', {
    expires: expires,
    httpOnly: true,
    secure: true,
  });

쿠키를 제거하려면 키와 값, 옵션이 정확히 일치해야 한다. (단, expires, maxAge 옵션은 제외)

옵션 중에 signed 옵션을 true 로 설정 시 쿠키 뒤에 서명이 붙는다.
내 서버에서 만든 쿠키임을 검증할 수 있으므로 대부분의 경우 서명 옵션을 활성화하는 것이 좋다.


2.5. express-session

app.use(
  session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET,
    cookie: {
      // 세션 쿠키에 대한 설정
      httpOnly: true,
      secure: false,
    },
    name: 'session-cookie', // 세션 쿠키명 (기본값은 connect.sid)
  }),
);

express-session세션 관리용 미들웨어 이다.

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

express-session 1.5 버전 이전에는 내부적으로 cookie-parser 를 사용하고 있어서 cookie-parser 미들웨어보다 뒤에 위치해야 했지만
1.5 버전 이후부터는 사용하지 않게 되어 순서가 상관없어졌다.

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

  • resave
    • 요청이 올 때 세션에 수정사항이 생기지 않아도 세션을 다시 저장할 지 여부
  • saveUninitialized
    • 세션에 저장할 내역이 없어도 처음부터 세션을 생성할 지 여부
  • secret
    • express-session 은 세션 관리 시 클라이언트로 쿠키를 보내는데 Node.js - http 모듈로 서버 생성3. 쿠키와 세션 에 나온 세션 쿠키 가 바로 이것임.
    • 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가해야 하고, 쿠키를 서명하는데 비밀키 가 필요함
    • cookie-parsersecret 값과 동일하게 설정하는 것이 좋음
  • cookie
    • 세션 쿠키에 대한 설정
    • maxAge, domain, path, expires, sameSite, httpOnly, secure 등 일반적인 쿠키 옵션이 모두 제공됨
  • name
    • 세션 쿠키의 이름, 기본값은 connect.sid
  • store
    • 지금은 메모리에 세션을 저장하고 있지만 서버 재시작 시 메모리가 초기화되어 세션이 사라지므로 배포 시에는 store 에 DB 를 연결하여 세션을 유지함

store 로 레디스가 많이 사용되는데 사용법은 추후 포스팅 예정입니다.


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

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

세션을 한번에 삭제하려면 req.session.destroy() 를 사용한다.

현재 세션의 아이디는 req.sessionID 로 확인한다.

세션을 강제로 저장하기 위해 req.session.save() 가 있긴한데, 일반적으로 요청이 끝날 때 자동으로 호출되기 때문에 직접 호출할 일은 거의 없다.

express-session 에서 서명한 세션 쿠키의 모양은 약간 특이한데 쿠키 앞에 s: 가 붙는다.
실제로는 encodeURIComponent 함수가 실행되어 s%3A 가 된다.
따라서 앞에 s%3A 가 붙은 경우 이 쿠키가 express-session 미들웨어에 의해 암호화된 것으로 생각하면 된다.


2.6. 미들웨어 내용 정리

지금까지 미들웨어를 직접 만들기도 하고, 미들웨어 패키지를 설치해 사용해보기도 하였다.

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

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).send(err.message);
});

미들웨어는 req, res, next 를 매개변수로 갖는 함수 (에러 처리 미들웨어만 예외적으로 err, req, res, next) 로서,
app.useapp.get, app.post 등으로 장착하여 사용한다.
특정 주소의 요청에만 미들웨어가 실행되게 하려면 첫 번째 인수에 주소를 넣으면 된다.


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

이렇게 동시에 여러 개의 미들웨어를 장착할 수도 있고, 다음 미들웨어로 넘어가려면 next() 를 호출해야 하지만 위 미들웨어들은 내부적으로 next 를 호출하고 있기 때문에 연달아 사용이 가능하다.
next 를 호출하지 않는 미들웨어는 res.send 나 res.sendFile 등의 메서드로 응답을 보내야 한다.

express.static, multer 과 같은 미들웨어는 정적 파일을 제공할 때 next 대신 res.sendFile 메서드로 응답을 보낸다.
따라서 정적 파일을 제공하는 경우엔 위 코드에서 express.json, express.urlencoded, cookieParser 미들웨어는 실행되지 않는다.
미들웨어 장착 순서에 따라 어떤 미들웨어는 실행이 되지 않을수도 있다는 것을 기억해야 한다.

그럼 항상 express.static 은 마지막에 두어야 하나…? (잘 모르겠다. ;;)

next 도 호출하지 않고 응답도 보내지 않으면 클라이언트는 응답을 받지 못해 계속 대기상태에 있게 된다.

next 에 인수를 넣을 수도 있다.

  • next() -> 다음 미들웨어로 이동
  • next('route') -> 다음 라우터의 미들웨어로 이동
  • next(route 외의 다른 인수) -> 에러 처리 미들웨어로 이동, 이 때의 인수는 에러 처리 미들웨어의 err 매개변수가 된다.
    예) next(err1) -> (err1, req, res, next) => { }

미들웨어 간에 데이터를 전달하는 방법도 있다.

세션은 세션이 유지되는 동안 데이터도 유지되므로 요청이 끝날 때까지만 데이터를 유지하고 싶다면 req 객체에 데이터를 넣어두면 된다.

app.use((req, res, next) => {
  req.data1 = '데이터를 넣어요.';
  next();
}, (req, res, next) => {
  console.log(req.data1);
  next();
})

앞에서 app.set(‘port’, process.env.PORT || 3000) 과 같이 app.set 으로 express 에서 데이터를 저장하였다.
app.set 으로 데이터 저장 시 app.get 혹은 req.app.get 으로 어디서든 데이터를 가져올 수 있다.
하지만 app.set 은 express 전역적으로 사용되기 때문에 개별 데이터를 넣기엔 부적절하므로 미들웨어 간 데이터 공유시에는 req 객체를 이용하도록 한다.


아래는 미들웨어 안에 미들웨어를 넣는 방법으로 조건에 따라 다른 미들웨어를 적용하는 패턴이다.

app.use(morgan('dev')); // 이 부분을

app.use((req, res, next) => {   // 이렇게 변경
    morgan('dev')(req, res, next);
})

위 부분을 확장하여 조건에 따라 다른 미들웨어를 적용할 수 있다.

app.use((req, res, next) => {
    if (precess.env.NODE_ENV === 'production') {
        morgan('combined')(req, res, next);
    } else {
      morgan('dev')(req, res, next);
    }
})

2.7. multer

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

멀티파트 형식

enctype 이 multipart/form-data 인 폼을 통해 업로드하는 데이터의 형식

> npm i multer

patheslint/prettier 셋팅 + Node.js - 기본 개념 (1): 내장 객체, 내장 모듈, util 의 4.2. path 를 참고하세요.

app.js

const multer = require('multer');
const path = require('path');

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 }, // 5MB
});
  • storage
    • destination, filename
      • done 매개변수는 함수
      • 에러가 있다면 done 의 첫 번째 인수에 에러를 넣고, 두 번째 인수엔 실제 경로나 파일명을 넣어줌
      • req 나 file 의 데이터를 가공하여 done 으로 넘기는 형식
    • disk 외에 aws-sdkmulter-s3 를 이용하여 S3 에 저장할 수도 있고, memory 에 저장할 수도 있음
  • limits
    • 업로드에 대한 제한 사항 설정

s3 와 memory 에 파일 업로드 예시

const multerS3 = require('multer-s3');
const aws = require('aws-sdk');

let s3 = new aws.S3();

upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: 'xxx',
    contentType: multerS3.AUTO_CONTENT_TYPE,
    metadata: function(req, file, done) {
        done(null, {fieldName: file.fieldname});
    },
    key: async function(req, file, done) {
      const ext = path.extname(file.originalname);
      done(null, path.basename(file.originalname, ext) + Date.now() + ext);
    },
    acl: 'public-read-write'
  })
})



upload2 = multer({ inMemory: true });

파일을 업로드할 폴더가 없으면 오류가 나므로 서버가 시작할 때 생성해준다.

app.js

try {
  fs.readdirSync('uploads');
} catch (err) {
  console.error('uploads 폴더가 없으므로 uploads 폴더 생성');
  fs.mkdirSync('uploads');
}

fs.readdirSync() 에 대한 내용은 Node.js - 기본 개념 (3): 파일시스템4.2. fs.readdir, fs.unlink, fs.rmdir 를 참고하세요.


여기까지 설정하고 나면 upload 변수가 생기는데 이 변수엔 여러 종류의 미들웨어가 들어있다.
여기서는 3가지로 나누어 살펴보도록 한다.

2.7.1. upload.single('img') - req.file 객에체 하나의 파일만 업로드

하나의 파일만 업로드하는 경우 single 미들웨어 를 사용한다.

multipart.html

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

app.js

// 하나의 파일만 업로드 하는 경우
app.post('/upload', upload.single('imagename'), (req, res) => {
  console.log(req.file, req.body);
  res.send('ok');
});

이렇게 single 미들웨어 를 라우터 미들웨어 앞에 넣어두면 multer 설정에 따라 파일을 업로드 한 후 req.file 생성하여 리턴한다.
req.bodytitle 처럼 파일이 아닌 데이터가 들어간다.

localhost:3000/upload 로 접속하여 파일을 업로드해보자.

하나의 파일만 업로드하는 경우

npm start

> chap06@1.0.0 start
> nodemon app

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
uploads 폴더가 없으므로 uploads 폴더 생성
3000 번 포트에서 대기 중
GET /upload 200 7.989 ms - 241
req.file:  {
  fieldname: 'imagename',
  originalname: '이재훈.jpeg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'uploads/',
  filename: '이재훈1638441412036.jpeg',
  path: 'uploads/이재훈1638441412036.jpeg',
  size: 203831
}
req.body:  [Object: null prototype] { title: '' }
POST /upload 200 14.596 ms - 2

2.7.2. upload.array('imgs') - req.files 객체에 input 태그의 name 이 동일한 여러 개의 파일을 업로드

upload.single 미들웨어 가 아닌 upload.array 미들웨어 를 사용하며, 업로드 결과가 req.file 이 아닌 req.files 배열에 들어간다.

multipart.html

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

app.js

// input 태그의 name 이 동일한 여러 개의 파일을 업로드하는 경우
app.post('/upload', upload.array('imagename'), (req, res) => {
  console.log('req.file: ', req.file);
  console.log('req.files: ', req.files);
  console.log('req.body: ', req.body);
  res.send('ok');
});

input 태그의 name 이 동일한 여러 개의 파일을 업로드하는 경우

req.file:  undefined
req.files:  [
  {
    fieldname: 'imagename',
    originalname: '이재훈.jpeg',
    encoding: '7bit',
    mimetype: 'image/jpeg',
    destination: 'uploads/',
    filename: '이재훈1638442154979.jpeg',
    path: 'uploads/이재훈1638442154979.jpeg',
    size: 203831
  },
  {
    fieldname: 'imagename',
    originalname: 'express-hapi-koa.png',
    encoding: '7bit',
    mimetype: 'image/png',
    destination: 'uploads/',
    filename: 'express-hapi-koa1638442154982.png',
    path: 'uploads/express-hapi-koa1638442154982.png',
    size: 254031
  }
]
req.body:  [Object: null prototype] { title: '' }
POST /upload 200 15.289 ms - 2

2.7.3. upload.fields([{ name: 'imagename1' }, { name: 'imagename2' }]) - req.files 객체에 input 태그의 name 이 다른 여러 개의 파일을 업로드

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

multipart.html

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

app.js

// input 태그의 name 이 다른 여러 개의 파일을 업로드하는 경우
app.post(
  '/upload',
  upload.fields([{ name: 'imagename1' }, { name: 'imagename2' }]),
  (req, res) => {
    console.log('req.files: ', req.files);
    console.log('req.body: ', req.body);
    res.send('ok');
  },
);
req.files:  [Object: null prototype] {
  imagename1: [
    {
      fieldname: 'imagename1',
      originalname: '이재훈.jpeg',
      encoding: '7bit',
      mimetype: 'image/jpeg',
      destination: 'uploads/',
      filename: '이재훈1638442482272.jpeg',
      path: 'uploads/이재훈1638442482272.jpeg',
      size: 203831
    }
  ],
  imagename2: [
    {
      fieldname: 'imagename2',
      originalname: 'express-hapi-koa.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: 'uploads/',
      filename: 'express-hapi-koa1638442482275.png',
      path: 'uploads/express-hapi-koa1638442482275.png',
      size: 254031
    }
  ]
}
req.body:  [Object: null prototype] { title: '' }

2.7.4. upload.none() - 파일 업로드 없이 텍스트 데이터만 multipart 형식으로 전송

이미지를 미리 업로드하고 req.body 에 이미지 데이터가 아닌 이미지 URL (텍스트) 만 있는 경우 사용한다.


위에서 본 내용의 전체 소스는 아래와 같다.

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 multer = require('multer');
const path = require('path');
const fs = require('fs');

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

try {
  fs.readdirSync('uploads');
} catch (err) {
  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 }, // 5MB
});

app.get('/upload', (req, res) => {
  res.sendFile(path.join(__dirname, 'multipart.html'));
});

// 하나의 파일만 업로드 하는 경우
/*app.post('/upload', upload.single('imagename'), (req, res) => {
  console.log('req.file: ', req.file);
  console.log('req.body: ', req.body);
  res.send('ok');
});*/

// input 태그의 name 이 동일한 여러 개의 파일을 업로드하는 경우
/*app.post('/upload', upload.array('imagename'), (req, res) => {
  console.log('req.file: ', req.file);
  console.log('req.files: ', req.files);
  console.log('req.body: ', req.body);
  res.send('ok');
});*/

// input 태그의 name 이 다른 여러 개의 파일을 업로드하는 경우
app.post(
  '/upload',
  upload.fields([{ name: 'imagename1' }, { name: 'imagename2' }]),
  (req, res) => {
    console.log('req.files: ', req.files);
    console.log('req.body: ', req.body);
    res.send('ok');
  },
);

app.get(
  '/',
  (req, res, next) => {
    console.log('GET / 요청에서만 실행됩니다.');
    next();
  },
  () => {
    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'), '번 포트에서 대기 중');
});

본 포스트는 조현영 저자의 Node.js 교과서 2판을 기반으로 스터디하며 정리한 내용들입니다.

참고 사이트 & 함께 보면 좋은 사이트






© 2020.08. by assu10

Powered by assu10