13편의 express.json() 으로는 못 받는 게 하나 있다 — 파일. 이미지·PDF·동영상은 multipart/form-data 라는 별도 인코딩으로 전송. JSON 파서로는 못 풀고 전용 미들웨어가 필요. 그 표준이 multer.
이번 편은 multer 의 핵심 패턴 — 메모리·디스크 저장 선택, MIME 검증, 크기 제한, S3 직업로드까지. 보안 사고가 자주 나는 영역이라 끝까지.
1. 설치와 가장 단순한 사용
$ npm install multer
$ npm install -D @types/multer
// server.js
import express from 'express';
import multer from 'multer';
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('photo'), (req, res) => {
console.log(req.file);
// {
// fieldname: 'photo',
// originalname: 'cat.jpg',
// mimetype: 'image/jpeg',
// destination: 'uploads/',
// filename: '8f3a1c2e9b4d5...',
// path: 'uploads/8f3a1c2e9b4d5...',
// size: 154823
// }
res.json({ url: `/uploads/${req.file.filename}` });
});
HTML 폼은 enctype="multipart/form-data" 필수. upload.single('photo') 가 미들웨어로 끼어들어 multipart 를 파싱하고 req.file 에 결과를 박는다.
2. 저장 방식 — memoryStorage vs diskStorage
| 방식 | 장점 | 단점 | 용도 |
| diskStorage (기본) | 큰 파일도 메모리 안전 | 디스크 IO, 임시 정리 필요 | 서버 로컬 저장 |
| memoryStorage | 빠름, 즉시 처리 가능 | 큰 파일은 메모리 폭발 | S3·CDN 즉시 업로드 |
S3 같은 외부 스토리지로 바로 보낼 거면 디스크에 안 쓰는 게 깔끔. memoryStorage 면 req.file.buffer 에 11편의 Buffer 객체가 통째로 담긴다.
// memoryStorage — 즉시 S3 업로드
const upload = multer({ storage: multer.memoryStorage() });
app.post('/upload-s3', upload.single('photo'), async (req, res) => {
const result = await s3.putObject({
Bucket: 'my-bucket',
Key: `${Date.now()}-${req.file.originalname}`,
Body: req.file.buffer, // ← Buffer 직접
ContentType: req.file.mimetype,
});
res.json({ url: `https://my-bucket.s3.../${result.Key}` });
});
3. 검증 — MIME 과 크기 제한 (보안 필수)
업로드는 보안 사고 1순위. 검증 없이 받으면 곧 사고.
const upload = multer({
storage: multer.diskStorage({
destination: 'uploads/',
filename: (req, file, cb) => {
// 원본 이름 그대로 쓰면 위험. 랜덤 + 확장자만 신뢰
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${Date.now()}-${crypto.randomUUID()}${ext}`);
},
}),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB 상한
files: 5, // 동시 5개 상한
},
fileFilter: (req, file, cb) => {
const ok = ['image/jpeg', 'image/png', 'image/webp'].includes(file.mimetype);
if (!ok) return cb(new Error('이미지 파일만 가능'));
cb(null, true);
},
});
최악의 실수 4가지 — ① 원본 파일명 그대로 저장(../../etc/passwd path traversal), ② 크기 제한 안 둠(디스크 가득), ③ MIME 안 검증(.php 업로드 후 실행), ④ 정적 폴더 공개(업로드 폴더가 서빙되어 악성 파일 다운로드). 4개 다 한 줄 한 줄 다 막아야.
4. 여러 파일 — array·fields
// 같은 이름으로 여러 개
app.post('/photos', upload.array('photos', 10), (req, res) => {
console.log(req.files); // 배열
res.json({ count: req.files.length });
});
// 서로 다른 이름의 파일들
app.post('/profile', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'banner', maxCount: 1 },
{ name: 'gallery', maxCount: 5 },
]), (req, res) => {
console.log(req.files);
// { avatar: [{...}], banner: [{...}], gallery: [{...}, ...] }
});
실전에선 fields 가 자주 — 프로필 페이지처럼 다른 종류 파일 묶음.
5. 에러 처리 — Multer 전용 에러
크기 초과·필터 거부 시 MulterError 가 throw. 14편 에러 미들웨어에서 분기.
// 에러 핸들러 (라우트 뒤에)
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ error: '파일이 너무 큽니다 (5MB 이하)' });
}
return res.status(400).json({ error: err.message });
}
// 그 외 에러
next(err);
});
특히 LIMIT_FILE_SIZE 가 흔하다. 사용자에게 친절한 메시지로 변환.
실전 — 거의 항상 S3·R2 직업로드 — 서버를 거치면 메모리·대역폭 낭비. 클라이언트가 S3 presigned URL 받아 브라우저에서 직접 업로드 → 끝난 뒤 서버에 알림. 서버는 URL 발급과 메타데이터만. 회사 코드 90% 가 이 방식. multer 는 이걸 못 하는 작은 프로젝트·내부 도구용.
요약 — 20편 좌표
여기까지 정리. npm i multer + upload.single/array/fields. 저장은 diskStorage(로컬) 또는 memoryStorage(S3 직행). 4가지 보안 필수 — 랜덤 파일명·크기 제한·MIME 검증·업로드 폴더 비공개. 여러 파일은 array·fields. MulterError 분기로 사용자 친화 메시지. 실전 권장은 S3 presigned URL 직업로드. 다음 편에서 로그 — winston·morgan.
다음 편 예고 — 로깅
winston·morgan 으로 서버 로그 관리. 21편.