Node.JS - Multer로 파일 업로드

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

git clone https://github.com/a-mean-blogger/multer-example.git
cd multer-example
git reset --hard ec58098
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/multer-example/tree/ec580985b4af566f951130360d4f39c3c9fcbe0e


Node.JS Express 서버로 파일을 업로드 하는 방법을 알아봅시다. Multer라는 package를 사용합니다.

이 강의는 Node.JS/Express로 기본적인 웹사이트를 만들 수 있는 분들을 대상으로 합니다. (최소 MEAN Stack 강의 시리즈의 Hello World!까지를 읽은 분들을 대상으로 합니다.)

multer의 기능을 확인해 볼 수 있는 간단한 웹페이지를 만들텐데 먼저 완성된 사이트를 살펴봅시다.

한 페이지에 4개의 form이 존재합니다.

  1. Single File Upload: 하나의 파일을 업로드 합니다. multer의 기본설정으로 파일 이름이 무작위값으로 바뀝니다.
  2. Single File Upload (Keep Original Filename): 하나의 파일을 업로드 합니다. multer에 설정을 추가하여 파일이름을 그대로 유지하도록합니다.
  3. Multiple File Upload: 여러개 파일을 한번에 업로드 합니다. multer의 기본설정으로 파일 이름이 무작위값으로 바뀝니다.
  4. Multiple File Upload (Keep Original Filename): 여러개 파일을 한번에 업로드 합니다. multer에 설정을 추가하여 파일이름을 그대로 유지하도록합니다.

여기서 볼 수 있듯이 multer는 기본 설정이 파일명을 무작위값으로 변경하는 것입니다. 오히려 기존의 파일명을 유지하려면 추가적인 설정이 필요하죠. 보안을 생각하여 이렇게 만든 것 같습니다. 어쨌든 파일을 업로드하면 서버에 파일이 생성되고 아래와 같은 confirmation 페이지로 이동하게 됩니다.

confirmation 페이지는 서버에 업로드된 파일의 정보를 보여줍니다. 위 페이지의 내용에 대해서는 본문에서 자세히 살펴보도록 하겠습니다.

프로젝트 생성 및 Package 설치

프로젝트 폴더를 생성합니다. 해당 폴더에서 command line(cmd, git bash 등)에 아래 명령어 입력하여 node.js 프로젝트를 생성합니다.

$ npm init --yes

프로젝트에 필요한 package들도 설치해줍니다.

$ npm install express ejs multer --save

multer가 이번 강의의 핵심 패키지입니다. 나머지 패키지들은 이미 MEAN Stack 강의 시리즈에서 설명을 하였습니다.

폴더 구조


코드

// index.js

var express   = require('express');
var app       = express();
var fs        = require('fs'); // 1

app.set('view engine', 'ejs');

// Routes
app.use('/', require('./routes/main'));

// Port setting
var port = 3000;
app.listen(port, function(){
  var dir = './uploadedFiles';
  if (!fs.existsSync(dir)) fs.mkdirSync(dir); // 2

  console.log('server on! http://localhost:'+port);
});

1. fs는 node.js에 들어 있는 module로 file system의 약자입니다. 서버의 파일/폴더에 접근할 수 있는 함수들이 들어 있습니다. fs 모듈은 파일업로드와 직접적인 연관이 있는 모듈은 아니고, 업로드될 파일을 저장할 폴더를 생성하기 위해서만 사용했습니다.

2. fs.existsSync()함수로 폴더가 존재하는지 확인하고, 없으면 fs.mkdirSync()함수로 폴더를 생성해 줍니다.

// routers/main.js

var express  = require('express');
var router   = express.Router();
var multer   = require('multer'); // 1

var storage  = multer.diskStorage({ // 2
  destination(req, file, cb) {
    cb(null, 'uploadedFiles/');
  },
  filename(req, file, cb) {
    cb(null, `${Date.now()}__${file.originalname}`);
  },
});
var upload = multer({ dest: 'uploadedFiles/' }); // 3-1
var uploadWithOriginalFilename = multer({ storage: storage }); // 3-2

router.get('/', function(req,res){
  res.render('upload');
});

router.post('/uploadFile', upload.single('attachment'), function(req,res){ // 4 
  res.render('confirmation', { file:req.file, files:null });
});

router.post('/uploadFileWithOriginalFilename', uploadWithOriginalFilename.single('attachment'), function(req,res){ // 5
  res.render('confirmation', { file:req.file, files:null });
});

router.post('/uploadFiles', upload.array('attachments'), function(req,res){ // 6
  res.render('confirmation', { file: null, files:req.files} );
});

router.post('/uploadFilesWithOriginalFilename', uploadWithOriginalFilename.array('attachments'), function(req,res){ // 7
  res.render('confirmation', { file:null, files:req.files });
});

module.exports = router;

1. multer module을 불러옵니다. 참고로 제 강의에 사용된 모든 multer 관련 코드는 공식문서 https://github.com/expressjs/multer/blob/master/doc/README-ko.md 를 활용하여 작성하였습니다.

2. 업로드한 파일의 이름을 유지하기 위해서는 multer에 storage 세팅을 해줘야 하는데 이때 사용될 변수입니다. 업로드된 파일명과 서버의 파일명이 완전히 동일하게 되면 중복된 파일 업로드에서 문제가 생길 수 있으니 파일명 앞에 시간을 정수로 달아줬습니다. 즉 똑같은 파일명이 업로드되더라도 앞에 시간 정수가 있기 때문에 구별됩니다.

3-1. multer로 파일이 저장될 위치만을 설정(dest: 'uploadedFiles)하여 upload 미들웨어를 만들었습니다. 이렇게 만든 upload 미들웨어는 저장되는 파일의 이름을 무작위로 변경하게 됩니다.

3-2. 이번에는 2번에서 만든 storage를 넣어서 저장될 파일의 이름을 유지하는 미들웨어를 만들었습니다. 3-1의 uploaduploadWithOriginalFilename은 파일명 변경/유지 외에는 하는 일이 완전히 똑같습니다. 즉 실제 프로젝트에서 파일명을 임의로 바뀌게 하여 보안성을 높이고 싶다면 upload처럼 설정하고, 파일명을 유지하여 관리하기 쉽게 하고 싶다면 uploadWithOriginalFilename처럼 설정하시면 됩니다. 이 예제는 강의용으로 둘 다 사용해 봅시다.

4. 기본 설정으로 하나의 파일업로드를 처리하는 route입니다. 파일명이 바뀌도록 upload를 사용하였고 하나의 파일을 처리하기 위해 upload.single()을 사용했습니다. upload.single()에는 html form에서 사용된 파일 input 필드의 이름(name)이 들어갑니다.

5. 변경된 storage 설정으로 하나의 파일업로드를 처리하는 route입니다. 파일명이 바뀌지 않도록 uploadWithOriginalFilename을 사용하였고 하나의 파일을 처리하기 위해 uploadWithOriginalFilename.single()을 사용했습니다.

6. 기본 설정으로 여러개의 파일업로드를 처리하는 route입니다. 파일명이 바뀌도록 upload를 사용하였고 여러개의 파일을 처리하기 위해 upload.array()를 사용했습니다. upload.array()에도 마찬가지로 html form에서 사용된 파일 input 필드의 이름(name)이 들어갑니다.

7. 변경된 storage 설정으로 여러개의 파일업로드를 처리하는 route입니다. 파일명이 바뀌지 않도록 uploadWithOriginalFilename을 사용하였고 여러개의 파일을 처리하기 위해 uploadWithOriginalFilename.array()를 사용했습니다.

즉, 파일명에 따라 upload/uploadWithOriginalFilename, 파일이 하나인지 여러개인지에 따라 .single()/.array()를 조합하여 총 4개의 route을 만들었습니다.

.single()을 사용해서 하나의 파일을 올린 경우 req.file에 업로드된 하나의 파일의 정보가 저장되고, .array()를 사용해서 여러개의 파일을 올린 경우 req.files에 업로드된 파일들의 정보가 배열로 저장됩니다.

생성된 req.file, req.files를 {file:... files:...}에 넣어서 confirmation 페이지로 보냅니다. 이 오브젝트의 구조는 다음과 같습니다

  • fieldname: 파일이 form의 어느 field에서 왔는지를 알려줍니다.
  • originalname: client에서 업로드된 파일이름입니다.
  • encoding: 파일의 인코딩입니다.
  • mimetype: 파일 타입입니다.
  • destination: 파일의 저장된 위치(폴더)입니다.
  • filename: 실제 저장된 파일의 이름입니다.
  • path: 실제 파일의 위치입니다.
  • size: 파일의 크기(바이트)입니다.

실전에서 이 정보들을 이용하여 DB에 파일 위치를 저장하거나 할 수 있는 중요한 정보입니다.

다음으로 template 파일들을 살펴봅시다.

<!-- views/upload.ejs -->

<h1><i>Express</i> and <i>Multer</i> File Upload Example</h1>
<hr>

<h3>1. Single File Upload</h3>
<form action="/uploadFile" enctype="multipart/form-data" method="post">
  <input type="file" name="attachment">
  <button type="submit" class="btn btn-primary">Upload</button>
</form>
<hr>

<h3>2. Single File Upload (Keep Original Filename)</h3>
<form action="/uploadFileWithOriginalFilename" enctype="multipart/form-data" method="post">
  <input type="file" name="attachment">
  <button type="submit" class="btn btn-primary">Upload</button>
</form>
<hr>

<h3>3. Multiple File Upload</h3>
<form action="/uploadFiles" enctype="multipart/form-data" method="post">
  <input type="file" name="attachments" multiple>
  <button type="submit" class="btn btn-primary">Upload</button>
</form>
<hr>

<h3>4. Multiple File Upload (Keep Original Filename)</h3>
<form action="/uploadFilesWithOriginalFilename" enctype="multipart/form-data" method="post">
  <input type="file" name="attachments" multiple>
  <button type="submit" class="btn btn-primary">Upload</button>
</form>

form에 파일을 업로드하려면 enctype="multipart/form-data" 항목(attribute)을 추가해야 합니다. 그리고 input의 type은 file로 하고, name을 적어줍니다. 이 name이 upload 미들웨어에 사용됩니다. 파일을 여러개 선택할 수 있게 하려면 input에 multiple 항목(attribute)을 추가해줍니다.

<!-- views/confirmation.ejs -->

<h1>Success!</h1>

<% if(file){ %>
<pre><%=JSON.stringify(file, null, 2)%></pre>
<% }%>

<% if(files){ %>
<pre><%=JSON.stringify(files, null, 2)%></pre>
<% }%>

<a href='/'>Back</a>

마지막으로 파일업로드가 된 후 이동할 confirmation 페이지에는 file이나 files의 정보를 표시합니다.

실행결과

nodemon을 사용하여 서버를 실행합니다.

1번 form을 이용하여 파일을 업로드해 봅시다.

req.file이 하나의 파일 정보를 표시하고 있으며, 파일명이 바뀌었습니다.

다음으로 4번 form을 이용하여 2개의 파일을 업로드해 봅시다.


req.files가 배열이고 두개의 파일 정보를 표시하고 있으며, 파일명이 유지되었습니다..

마치며..

공식문서 https://github.com/expressjs/multer/blob/master/doc/README-ko.md를 반드시 읽어보시기 바랍니다. 흔하지 않게 한글로 된 문서를 제공합니다! 공식문서를 살펴보시면 위 강의에서는 설명되지 않은 업로드파일의 용량 제한 등 더욱 자세한 설정들을 하는 방법을 제공합니다.

이 강의에서는 어떻게 파일을 올리는지만 설명하였고, 서버에 업로드된 파일을 활용하려면 fs 모듈에 대해 더 알아야합니다. 역시 공식문서 https://nodejs.org/api/fs.html 를 참고할 수 있습니다. 또한 게시판 만들기(고급)/게시판 - 파일첨부 기능 만들기도 활용해 보시기 바랍니다.

댓글

현승환 2021.03.09
공식문서는 설명이 별로 없어서 좀 힘들었는데 덕분에 많은거 배웠습니다 정말 감사합니다 깃 주소는 지금 만료가 됬는지 404 에러가 뜨네요
I
Ian H 2021.03.09
@현승환,
앗, 링크가 깨져있었네요. 복구하였습니다. 알려주셔서 감사합니다!
무도인 2021.08.21
안녕하세요. 폴더구조에 UploadFiles 폴더가 있어야지 않나용? 
I
Ian H 2021.08.21
@무도인,
UploadFiles 폴더 따로 안만들어도 괜찮습니다. 코드가 실행되면 자동으로 만들어져요^^
뾰로통 2021.09.27
안녕하세요. 좋은 글 감사합니다. 도움이 많이 되었어요! 그런데 로컬 말고 다른 서버에 파일을 업로드 하고 싶은데 이 경우에는 어떤 부분들을 수정해야하나요? destination에 uploadedFiles/ 부분을 바꾸면 되나요..?
I
Ian H 2021.09.27
@뾰로통,
안녕하세요! 로컬 컴퓨터에 연결된 서버를 말씀하시는 거죠? 해당 부분을 수정하면 될 거예요^^
김세현 2021.12.30
안녕하세요! 좋은 글 감사합니다. 개발에 도움이 많이 되었어요. 한 가지 질문을 더 여쭙고싶은데 이미지를 업로드하는데 500~800kb가 넘어가면 서버가 멈추더라구요 에러가 나는지도 모르겠습니다. destination, filename에 넣는 함수는 실행이 되는데 그이후로 router.post에 넣은 콜백함수가 실행이 안되고 멈춥니다 혹시 왜그런지 아실까요..?
I
Ian H 2021.12.30
@김세현,
안녕하세요!
https://www.a-mean-blog.com/ko/blog/단편강좌/_/node-js-디버깅-방법 강의 참고하셔서 정확히 코드 어디서 문제가 발생하는지 한번 확인해 보세요.
정 해결이 어려우시면 github에 코드를 올려주시면 제가 한번 확인해 보겠습니다^^
김세현 2021.12.31
@Ian H,
답변 감사합니다! 주신 링크 읽어봤는데 저희 같은경우에는 서버가 폐쇄망 서버에다가 접속할수있는 노트북도 인터넷을 사용하지 않고있어서 링크의 디버그 방법으로는 디버깅이 힘들것같습니다. 또 저희 같은 경우에 폐쇄망 서버 외에 네이버클라우드서버같은 다른 서버를 이용하면 제가 말씀드린 문제가 생기지않는데 혹시 이부분에 관해서 아시는게 있으실까요?
I
Ian H 2021.12.31
@김세현,
코드문제가 아니라 네트워크나 서버 문제인듯한데요, 그쪽은 제가 잘 모릅니다. 죄송합니다!
조아농 2022.01.18
현재 코드에서업로드한 파일을 다시 다운받을 수 없을까요?
I
Ian H 2022.01.18
@조아농,
위코드로 업로드된 파일을 다운받으려면,
1. 업로드된 파일의 정보를 db에 저장하고 2. 해당 정보를 보여줄 수 있는 페이지를 만들고 3. 파일을 가져오는 route 을 만들어서 #2의 페이지에 표시하여야 합니다.
본문에 링크된 관련 강의를 참고해 주세요^^
조아농 2022.01.19
const key = 12345; router.get('/', function(req,res){   res.render('upload'); }); router.post('/uploadFileWithOriginalFilename', (req,res)=> {   var name = req.body.keyValue1;   if(key == name){     router.post('/uploadFileWithOriginalFilename', uploadWithOriginalFilename.single('attachment'), function(req,res){         res.render('confirmation', { file:req.file, files:null });     });   } }) 기존예제에서 변형해 input 값을 가져와 key값과 일치하면 업로드 되게 만들어보려고 하는데 req.body.keyValue1 를 잡아오지 못합니다 방법이 있을까요?
I
Ian H 2022.01.19
@조아농,
우선 form의 값을 가져오려면 bodyParser가 필요한데 사용하셨는지요? 아니라면 https://www.a-mean-blog.com/ko/blog/Node-JS-첫걸음/주소록-만들기/주소록-Index-New-Create 강의에서 bodyParser에 관련된 내용을 찾아서 사용하시기 바랍니다.
또한 https://www.a-mean-blog.com/ko/blog/단편강좌/_/node-js-디버깅-방법 을 익히셔서 코드의 흐름을 직접 확인할 수 있도록 합시다.
bodyParser가 정상적으로 사용되었고, 디버깅 방법으로 코드의 흐름을 따라가다보면 if(key == name)의 결과는 참이지만 업로드는 되지 않는 것을 확인할 수 있을 것입니다. 이는 router.post가 '이벤트 리스너'이기 때문입니다. (일반적인 함수처럼 그냥 호출하여 사용하는 것이 아닙니다.) 그러므로 router.post는 항상 바깥에 존재해야 합니다.
router.post('/uploadFileWithOriginalFilename', uploadWithOriginalFilename.single('attachment'), function(req,res){   var name = req.body.keyValue1;   if(key == name){     // key == name인 경우 할 일   }   else {    // key == name이 아닌 경우 할 일   } });
댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP