node.js의 장점중의 하나가 비동기 IO를 이용해서 IO 처리에 강하다는 것이다.

그중에서 특히나 활용할만한 것중의 하나는 파일 업로드 이다.

node.js에서 파일 업로드는 다양한 방식으로 구현이 가능하고 이를 지원하는 모듈도 많은데, 그중에서 busboy,multer, multiparty 등이 많이 사용된다.

여기서는 multiparty와 busboy에 대해서 소개하고자 한다.

 

Multiparty를 이용한 파일 업로드

multiparty모듈을 사용하기 위해서는 multiparty모듈에 대한 의존성을 추가해야 한다. 다음은 package.json 파일에 multiparty 모듈에 대한 의존성을 추가한 예이다.

{

  “name”: “FileUpload”,

  “version”: “0.0.0”,

  “private”: true,

  “scripts”: {

    “start”: “node ./bin/www”

  },

  “dependencies”: {

    “body-parser”: “~1.13.2”,

    “cookie-parser”: “~1.3.5”,

    “debug”: “~2.2.0”,

    “ejs”: “~2.3.3”,

    “express”: “~4.13.1”,

    “morgan”: “~1.6.1”,

    “serve-favicon”: “~2.3.0”,

    “multiparty”:”~4.1.2″

  }

}

 

Figure 15 multiparty 모듈 의존성이 추가된 package.json

다음, app.js에서, /upload URL에 대해서 라우터 설정을 한다.

var upload = require(‘./routes/upload.js’);

app.use(‘/upload’, upload);

Figure 16 app.js에 fileupload 라우터 추가

HTML 파일로 파일을 선택해서 보내는 파일을 작성한다.

다양한 사용 방법을 이해하기 위해서 두개의 파일과, 하나의 텍스트 폼필드를 보내는 HTML을 /public/multifileupload.html로 다음과 같이 작성한다.

<!DOCTYPE html>

<html>

<head>

<meta charset=“UTF-8”>

<title>Insert title here</title>

</head>

<body>

 <form action=“/upload” method=“post” enctype=“multipart/form-data”>

    name <input type=“text” name=“username”>

    <p>

      file :

    <input type=“file” name=“myfile1”>

    <input type=“file” name=“myfile2”>

    <input type=“submit” value=“Upload”>

 </form>

</body>

</html>

Figure 17 /public/multifileupload.html

폼내에서 username이라는 텍스트 필드를 보내고, myfile1과 myfile2라는 이름으로 두개의 파일을 전송한다.

이때 multipart/form-data 라는 형식으로 HTTP POST로 /upload 라는 URL로 데이타를 보낸다.

 

이제 본격적으로 파일을 업로드 하여 저장하는 코드를 작성해보자

var express = require(‘express’);

var router = express.Router();

var multiparty = require(‘multiparty’);

var fs = require(‘fs’);

 

/* GET home page. */

router.post(‘/’, function(req, res, next) {

 

      var form = new multiparty.Form();

     

      // get field name & value

      form.on(‘field’,function(name,value){

           console.log(‘normal field / name = ‘+name+’ , value = ‘+value);

      });

     

      // file upload handling

      form.on(‘part’,function(part){

           var filename;

           var size;

           if (part.filename) {

                 filename = part.filename;

                 size = part.byteCount;

           }else{

                 part.resume();

          

           }    

 

           console.log(“Write Streaming file :”+filename);

           var writeStream = fs.createWriteStream(‘/tmp/’+filename);

           writeStream.filename = filename;

           part.pipe(writeStream);

 

           part.on(‘data’,function(chunk){

                 console.log(filename+’ read ‘+chunk.length + ‘bytes’);

           });

          

           part.on(‘end’,function(){

                 console.log(filename+’ Part read complete’);

                 writeStream.end();

           });

      });

 

      // all uploads are completed

      form.on(‘close’,function(){

           res.status(200).send(‘Upload complete’);

      });

     

      // track progress

      form.on(‘progress’,function(byteRead,byteExpected){

           console.log(‘ Reading total  ‘+byteRead+’/’+byteExpected);

      });

     

      form.parse(req);

 

 

});

 

module.exports = router;

 

Figure 18 파일 업로드 핸들러

HTTP POST 핸들러(router.post)를 생성하고, 그 안에서 multipart 를 처리할 form 객체를 생성한다.

      var form = new multiparty.Form();

다음으로, 이 form에 이벤트 핸들러들을 정의한다.

  • field : 파일이 아닌 일반 필드가 들왔을때, 발생하는 이벤트
  • part : HTML 파트가 들어왔을 때 발생하는 이벤트로 파일 업로드에서는 파일 파트만을 잡아서 처리한다.
  • close : 폼 데이타가 모두 업로드 되었을때 발생하는 이벤트
  • progress : 폼 데이타를 업로드 하는 중간중간에 현재 진행 상태를 나타내가 위해서 발생되는 이벤트

 

각각의 이벤트 핸들러를 자세하게 살펴보도록 하자

      form.on(‘field’,function(name,value){

           console.log(‘normal field / name = ‘+name+’ , value = ‘+value);

      });

Figure 19 파일이 아닌 일반 폼필드를 처리하는 부분

field 이벤트 핸들러에서는 콜백 함수에 name과 value를 인자로 넘겨서 일반 필드의 필드명과 값을 전달한다. 

다음이 파일 업로딩 처리를 하는 part 이벤트 처리 부분이다

.

           if (part.filename) {

                 filename = part.filename;

                 size = part.byteCount;

           }else{

                 part.resume();

          

           }

Figure 20 파일인지 아닌지를 구분하여 처리하는 부분

part 이벤트 핸들러 내에서 만약에 파일일 경우에만 part.filename이 지정되서 온다. 이 part 이벤트는 파일이 아니라 다른 필드에서도 호출이 되는데, 일반 필드의 경우에는 처리를 하지 않기 위해서 part.filename이 정의 되어 있지 않은 경우에는 part.resume()을 호출하여, 처리를 넘어간다.

 

만약에 파일일 경우에는 업로드된 데이타를 직접 파일에 써야 한다.

var writeStream = fs.createWriteStream(‘/tmp/’+filename);

part.pipe(writeStream);

Figure 21 HTTP request 스트림과 파일 writeStream을 파이프로 연결

writeStream을 생성하고, 업로드되는 파일 업로드 스트림을 파일 writeStream에 pipe를 이용해서 연결한다.

Request part 에서 들어오는 파일 데이타 스트림을 바로 파일 writeStream에 연결해서 파일이 써지도록 한다.

 

           part.on(‘data’,function(chunk){

                 console.log(filename+’ read ‘+chunk.length + ‘bytes’);

           });

          

           part.on(‘end’,function(){

                 console.log(filename+’ Part read complete’);

                 writeStream.end();

           });

‘data’ 이벤트는 데이타가 읽어질때 발생을 하는데, 읽어진 데이타는 chunk 라는 변수로 넘어오게 된다. 여기서는 파일을 얼마나 읽었는지를 표시 하기 위해서, chunk.length를 출력하도록 하였다.

다음으로는 ‘end’라는 이벤트가 있는데, 이 에빈트는 해당 part를 다 읽었을때 발생을 한다. 파일을 다 읽었으면, 파일을 다 써진것으로 취급하고, writeStream.end()를 이용해서 파일 writeStream을 닫는다.

 

form.on(‘close’,function(){

           res.status(200).send(‘Upload complete’);

      });

     

Figure 22 form에서 close 이벤트 처리

form 을 다 읽으면 (HTTP 요청을 다 읽으면) ‘close’ 이벤트가 발생하는데, close 이벤트가 발생하면 HTTP 응답으로 200 성공과 함께 웹 화면에 성공 메세지를 출력한다.

form.on(‘progress’,function(byteRead,byteExpected){

           console.log(‘ Reading total  ‘+byteRead+’/’+byteExpected);

      });

 

Figure 23 form에서 progress 이벤트 처리

추가로 form에 ‘progress’ 라는 이벤트 처리를 하였는데, 이 이벤트는 폼을 읽는 중에 처리 상황을 알려주는 이벤트이다. 전체 폼 데이타의 양 (byteExpected)대비, 얼마나 데이타를 읽었는지 (byteRead)를 매번 리턴해줌으로써, 파일 업로드 프로그래스 창과 같은 것을 만들 수 있다.

 

코드를 실행해보자

Figure 24 multifileupload.html 실행 화면

 

multifileupload.html에서 파일 두개를 선택하고 name 부분에 사용자 이름을 넣고 upload 버튼을 누룬다.

Figure 25 파일 업로드 완료 화면

 

파일 업로드가 완료된 결과이다.

아래는 디버깅을 위해서 console에 출력된 내용이다.

 

 

normal field / name = username , value = Terry

Write Streaming file :msceo.jpeg

Write Streaming file :trek2.jpeg

msceo.jpeg read 94bytes

msceo.jpeg read 1bytes

msceo.jpeg read 12227bytes

trek2.jpeg read 35798bytes

msceo.jpeg Part read complete

 Reading total  86369/86369

trek2.jpeg read 1bytes

trek2.jpeg read 7456bytes

trek2.jpeg read 1bytes

trek2.jpeg read 9574bytes

trek2.jpeg read 20787bytes

trek2.jpeg Part read complete

_[0mPOST /upload _[32m200 _[0m5.645 ms – 15_[0m

 

Figure 26 console 로그 내용

 

지금까지 multiparty를 이용한 파일 업로드 방법에 대해서 알아보았다. 다소 코드가 길기는 한데, 자세한 설명 및 다양한 활용을 위해서 많은 코드를 넣었다. 특히 스트림을 처리하는 방식은 로컬 디스크에 파일을 저장 하는 데 뿐 아니라, 읽어드리는 파일을 바로 네트워크를 통해서 클라우드 스토리지인 아마존 S3나, 마이크로소프트 Azure의 BlobStorage등에 바로 연결이 가능하기 때문에, 이 코드를 사용했다.

 

 

Multer를 이용한 간단한 파일 업로드

간단하게 파일을 올리자면 multiparty를 사용하는 방법 이외에도 multer라는 모듈을 사용할 수 있다. Multer는 busboy라는 multipart 처리 모듈을 파일 업로드만 간편하게 사용할 수 있도록 한번 더 감싼 프레임웍이다.

 

먼저 package.json에 multer에 대한 의존성을 추가한다.

{

  “name”: “FileUpload”,

  “version”: “0.0.0”,

  “private”: true,

  “scripts”: {

    “start”: “node ./bin/www”

  },

  “dependencies”: {

    “body-parser”: “~1.13.2”,

    “cookie-parser”: “~1.3.5”,

    “debug”: “~2.2.0”,

    “ejs”: “~2.3.3”,

    “express”: “~4.13.1”,

    “morgan”: “~1.6.1”,

    “serve-favicon”: “~2.3.0”,

    “multiparty”:”~4.1.2″,

    “multer”:”~1.1.0″

  }

}

 

Figure 27 package.json에 multer 의존성 추가

다음으로 app.js에 다음 코드를 추가하면 파일을 업로드할 수 있다.

var multer = require(‘multer’);

app.post(‘/simpleupload’, multer({ dest: ‘/tmp/upload/’}).single(‘myfile’), function(req,res){

      console.log(req.body); //form fields

      console.log(req.file); //form files

      res.status(204).end();

});

Figure 28 app.js에 multer를 이용한 파일 업로드 로직 추가

HTTP POST /simpleupload에 대해서 multer를 이용해서 파일 업로드를 수행한다. multer({ dest: ‘/tmp/upload/’}) 는 파일 업로드 디렉토리를 /tmp/upload로 지정한다는 의미이고, .single(‘myfile’) 는 폼 필드중에서 파일은 ‘myfile’ 이라는 필드명으로 업로드 된다는 의미이다.

업로드가 끝나면 파일에 대한 정보는 req.file을 통해서 JSON 문자열로 받을 수 있다.파일 이외에 폼필드 정보는 req.body를 통해서 얻을 수 있다.

실제로 실행하여 console로 나온 결과를 보면 다음과 같다.

 

{}

{ fieldname: ‘myfile’,

  originalname: ‘20160224_104138.jpg’,

  encoding: ‘7bit’,

  mimetype: ‘image/jpeg’,

  destination: ‘/tmp/upload/’,

  filename: ‘8563e0bef6efcc4d709f2d1debb35777’,

  path: ‘/tmp/upload/8563e0bef6efcc4d709f2d1debb35777’,

  size: 1268337 }

Figure 29 console 로그로 나온 실행 결과

‘20160224_104138.jpg’ 파일을 업로드 했으며, 저장은 ‘/tmp/upload/8563e0bef6efcc4d709f2d1debb35777’ 으로 저장되었고, 파일의 사이즈는 1268337 이다

 

이 파일을 적절한 곳에 rename으로 옮겨서 사용하면 된다.

샘플 코드 주소 https://github.com/bwcho75/nodejs_tutorial/tree/master/FileUpload