위키피디아나 쿠팡 등의 서비스를 이용하면서 우리가 직접 이미지를 업로드 할 일은 별로 없겠지만
위키피디아에서는 수많은 지식인들이, 쿠팡에서는 수많은 판매자들이,
매일같이 이미지를 추가/삭제하는 기능을 이용하고 있을 것이다.
그리고 그렇게 추가된 이미지는 우리 같은 소비자에게 제공된다.
인스타그램은 누구라도 스마트폰 카메라로 찍은 이미지를 손쉽게 업로드 할 수 있는 기능을 제공하기 때문에
이미지 업로드라고 하면, 대부분의 사람들에게 인스타그램이 좀 더 친숙하게 느껴질 것이다.
이렇듯 이미지 업로드 기능은 영역을 가리지 않고, 다양한 서비스에서 사용되고 있다.
그렇기에 매우 중요하며 반드시 알아둬야 할 기능이다.
express를 통해 간단한 이미지 업로드 기능과, 업로드 된 이미지들을 브라우저를 통해 볼 수 있도록 제공하는 부분까지 구현해보자.
npm init -y
npm i express multer
img_server라는 새로운 디렉토리를 만들고 npm init -y
로 패키지 초기화를 진행한다.
express
와 multer
모듈을 설치한다.
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"
이 추가된 것과 input
이 file
type이라는 차이점이 있다.
x-www-form-urlencoded
때와 마찬가지로 <form>
, <input />
을 통해 action
이 가리키는 URL로 body에 데이터를 첨부하여 POST로 요청을 보낸다.
x-www-form-urlencoded
는 a=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 객체를 인자로 전달한다.
dest
는 destination
의 약자로 업로드 될 목적지를 뜻한다.
multer
함수로 upload
라는 객체를 반환하는데, 또 다시 upload.single()
메소드를 호출하면 업로드 미들웨어를 생성할 수 있다.
이 미들웨어는 multipart/form-data
형식의 body를 파싱해서 파일로 다시 변환하고 dest
에 등록된 경로에 업로드한다.
upload.single()
이외에도 다른 메소드들이 있는데, 파일을 한개만 업로드 할 때는 single
을 사용한다.
그 옆에 'myFile'
을 인자로 준 이유는 아까 index.html
의 input
태그에서 name
속성이 myFile
이었기 때문이다.
QueryString가 a=1&b=2
와 같은 KEY=VALUE 형태였듯, formData의 KEY가 input
의 name
속성이라 생각해도 된다.
이제 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
옵션으로 준 그대로 출력된다.
filename
은 multer
에 의해 임의로 만들어진 파일명이다.
서버에 업로드 될 때 저 이름으로 저장된다.
path
는 dest
+ 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
을 인자로 받는데, req
는 express의 Request 객체고, file
은 우리가 업로드 한 파일에 대한 정보가 들어있다.
done()
은 함수인데, 미들웨어에서 사용하는 next()
와 비슷하게 다음 미들웨어로 작업을 넘기도록 하며 1번째 인자로는 오류를, 2번째 인자로는 파일 이름을 지정한다.
done
의 첫 번째 매개변수로 오류를 전달하면 이미지의 이름이 중복인지 체크하여 중복이면 오류를 발생시키는 등의 처리가 가능할 것이다.
dest
속성만 사용할 때는 multer
가 지어준 임의의 이름을 그대로 사용해야 했지만, filename
함수 안에서 2번째 인자 file
로 파일에 대한 정보를 받아서 업로드 하기 전의 파일명인 _.png
를 그대로 사용할 수도 있다.
destination
은 아까 본 dest
와 비슷하게 업로드 시 저장될 폴더를 지정할 수 있다.
방금 upload
라우터에서 출력한 req.file
과 filename
메소드의 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.originalname
을 done()
의 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
가 될 것이다.
그래서 filename
은 49936f88-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);
},
// ...
파일명이 _.png
면 done()
의 첫번째 인자로 오류를 전달하도록 했다.
이제 _.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
태그의 name
도 myFiles
로 바꿔주자.
// 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
}
]
여러 파일 업로드도 거의 비슷하며 어려울 것이 없었다.
그런데 하나의 input
을 multiple
로 지정해서 여러 개의 파일을 보내는 것이 아니라,
여러 개의 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 />
만약 myFile1
만 multiple
로 지정한다면?
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>
file
과 text
를 한 번에 전송하는 경우다.
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()
로 각 input
의 name
을 지정했다.
이번에는 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
에 데이터가 담겨서 전송된다는 것을 알 수 있다.