[Node.js] express + multer로 이미지 업로드 구현하기

김씨·2023년 1월 23일
2

Node.js

목록 보기
17/17
post-thumbnail

위키피디아나 쿠팡 등의 서비스를 이용하면서 우리가 직접 이미지를 업로드 할 일은 별로 없겠지만
위키피디아에서는 수많은 지식인들이, 쿠팡에서는 수많은 판매자들이,
매일같이 이미지를 추가/삭제하는 기능을 이용하고 있을 것이다.

그리고 그렇게 추가된 이미지는 우리 같은 소비자에게 제공된다.

인스타그램은 누구라도 스마트폰 카메라로 찍은 이미지를 손쉽게 업로드 할 수 있는 기능을 제공하기 때문에
이미지 업로드라고 하면, 대부분의 사람들에게 인스타그램이 좀 더 친숙하게 느껴질 것이다.

이렇듯 이미지 업로드 기능은 영역을 가리지 않고, 다양한 서비스에서 사용되고 있다.
그렇기에 매우 중요하며 반드시 알아둬야 할 기능이다.

express를 통해 간단한 이미지 업로드 기능과, 업로드 된 이미지들을 브라우저를 통해 볼 수 있도록 제공하는 부분까지 구현해보자.

npm init -y
npm i express multer

img_server라는 새로운 디렉토리를 만들고 npm init -y로 패키지 초기화를 진행한다.
expressmulter 모듈을 설치한다.
multer는 이미지 업로드 구현을 위해 필요한 모듈이다.
(사실 이미지 뿐만 아니라 텍스트, 동영상 등 모든 종류의 파일을 업로드 할 수 있다.)

// main.js
const express = require('express');

const app = express();

const path = require('path');
const publicPath = path.join(__dirname, 'public');
app.use(express.static(publicPath));

app.listen(3000, () => {
    console.log('server is running at 3000');
});

직전 포스트에서 작성한 static 파일을 제공하는 코드에서부터 시작해보자.
이미지를 업로드 한 뒤, public 폴더에 저장하여 사용자가 업로드 한 이미지를 볼 수 있도록 제공할 것이기 때문에 이 부분이 필요할 것이다.

이미 자세히 알아본 부분이기에 추가적인 설명 없이 진행하겠다.

이미지 업로드도 특별할 것은 없다.
x-www-form-urlencoded, json과 같이 이미지를 문자열 쪼가리로 변환하여 스트림으로 서버에 전송해 주는 것이 전부다.

명칭이 조금 다른데, multipart/form-data라는 형식으로 request body에 이미지 데이터가 첨부된다.

이미지의 용량이 다른 데이터에 비해 상당히 클 수 있기 때문에 이름 그대로 multipart, 여러 부분으로 쪼개서 전송한다는 뜻이다.

MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=frontier

This is a message with multiple parts in MIME format.
--frontier
Content-Type: text/plain

This is the body of the message.
--frontier
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64

PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg
Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==
--frontier--

대략 위와 같은 모습이다.
복잡해 보이지만, 파싱은 multer가 알아서 해주기 때문에 걱정할 필요는 없다.

우선 public 폴더에 index.html이라는 파일을 하나 생성하자.

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Image Server</title>
</head>
<body>
    <form action="/upload" method="POST" enctype="multipart/form-data">
        <input type="file" name="myFile" />
        <button type="submit">Upload</button>
    </form>
</body>
</html>

조금 난해해 보일 수도 있지만 회원가입을 구현할 때 만든 form과 거의 비슷한 형태다.
enctype="multipart/form-data"이 추가된 것과 inputfile type이라는 차이점이 있다.

x-www-form-urlencoded 때와 마찬가지로 <form>, <input />을 통해 action이 가리키는 URL로 body에 데이터를 첨부하여 POST로 요청을 보낸다.

x-www-form-urlencodeda=1&b=2&c=3과 같이 QueryString 형태로 데이터를 보내줬었지만 multipart는 어떤 형태를 띄고 있는지 위에서 봤다.

QueryString은 비교적 간단한 데이터를 body에 첨부해 보낼 때 사용하지만, 파일의 경우 이미지나 동영상 등의 매우 큰 용량을 가진 데이터들이 보내질 수 있기 때문에 포맷 자체가 다른 것이다.

이제 GET /로 접속하면 public/index.html이 렌더링 되어 이미지를 업로드 할 수 있는 화면이 나타날 것이다.

main.js 파일에 app.use(express.static(publicPath)); 미들웨어를 등록한 덕분에 public 폴더 내에 존재하는 파일을 Serving 할 수 있는 상태다.

그런데 index.html은 루트 페이지를 가리키는 특별한 파일명이기 때문에 GET /index.html이 아닌 GET /으로 접속해도 index.html 파일의 내용을 반환해준다.

그래서 따로 app.get('/') 라우터를 만들지 않아도 GET /에 접속 시, 위와 같이 index.html의 내용이 그대로 렌더링 된 것을 알 수 있다.

app.use(express.static(publicPath));

app.get("/", (req, res) => {
  res.end("HOME");
});

그럼 위와 같이 GET /에 대한 라우터가 존재하면 둘 중 어떤 것이 렌더링 될까.

항상 미들웨어는 순서가 중요하다고 했다.
app.use(express.static(publicPath));가 위에 있기 때문에, GET /에 요청을 보내면 index.html을 렌더링 한 뒤 종료된다.
express.static 미들웨어에서 내부적으로 res.sendFile(); 등의 함수가 실행될 것이기 때문에 res.end();로 먼저 응답을 완료하기 때문이다.

app.get("/", (req, res) => {
  res.end("HOME");
});

app.use(express.static(publicPath));

이 둘의 위치를 바꾸면 당연히 HOME이 응답될 것이다.

res.end();로 응답을 완료해버리기 때문에 다음 미들웨어인 express.static()가 실행되지 않기 때문이다.

아직 이미지 업로드 기능을 구현하지 않았으므로 일단 main.js에서 코드를 추가로 작성해보자.
아래 코드를 보기 전에 files라는 폴더도 하나 만들자.

// main.js
const multer = require('multer');

const upload = multer({
	dest: 'files/'
});

const uploadMiddleware = upload.single('myFile');

app.use(uploadMiddleware);

app.post('/upload', (req, res) => {
    console.log(req.file);
    res.status(200).send('uploaded');
});

multer 함수에 dest라는 속성을 가진 JS 객체를 인자로 전달한다.
destdestination의 약자로 업로드 될 목적지를 뜻한다.

multer 함수로 upload라는 객체를 반환하는데, 또 다시 upload.single() 메소드를 호출하면 업로드 미들웨어를 생성할 수 있다.

이 미들웨어는 multipart/form-data 형식의 body를 파싱해서 파일로 다시 변환하고 dest에 등록된 경로에 업로드한다.

upload.single() 이외에도 다른 메소드들이 있는데, 파일을 한개만 업로드 할 때는 single을 사용한다.
그 옆에 'myFile'을 인자로 준 이유는 아까 index.htmlinput 태그에서 name 속성이 myFile 이었기 때문이다.

QueryString가 a=1&b=2와 같은 KEY=VALUE 형태였듯, formData의 KEY가 inputname 속성이라 생각해도 된다.

이제 GET /에 접속해서 파일을 업로드해보자.

Choose File을 눌러서 원하는 사진을 아무거나 선택한 뒤, Upload 버튼을 누르자.

// req.file
{
  fieldname: 'myFile',
  originalname: '_.png',
  encoding: '7bit',
  mimetype: 'image/png',
  destination: 'files/',
  filename: 'b7cc5ab2b1b50e5b033839a859aa18e2',
  path: 'files/b7cc5ab2b1b50e5b033839a859aa18e2',
  size: 74107
}

uploadMiddleware에 의해 body가 파싱되어 req.file에 저장되며, 그걸 출력하면 위와 같은 형태를 하고 있다.

fieldname이 아까 input 태그의 name 값과 동일한 것을 볼 수 있다.

originalname은 업로드 한 파일의 원래 이름이다.

확장자가 png인 이미지 파일임을 mimetype 속성을 통해 알 수 있다.

destination은 우리가 multer() 함수의 dest 옵션으로 준 그대로 출력된다.

filenamemulter에 의해 임의로 만들어진 파일명이다.
서버에 업로드 될 때 저 이름으로 저장된다.

pathdest + filename이며, size는 파일 용량이다.

files 폴더 내부를 살펴보면 b7cc5ab2b1b50e5b033839a859aa18e2라는 파일이 저장된 것을 볼 수 있다.

uploadMiddleware가 파싱 및 파일 업로드를 모두 자동으로 처리한 것이다.

업로드한 사진 원본 이름은 _.png였다.
확장자도 사라지고 이름도 원본과 전혀 다른 b7cc5ab2b1b50e5b033839a859aa18e2으로 저장되어 알아보기 어렵다.

여러 명이 사용하는 서비스의 경우 _.png라는 제목을 가진 사진을 수십 명이 업로드 한다면, 이름이 같기 때문에 이전 사진이 사라지고 새로운 사진으로 덮어 씌워질 것이다.

이를 방지하기 위해 multer가 임의의 값을 이름으로 지정해주는 듯 하다.

그런데 이렇게 사용하면 확장자가 날아가버리는 문제가 있다.

당연히 이름과 확장자를 직접 지정해줄 수 있는 방법이 존재한다.

그런데 multer() 함수의 옵션을 조금 변경할 필요가 있다.

dest를 지우고 storage라는 새로운 옵션을 주면 된다.

// main.js
// ...
const upload = multer({
    storage: multer.diskStorage({
      	filename(req, file, done) {
          	console.log(file);
			done(null, file.originalname);
        },
		destination(req, file, done) {
      		console.log(file);
		    done(null, path.join(__dirname, "public"));
	    },
    }),
});

const uploadMiddleware = upload.single('myFile');

app.use(uploadMiddleware);

app.post('/upload', (req, res) => {
    console.log(req.file);
    res.status(200).send('uploaded');
});

dest를 없애고 storage라는 속성을 추가한 뒤, multer.diskStorage()를 값으로 주었다.

storage는 말 그대로 업로드 할 이미지 파일을 어디에 저장할 것인가에 대한 옵션이다.

아까 dest를 지정했을 때보다 좀 더 세부적인 내용을 지정할 수 있다.

storage 내부에 filename, destination 이라는 함수를 넣어줌으로 좀 더 정교한 설정이 가능해진 것이다.

이 함수들은 req, file, done을 인자로 받는데, reqexpress의 Request 객체고, file은 우리가 업로드 한 파일에 대한 정보가 들어있다.
done()은 함수인데, 미들웨어에서 사용하는 next()와 비슷하게 다음 미들웨어로 작업을 넘기도록 하며 1번째 인자로는 오류를, 2번째 인자로는 파일 이름을 지정한다.

done의 첫 번째 매개변수로 오류를 전달하면 이미지의 이름이 중복인지 체크하여 중복이면 오류를 발생시키는 등의 처리가 가능할 것이다.

dest 속성만 사용할 때는 multer가 지어준 임의의 이름을 그대로 사용해야 했지만, filename 함수 안에서 2번째 인자 file로 파일에 대한 정보를 받아서 업로드 하기 전의 파일명인 _.png를 그대로 사용할 수도 있다.

destination은 아까 본 dest와 비슷하게 업로드 시 저장될 폴더를 지정할 수 있다.

방금 upload 라우터에서 출력한 req.filefilename 메소드의 2번째 인자인 file은 비슷하지만 file의 정보가 조금 더 적다.

// file object (in filename fn)
{
  fieldname: 'myFile',
  originalname: '_.png',
  encoding: '7bit',
  mimetype: 'image/png'
}
// file object (in destination fn)
{
  fieldname: 'myFile',
  originalname: '_.png',
  encoding: '7bit',
  mimetype: 'image/png'
}

filename 메소드에서 file을 출력하면 위와 같이 나오는 것을 볼 수 있다.
req.file은 이미 미들웨어가 모두 처리되어 서버에 업로드 된 상태였기 때문에 서버에 업로드 된 후의 destination, filename, path, size 등의 추가적인 정보를 가지고 있다.

물론 file도 파일의 원본명을 가지고 있기 때문에 file.originalnamedone()의 2번째 인자로 넘겨주면 이름과 확장자를 그대로 유지하면서 파일 업로드가 가능하다.

참고로 filename, destination이 받는 인자는 동일하며 file도 같은 데이터다.
실행 순서는 destination -> filename이다.

// main.js
// ...
const uploadMiddleware = upload.single('myFile');

// app.use(uploadMiddleware);

app.post('/upload', uploadMiddleware, (req, res) => {
    console.log(req.file);
    res.status(200).send('uploaded');
});

app.use()가 아닌 app.post('/upload') 라우터의 2번째 인자로 uploadMiddleware를 옮겼다.

이렇게 변경하면 POST /upload로 요청을 보낼 때만 uploadMiddleware가 실행되어 업로드가 진행된다.

이미지 업로드가 발생하는 라우터는 매우 한정적이기 때문에 특정 라우터를 지정해서 미들웨어를 등록해주는 편이 합리적이다.

// main.js
// ...
const upload = multer({
    storage: multer.diskStorage({
      	filename(req, file, done) {
          	console.log(file);
			done(null, file.originalname);
        },
		destination(req, file, done) {
      		console.log(file);
		    done(null, path.join(__dirname, "public"));
	    },
    }),
  	// 추가된 속성
	limits: { fileSize: 0 },
});
// ...

limits 라는 옵션도 존재한다.
그 중에서도 limits.fileSize 속성으로 업로드 할 파일의 최대 용량을 제한할 수 있다.
0으로 지정하면 당연히 어떤 파일도 업로드 할 수 없을 것이다.

코드를 재실행하고 한 번 업로드를 시도해보자.

MulterError: File too large

파일이 너무 크다는 오류 메세지가 발생하는 것을 알 수 있다.

// ...
limits: { fileSize: 1024 * 1024 },
// ...

limits.fileSize을 넉넉하게 1024 * 1024로 변경해주자.

아까 dest 옵션을 지정했을 때 임의의 값이 파일명으로 자동 지정되던 것을 기억할 것이다.

사용자가 업로드한 이름 그대로 저장하게 되면, 여러 사용자가 이름이 중복되는 파일을 업로드 했을 경우, 덮어쓰기가 되어 기존 파일이 유실될 수 있다는 이유로 추측된다고 했다.

그래서 보통 중복되기 어려운 임의의 값 등을 파일명에 추가하여 저장하고, 데이터베이스에 그 경로를 저장하는 방식이 사용된다.

npm i uuid4

중복되기 매우 어려운 고유의 값을 만들어주는 uuid4 모듈을 설치하자.

const uuid4 = require('uuid4');
// ...
        filename(req, file, done) {
            const randomID = uuid4();
            const ext = path.extname(file.originalname);
            const filename = randomID + ext;
            done(null, filename);
        },

uuid4라는 모듈을 설치해서 단순하게 랜덤 uuid를 발급받아 이를 파일명으로 저장해보겠다.

uuid4() 함수는 49936f88-301b-4032-9d62-1280a4e4995a와 같은 형태의 랜덤값을 만들어준다.

보다시피 중복 확률이 상상 이상으로 매우 낮기 때문에 이렇게 사용해도 보통은 큰 문제가 없을 것 같다고 보여진다.

file.originalname은 업로드한 파일의 원래 제목이다.
나의 경우에는 _.png를 업로드했기 때문에 _.png가 되겠다.
path.extname()은 파일명을 인자로 주면 확장자를 추출하는 함수다.
.png가 될 것이다.

그래서 filename49936f88-301b-4032-9d62-1280a4e4995a.png를 연결한 값이 된다.

실제로 그렇게 저장된 것을 볼 수 있다.

그리고 아까 언급했듯, multer를 통해 이미지가 아닌 .txt 등의 텍스트 파일은 물론 동영상도 업로드가 가능하다는 사실을 잊지 말자.

위 사진을 통해 다양한 종류의 파일들이 정상적으로 업로드 된 것을 볼 수 있다.


업로드 중 오류가 발생했을 경우

현재까지 작성한 코드는 다음과 같다.

const express = require("express");
const app = express();
const path = require("path");
const publicPath = path.join(__dirname, "public");
const multer = require("multer");
const uuid4 = require("uuid4");

app.use(express.static(publicPath));

const upload = multer({
  storage: multer.diskStorage({
    filename(req, file, done) {
      const randomID = uuid4();
      const ext = path.extname(file.originalname);
      const filename = randomID + ext;
      done(null, filename);
    },
    destination(req, file, done) {
      done(null, path.join(__dirname, "files"));
    },
  }),
  limits: { fileSize: 1024 * 1024 },
});

const uploadMiddleware = upload.single("myFile");

app.post("/upload", uploadMiddleware, (req, res) => {
  console.log(req.file);
  res.status(200).send("uploaded");
});

app.listen(10000, () => {
  console.log("server is running at 3000");
});

오류 처리 미들웨어가 없어도 업로드 할 파일의 크기가 limits.fileSize보다 크면 오류 메세지가 출력되었다.

// ...
// 업로드 용량 제한 0으로 재설정
limits: { fileSize: 0 },
// ...
// 오류 처리 미들웨어
app.use((err, req, res, next) => {
  console.log("error middleware");
  console.log(err.toString());
  res.send(err.toString());
});

limits.fileSize를 다시 0으로 변경한 뒤, 오류 처리 미들웨어를 추가하고 다시 업로드를 시도해보자

오류 메세지가 잘 응답된다.

error middleware
MulterError: File too large

또한 터미널에서도 오류 메세지와 함께 오류 처리 미들웨어도 실행된 것을 볼 수 있다.

limits.fileSize에 적힌 용량을 초과하는 이미지가 들어오면 자동으로 next(MulterError)를 호출하여 오류 처리 미들웨어로 넘어간다고 생각하면 될 것 같다.

그렇다면 filename, destination 함수 내의 done()의 첫번째 인자로 오류를 전달하면 어떻게 될까.

// ...
        filename(req, file, done) {
            if (file.originalname === '_.png') {
                done('cannot use this name');
            }
            
            const randomID = uuid4();
            const ext = path.extname(file.originalname);
            const filename = randomID + ext;
            done(null, filename);
        },
// ...

파일명이 _.pngdone()의 첫번째 인자로 오류를 전달하도록 했다.
이제 _.png를 업로드해보자.

error middleware
cannot use this name

cannot use this name라는 단순 문자열을 전달했는데, 문자열은 .toString() 메소드를 호출해도 문자열을 반환하기에, 전달한 문자열 그대로 출력되었다.

done()의 첫번째 인자에 null이 아닌 요소를 전달해도 오류 처리 미들웨어로 그대로 넘어가는 것을 알 수 있다.

그리고 limits보다 먼저 실행된다는 것도.

limits.fileSize의 용량을 0으로 그대로 둔 상태인데, 이미 filename에서 오류가 발생하여 limits의 용량 제한 오류는 터미널에 출력되지도 않았다.

이로 말미암아 오류 처리 순서도 알 수 있다.


여러 개의 이미지 업로드

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Image Server</title>
</head>
<body>
    <form action="/upload" method="POST" enctype="multipart/form-data">
        <input type="file" name="myFiles" multiple />
        <button type="submit">Upload</button>
    </form>
</body>
</html>

여러 개의 이미지를 업로드하기 위해서 public/index.html<form> 태그에 multiple이라는 속성을 삽입한다.

여러 개의 파일임을 명시하기 위해 input 태그의 namemyFiles로 바꿔주자.

// main.js
// ...
const uploadMiddleware = upload.array("myFiles");

app.post("/upload", uploadMiddleware, (req, res) => {
  console.log(req.files);
  res.status(200).send("uploaded");
});
// ...

main.js에서 업로드 미들웨어를 생성할 때 upload.single()이었던 부분을 upload.array()로 변경해준다.

파일 하나가 아닌 배열 형태로 여러 개를 파싱해서 받아올 것이기 때문에 multer에서 제공하는 이 함수를 사용해야 한다.

물론 인자도 myFiles로 변경한다.

Request 객체 내의 속성도 req.file이 아닌 req.files로 들어오게 된다.

storage 속성 내의 destination, filename은 수정할 필요 없다.
파일 하나당 한 번씩 실행하면 되기 때문에 이 메소드 내부에서 file을 배열로 다룰 필요는 없기 때문이다.

이제 GET /로 접속해서 Choose Files를 누르고 여러 개의 파일을 선택해보자.
맥이면 CMD + 클릭, 윈도우면 Control + 클릭으로 파일을 여러 개 선택할 수 있을 것이다.

Upload 버튼을 누르면 정상적으로 업로드가 완료되었다고 나오며 req.files가 아래와 같이 출력되었을 것이다.

// req.files 출력 결과
[
  {
    fieldname: 'myFiles',
    originalname: '_.png',
    encoding: '7bit',
    mimetype: 'image/png',
    destination: '/Users/x/Documents/dev/node_playground/test/files',
    filename: 'fea8f281-f412-43b3-b349-b865d10ca634.png',
    path: '/Users/x/Documents/dev/node_playground/test/files/fea8f281-f412-43b3-b349-b865d10ca634.png',
    size: 74107
  },
  {
    fieldname: 'myFiles',
    originalname: 'Hive+Systems+Password+Table.png',
    encoding: '7bit',
    mimetype: 'image/png',
    destination: '/Users/x/Documents/dev/node_playground/test/files',
    filename: '6c6e27e5-67d3-4a28-8b12-df8e0ec537bd.png',
    path: '/Users/x/Documents/dev/node_playground/test/files/6c6e27e5-67d3-4a28-8b12-df8e0ec537bd.png',
    size: 336747
  },
  {
    fieldname: 'myFiles',
    originalname: 'jdk.png',
    encoding: '7bit',
    mimetype: 'image/png',
    destination: '/Users/x/Documents/dev/node_playground/test/files',
    filename: '8c455ded-42e7-4b6d-b695-3cc7a5251224.png',
    path: '/Users/x/Documents/dev/node_playground/test/files/8c455ded-42e7-4b6d-b695-3cc7a5251224.png',
    size: 56059
  },
  {
    fieldname: 'myFiles',
    originalname: 'image-asset.png',
    encoding: '7bit',
    mimetype: 'image/png',
    destination: '/Users/x/Documents/dev/node_playground/test/files',
    filename: 'a6ec7761-0268-4421-859d-e4e3b072455c.png',
    path: '/Users/x/Documents/dev/node_playground/test/files/a6ec7761-0268-4421-859d-e4e3b072455c.png',
    size: 276521
  },
  {
    fieldname: 'myFiles',
    originalname: 'ISO_C++_Logo.svg-1.png',
    encoding: '7bit',
    mimetype: 'image/png',
    destination: '/Users/x/Documents/dev/node_playground/test/files',
    filename: 'c6060bd4-fb69-4450-a3af-18c3b07b9258.png',
    path: '/Users/x/Documents/dev/node_playground/test/files/c6060bd4-fb69-4450-a3af-18c3b07b9258.png',
    size: 112631
  }
]

여러 파일 업로드도 거의 비슷하며 어려울 것이 없었다.

그런데 하나의 inputmultiple로 지정해서 여러 개의 파일을 보내는 것이 아니라,
여러 개의 input을 만들어서 사진을 여러 개 업로드 하는 경우에는 코드가 약간 달라진다.

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Image Server</title>
</head>
<body>
    <form action="/upload" method="POST" enctype="multipart/form-data">
        <input type="file" name="myFile1" />
        <input type="file" name="myFile2" />
        <input type="file" name="myFile3" />
        <button type="submit">Upload</button>
    </form>
</body>
</html>

위와 같은 경우를 말하는 것이다.

이 경우 Choose File이 3개 존재하게 될 것이다.

const uploadMiddleware = upload.fields([
  { name: "myFile1" },
  { name: "myFile2" },
  { name: "myFile3" },
]);

app.post("/upload", uploadMiddleware, (req, res) => {
  console.log(req.files);
  res.status(200).send("uploaded");
});

main.js에서의 처리도 약간 바뀐다.
upload.fileds()에 배열 형태로 각 파일의 이름을 속성으로 지정해주면 된다.

이번에도 여러 개의 파일을 받기 때문에 req.files에 파일들이 담겨있다.
req.files.myFile1과 같은 식으로 개별 파일을 가져올 수도 있다.

// req.files 출력 결과
[Object: null prototype] {
  myFile1: [
    {
      fieldname: 'myFile1',
      originalname: '_.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: 'db8bdf6a-6214-4eab-a1a5-dd71ae58fb2f.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/db8bdf6a-6214-4eab-a1a5-dd71ae58fb2f.png',
      size: 74107
    }
  ],
  myFile2: [
    {
      fieldname: 'myFile2',
      originalname: 'Hive+Systems+Password+Table.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: '7487a2b1-4cc6-4e4d-85a6-c920aa68f105.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/7487a2b1-4cc6-4e4d-85a6-c920aa68f105.png',
      size: 336747
    }
  ],
  myFile3: [
    {
      fieldname: 'myFile3',
      originalname: 'dart.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: '1c56b858-a383-4ae8-92bd-43a0d4eac4ae.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/1c56b858-a383-4ae8-92bd-43a0d4eac4ae.png',
      size: 25665
    }
  ]
}

재미있는 것은 myFile1, myFile2, myFile3 모두 하나의 파일만 가지고 있는데도 배열로 이루어졌다는 점이다.

<!-- public/index.html -->
<input type="file" name="myFile1" multiple />

만약 myFile1multiple로 지정한다면?

myFile1은 여러 개의 파일을 선택할 수 있다.
(multiple 속성이 없는 input은 브라우저에서 여러 파일 선택 자체가 안된다.)

// req.files 출력 결과
[Object: null prototype] {
  myFile1: [
    {
      fieldname: 'myFile1',
      originalname: 'ISO_C++_Logo.svg-1.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: '36c78374-777b-4b58-a08d-f3f62445df83.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/36c78374-777b-4b58-a08d-f3f62445df83.png',
      size: 112631
    },
    {
      fieldname: 'myFile1',
      originalname: 'jdk.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: 'd3d0fb2b-e23f-4036-8e98-fb3954e99895.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/d3d0fb2b-e23f-4036-8e98-fb3954e99895.png',
      size: 56059
    },
    {
      fieldname: 'myFile1',
      originalname: 'nodejs.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: '23041d3f-2865-41d8-9e1d-fd65b779bcc1.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/23041d3f-2865-41d8-9e1d-fd65b779bcc1.png',
      size: 48769
    }
  ],
  myFile2: [
    {
      fieldname: 'myFile2',
      originalname: '_.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: 'e532ee6c-4f32-461f-8931-b52d37f087f5.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/e532ee6c-4f32-461f-8931-b52d37f087f5.png',
      size: 74107
    }
  ],
  myFile3: [
    {
      fieldname: 'myFile3',
      originalname: 'a.txt',
      encoding: '7bit',
      mimetype: 'text/plain',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: '7535f6af-bf09-4428-a90c-f0fb530c7e52.txt',
      path: '/Users/x/Documents/dev/node_playground/test/files/7535f6af-bf09-4428-a90c-f0fb530c7e52.txt',
      size: 34
    }
  ]
}

이번에도 똑같이 upload.fileds()로 만든 미들웨어이기 때문에 req.files로 객체 형태의 데이터가 들어오는 것을 볼 수 있다.

모두 배열 형태이지만 첫 번째 myFile1 배열에만 여러 개의 파일이 들어가 있다.

여러 개의 파일을 다양한 형태로 업로드 시키는 방법에 대해서 알아보았다.

이제 쉽게 구현할 수 있을 것이다.


텍스트, 이미지 동시에 전달하기

multipart/form-data<input type="file" />만 전달하는 것이 아니라 일반적인 텍스트 인풋, <input type="text" /> 형태의 데이터도 같이 전달할 수 있다.

이 경우, 파일은 req.file에 들어가지만 나머지는 req.body로 파싱된다는 점을 미리 알아두자.

public/img.html 파일을 다음과 같이 수정하자.

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Image Server</title>
  	<!-- 직접 POST 요청을 보내기 위해 axios 불러오기 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.2.3/axios.min.js"></script>
</head>
<body>
    <form action="/upload" method="POST" enctype="multipart/form-data">
        Title: <input type="text" name="title1" />
        <br/>
        <input type="file" name="myFile1" />
        <button type="submit">Upload</button>
    </form>
</body>
<script>
	<!-- submit 버튼을 누를 때 실행 -->
    document.querySelector('form').addEventListener('submit', e => {
        e.preventDefault();
  
        const formData = new FormData();
  
		<!-- FormData 객체에 title1, myFile1 데이터 추가 -->
        formData.append('title1', e.target.title1.value);
        formData.append('myFile1', e.target.myFile1.files[0]);
  
		<!-- POST /upload로 formData 첨부하여 요청 -->
        axios.post('/upload', formData);
    });
</script>
</html>

filetext를 한 번에 전송하는 경우다.
formData에 직접 title1, myFile1 데이터를 추가하도록 변경하였다.

// main.js
// ...
const uploadMiddleware = upload.fields([
  { name: "myFile1" },
  { name: "title1" },
]);

app.post("/upload", uploadMiddleware, (req, res) => {
  console.log(req.body);
  console.log(req.files);
  res.status(200).send("uploaded");
});
// ...

upload.fields()로 각 inputname을 지정했다.
이번에는 req.body, req.files 모두 데이터가 담겨있기 때문에 둘 다 출력해보자.

// req.body 출력 결과
[Object: null prototype] { title1: '1' }

// req.files 출력 결과
[Object: null prototype] {
  myFile1: [
    {
      fieldname: 'myFile1',
      originalname: '_.png',
      encoding: '7bit',
      mimetype: 'image/png',
      destination: '/Users/x/Documents/dev/node_playground/test/files',
      filename: '73b45af8-249a-45eb-a923-993d529e4d22.png',
      path: '/Users/x/Documents/dev/node_playground/test/files/73b45af8-249a-45eb-a923-993d529e4d22.png',
      size: 74107
    }
  ]
}

위와 같이 각자 req.body, req.files에 데이터가 담겨서 전송된다는 것을 알 수 있다.

0개의 댓글