troubleshooting2025-05-14·8·66/348

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.2

Problem

파일 업로드 요청을 처리할 때 다음과 같은 에러가 발생했습니다:

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 memory

Analysis

원인 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 bytes

Solution

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

  1. formidable v3 마이그레이션: v2에서 v3로 업그레이드 시 API 문서를 반드시 확인하세요
  2. Content-Type 검증: multipart/form-data 헤더가 올바르게 전송되는지 서버 측에서 검증하세요
  3. 파일 크기 제한 설정: maxFileSizemaxTotalFileSize를 명시적으로 설정하여 메모리 부족을 방지하세요
  4. 에러 코드 분류: formidable의 에러 코드를 분류하여 클라이언트에 적절한 응답을 반환하세요
  5. 대용량 파일 처리: 대용량 파일의 경우 스트리밍 방식을 고려하세요

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.