Node.js 교재 · 20편 · 파일 업로드

파일 업로드 — multer로 이미지·파일 받기

multipart/form-data 가 무엇이고, multer 가 그 위에서 무엇을 자동화하나.

multipart 파일이 서버로 업로드되는 컨셉 일러스트

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 같은 외부 스토리지로 바로 보낼 거면 디스크에 안 쓰는 게 깔끔. memoryStoragereq.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편.

© 2026 주나이테크(주) @JUNAITECH