Multer
는 Node.js에서 파일 업로드를 처리하기 위해 주로 사용되는 미들웨어입니다. Express 또는 다른 Node.js 서버에서 클라이언트가 전송하는 파일을 서버에 저장하거나 처리하는 데 사용됩니다.Multer
는 특히 multipart/form-data 형식(파일 업로드 시 주로 사용되는 형식)을 파싱하는 데 유용합니다.
Multipart/form-data 형식은 텍스트 필드와 파일을 동시에 전송할 수 있도록 해주는 인코딩 타입입니다. 예를 들어, 사용자가 이름과 프로필 사진을 동시에 제출하는 경우 이 형식을 사용합니다.
이 이름에서 알 수 있듯이, multipart
는 데이터가 여러 부분으로 나뉘어 전송됨을 의미합니다. 각 부분은 고유의 헤더와 내용을 가지며, 서로 다른 데이터 유형을 표현할 수 있습니다. 이러한 부분들은 boundary
라는 구분자를 사용하여 구분됩니다.
multipart/form-data
는 각 데이터를 경계(boundary)로 구분하여 나누어진 여러 파트로 구성됩니다.
경계(boundary)
각 파트(부분)는 고유한 boundary
로 구분됩니다. 이 boundary
는 클라이언트와 서버 간 데이터를 구분하는 기준이 됩니다. HTTP 요청의 Content-Type 헤더는 다음과 같이 boundary
정보를 포함합니다.
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
multipart/form-data
요청은 여러 파트로 나뉩니다. 각 파트는 헤더와 바디로 구성됩니다.
Content-Type 헤더
multipart/form-data
요청의 Content-Type
헤더는 데이터가 multipart
형식임을 명시하며, 경계(boundary)를 정의합니다.
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
요청 본문 바디
요청의 본문(body)은 여러 개의 파트(part)로 구성되며, 각 파트는 boundary
로 구분되며, 파트 헤더와 본문 내용으로 구성되어 있습니다.
name
)과 파일명(filename
)이 포함됩니다.예시 form-data
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
// part 1
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
홍길동
// part 2
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="profile_pic"; filename="example.jpg"
Content-Type: image/jpeg
(binary data representing the file)
------WebKitFormBoundary7MA4YWxkTrZu0gW--
boundary
로 구분됩니다. 이 예시에서는 "----WebKitFormBoundary7MA4YWxkTrZu0gW"
가 사용됩니다. 각 파트의 시작과 끝에는 이 문자열이 포함되어 구분됩니다."username"
필드는 일반 텍스트 데이터로, "홍길동"이라는 이름이 포함되어 있습니다.profile_pic"
필드는 파일 업로드를 포함합니다.name
과 함께 filename
속성을 포함하며, 이는 클라이언트가 선택한 파일의 원래 이름입니다.image/jpeg
로 설정되어 있습니다.multipart/form-data
로 설정되며, 각 필드와 파일이 서로 다른 파트로 나뉘어 서버로 전송됩니다.<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>폼 데이터 콘솔 출력 테스트</title>
</head>
<body>
<form id="uploadForm" enctype="multipart/form-data">
<label for="username">이름:</label>
<input type="text" id="username" name="username"><br><br>
<label for="profile_pic">프로필 사진:</label>
<input type="file" id="profile_pic" name="profile_pic"><br><br>
<input type="submit" value="업로드">
</form>
<script>
document.getElementById('uploadForm').addEventListener('submit', function(event) {
event.preventDefault(); // 폼의 기본 전송 동작을 막음
// FormData 객체를 사용해 폼 데이터를 가져옴
const formData = new FormData(event.target);
// FormData 객체의 내용을 반복하며 콘솔에 출력
for (let [key, value] of formData.entries()) {
if (value instanceof File) {
console.log(`${key}: ${value.name} (파일 크기: ${value.size}바이트)`);
} else {
console.log(`${key}: ${value}`);
}
}
});
</script>
</body>
</html>
enctype="multipart/form-data
: 이 속성이 지정되지 않으면 파일이 전송되지 않습니다. 일반적으로 enctype
은 기본적으로 application/x-www-form-urlencoded
로 설정되지만, 파일을 전송할 때는 multipart/form-data
로 변경해야 합니다.위 에서 설명한 Mutipart/form-data는 body-parser로는 요청 본문 해석이 불가능합니다.
Mutipart/form-data를 해석하기 위해 사용되는 것이 Multer입니다.
npm install multer
Multer
는 HTTP 요청의 본문(body)에서 파일과 데이터를 파싱합니다. 업로드된 파일을 서버의 지정된 폴더에 저장하거나 메모리에 임시로 보관할 수 있습니다.Multer
는 파일뿐만 아니라 일반적인 form-data 필드도 함께 처리할 수 있습니다. 파일 업로드와 동시에 텍스트 필드도 함께 전송할 수 있는 점이 큰 장점입니다.const express = require('express');
const multer = require('multer');
const app = express();
// 파일이 업로드될 디렉토리를 지정
const upload = multer({ dest: 'uploads/' });
// 싱글 파일 업로드 처리
app.post('/upload', upload.single('file'), (req, res) => {
console.log(req.file); // 파일 관련 정보 출력
console.log(req.body); // 기타 폼 데이터 출력
res.send('파일 업로드 완료');
});
// 서버 시작
app.listen(8080, () => {
console.log('서버가 8080번 포트에서 실행 중입니다.');
});
위 코드에서 upload.single('file')
부분은 클라이언트가 전송한 파일을 처리하도록 지정하는 부분입니다. 'file'
은 HTML form에서 파일 input의 이름(name)과 일치해야 합니다.
1 ) DiskStorage: 디스크에 파일을 저장하며, 파일 이름이나 저장 경로를 사용자 정의할 수 있습니다.
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/'); // 저장할 디렉토리 지정
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix); // 파일 이름 설정
}
});
const upload = multer({ storage: storage });
2 ) MemoryStorage : 파일을 메모리에 버퍼 형태로 저장합니다. 주로 파일의 내용을 즉시 처리하거나 외부 클라우드 스토리지에 업로드할 때 사용합니다.
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
1 ) single(fieldname)
: 폼에서 단일 파일을 업로드할 때 사용됩니다. 특정 필드 이름(fieldname
)을 지정하여 해당 필드에서 하나의 파일만 업로드하도록 처리합니다.
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('profile_pic'), (req, res) => {
console.log(req.file); // 업로드된 파일 정보 출력
console.log(req.body); // 기타 폼 데이터 출력
res.send('단일 파일 업로드 완료');
});
2 ) array(fieldname, maxCount)
: 특정 필드에서 여러 개의 파일을 업로드할 때 사용됩니다. fieldname
은 파일 필드의 이름이며, maxCount
는 허용되는 파일의 최대 개수를 의미합니다.
const upload = multer({ dest: 'uploads/' });
app.post('/upload-multiple', upload.array('photos', 5), (req, res) => {
console.log(req.files); // 업로드된 파일 정보 출력 (배열 형태)
console.log(req.body); // 기타 폼 데이터 출력
res.send('여러 파일 업로드 완료');
});
3 ) fields(fieldsArray)
: 서로 다른 여러 필드에서 파일을 업로드할 때 사용됩니다. fieldsArray
는 파일 필드의 이름과 각 필드별 최대 파일 개수를 설정할 수 있는 배열입니다.
const upload = multer({ dest: 'uploads/' });
app.post('/upload-fields', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]), (req, res) => {
console.log(req.files); // 업로드된 파일 정보 출력
console.log(req.body); // 기타 폼 데이터 출력
res.send('여러 필드의 파일 업로드 완료');
});
4 ) none()
: 파일 없이 폼 데이터만 처리할 때 사용됩니다. 이 미들웨어는 파일 업로드를 허용하지 않으며, 일반 텍스트 폼 데이터만 허용합니다.
const upload = multer();
app.post('/upload-data', upload.none(), (req, res) => {
console.log(req.body); // 폼 데이터 출력
res.send('폼 데이터 처리 완료');
});
💡 Multer 미들웨어 선택 기준
single()
을 사용합니다.array()
를 사용합니다.fields()
를 사용합니다.none()
을 사용합니다.파일 크기 제한
const upload = multer({
dest: 'uploads/',
limits: { fileSize: 1024 * 1024 * 5 } // 최대 파일 크기를 5MB로 제한
});
파일 타입 필터링
특정 파일 타입만 허용하려면 fileFilter
를 사용할 수 있습니다
const fileFilter = (req, file, cb) => {
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
cb(null, true); // 허용할 파일 타입
} else {
cb(new Error('허용되지 않는 파일 형식입니다.'), false);
}
};
const upload = multer({
dest: 'uploads/',
fileFilter: fileFilter
});
파일 업로드 중 발생할 수 있는 오류를 처리하기 위해 일반적인 Express 에러 처리 방식을 사용할 수 있습니다.
app.post('/upload', upload.single('file'), (req, res, next) => {
res.send('파일 업로드 성공');
}, (error, req, res, next) => {
res.status(400).send({ message: error.message });
});
아래는 Multer를 사용해 단일 파일 업로드, 멀티 파일 업로드, 필드별 파일 업로드, 파일 없이 폼 데이터만 업로드를 처리하는 전체 Express 서버 예제 코드입니다. 템플릿 엔진으로는 EJS를 사용하고, 파일 업로드는 diskStorage
를 사용해 파일을 서버에 저장하도록 설정하였습니다.
<!-- // views/index.ejs -->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>파일 업로드 테스트</title>
</head>
<body>
<h1>파일 업로드 테스트</h1>
<!-- 단일 파일 업로드 폼 -->
<h2>1) 단일 파일 업로드</h2>
<form action="/upload-single" method="POST" enctype="multipart/form-data">
<input type="file" name="singleFile" required><br><br>
<button type="submit">업로드</button>
</form>
<!-- 멀티 파일 업로드 폼 -->
<h2>2) 멀티 파일 업로드 (최대 5개)</h2>
<form action="/upload-multiple" method="POST" enctype="multipart/form-data">
<input type="file" name="multipleFiles" multiple required><br><br>
<button type="submit">업로드</button>
</form>
<!-- 필드별 파일 업로드 폼 -->
<h2>3) 필드별 파일 업로드</h2>
<form action="/upload-fields" method="POST" enctype="multipart/form-data">
<label for="avatar">아바타:</label>
<input type="file" name="avatar" required><br><br>
<label for="gallery">갤러리 이미지 (최대 8개):</label>
<input type="file" name="gallery" multiple><br><br>
<button type="submit">업로드</button>
</form>
<!-- 파일 없이 폼 데이터만 업로드 -->
<h2>4) 파일 없이 폼 데이터 업로드</h2>
<form action="/upload-data" method="POST">
<label for="username">이름:</label>
<input type="text" name="username" required><br><br>
<label for="email">이메일:</label>
<input type="email" name="email" required><br><br>
<button type="submit">전송</button>
</form>
</body>
</html>
// app.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// 템플릿 엔진 설정
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// 정적 파일 서빙
app.use(express.static('uploads'));
// 업로드 설정 (diskStorage)
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
// 파일 크기 제한: 5MB
const upload = multer({
storage: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB로 파일 크기 제한
});
// 1) 단일 파일 업로드 처리 라우트
app.post('/upload-single', upload.single('singleFile'), (req, res, next) => {
if (!req.file) {
return res.status(400).send('파일이 업로드되지 않았습니다.');
}
console.log(req.file); // 업로드된 파일 정보
res.send('단일 파일 업로드 완료');
});
// 2) 멀티 파일 업로드 처리 라우트
app.post('/upload-multiple', upload.array('multipleFiles', 5), (req, res, next) => {
if (!req.files || req.files.length === 0) {
return res.status(400).send('파일이 업로드되지 않았습니다.');
}
console.log(req.files); // 업로드된 파일들 정보
res.send('멀티 파일 업로드 완료');
});
// 3) 필드별 파일 업로드 처리 라우트
app.post('/upload-fields', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]), (req, res, next) => {
if (!req.files || (!req.files['avatar'] && !req.files['gallery'])) {
return res.status(400).send('파일이 업로드되지 않았습니다.');
}
console.log(req.files); // 각 필드별 업로드된 파일 정보
res.send('필드별 파일 업로드 완료');
});
// 4) 파일 없이 폼 데이터만 업로드 처리 라우트
app.post('/upload-data', upload.none(), (req, res, next) => {
console.log(req.body); // 폼 데이터 출력
res.send('폼 데이터 업로드 완료');
});
// 메인 페이지 라우트 - 업로드 폼 제공
app.get('/', (req, res) => {
res.render('index');
});
// 에러 처리 미들웨어
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
// Multer에서 발생한 에러 처리
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).send('파일 크기가 너무 큽니다. 최대 크기는 5MB입니다.');
}
return res.status(400).send(`Multer 에러: ${err.message}`);
} else if (err) {
// 기타 에러 처리
return res.status(500).send(`서버 에러: ${err.message}`);
}
next();
});
// 서버 시작
app.listen(8080, () => {
console.log(`서버가 8080 포트에서 실행 중입니다.`);
});