Node.js에서 formidable 파일 파싱 에러 해결하기
formidable 라이브러리를 사용한 파일 업로드 시 발생하는 파싱 에러와 그 해결 방법을 다룹니다.
Introduction
Node.js에서 파일 업로드 기능을 구현할 때 formidable은 오랫동안 사랑받아온 라이브러리입니다. 하지만 버전 업그레이드나 설정 오류로 인해 예상치 못한 파싱 에러가 발생할 수 있습니다. 이번 포스트에서는 formidable 파일 파싱 에러의 원인을 분석하고 해결하는 방법을 살펴보겠습니다.
Environment
# Node.js 버전
node --version
# v20.11.0
# formidable 버전
npm list formidable
# formidable@3.5.1
# Express 버전
npm list express
# express@4.18.2Problem
파일 업로드 요청을 처리할 때 다음과 같은 에러가 발생했습니다:
Error: multipart: Part did not end with expected boundary
at Multipart._flush (C:\projects\app\node_modules\formidable\src\index.js:358:11)
at Multipart._flush (node:internal/streams/legacy:59:10)또한 대용량 파일 업로드 시에는 메모리 관련 에러도 함께 나타났습니다:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memoryAnalysis
원인 1: formidable v3의 API 변경
formidable v2에서 v3로 업그레이드하면서 API가 크게 변경되었습니다. 가장 큰 차이는 파싱 방식입니다:
// v2 (레거시 방식) - 더 이상 작동하지 않음
const formidable = require('formidable');
const form = new formidable.IncomingForm();
form.parse(req, (err, fields, files) => {
// ...
});
// v3 (새로운 방식)
const { IncomingForm } = require('formidable');
const form = new IncomingForm();
form.parse(req, (err, fields, files) => {
// ...
});원인 2: Content-Type 헤더 누락
클라이언트에서 multipart/form-data Content-Type을 명시적으로 설정하지 않으면 파싱이 실패합니다:
// 클라이언트 측에서 올바른 헤더 설정 필요
const formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/upload', {
method: 'POST',
body: formData,
// Content-Type은 브라우저가 자동으로 설정해야 함
// 수동으로 설정하면 boundary 문제가 발생할 수 있음
});원인 3: maxFileSize 설정 부재
기본 최대 파일 크기 제한을 초과하면 에러가 발생합니다:
Error: options.maxFileSize exceeded, received 52428800 bytesSolution
1. 올바른 formidable 설정
const express = require('express');
const { IncomingForm } = require('formidable');
const path = require('path');
const fs = require('fs');
const app = express();
// 업로드 디렉토리 생성
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
app.post('/upload', (req, res) => {
const form = new IncomingForm({
uploadDir: uploadDir,
keepExtensions: true,
maxFileSize: 50 * 1024 * 1024, // 50MB
maxTotalFileSize: 100 * 1024 * 1024, // 100MB
maxFieldsSize: 10 * 1024 * 1024, // 10MB for text fields
multiples: true,
filename: (name, ext, part, form) => {
return `${Date.now()}-${part.originalFilename}`;
},
});
form.parse(req, (err, fields, files) => {
if (err) {
console.error('Parse error:', err);
return res.status(400).json({
error: 'File upload failed',
message: err.message
});
}
res.json({
success: true,
files: files.file,
fields: fields
});
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});2. 에러 핸들링 미들웨어 추가
app.use((err, req, res, next) => {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({
error: 'File too large',
maxSize: '50MB'
});
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({
error: 'Unexpected field name'
});
}
next(err);
});3. 스트리밍 방식으로 메모리 최적화
const { Writable } = require('stream');
app.post('/upload-stream', (req, res) => {
const form = new IncomingForm({
uploadDir: uploadDir,
keepExtensions: true,
maxFileSize: 100 * 1024 * 1024,
});
const fileChunks = {};
form.on('file', (name, file) => {
const chunkPath = file.filepath;
fileChunks[name] = file;
});
form.on('error', (err) => {
console.error('Form error:', err);
res.status(500).json({ error: err.message });
});
form.on('end', () => {
res.json({
success: true,
files: Object.keys(fileChunks)
});
});
form.parse(req);
});Lessons Learned
- formidable v3 마이그레이션: v2에서 v3로 업그레이드 시 API 문서를 반드시 확인하세요
- Content-Type 검증: multipart/form-data 헤더가 올바르게 전송되는지 서버 측에서 검증하세요
- 파일 크기 제한 설정:
maxFileSize와maxTotalFileSize를 명시적으로 설정하여 메모리 부족을 방지하세요 - 에러 코드 분류: formidable의 에러 코드를 분류하여 클라이언트에 적절한 응답을 반환하세요
- 대용량 파일 처리: 대용량 파일의 경우 스트리밍 방식을 고려하세요
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.