[Node.js] Express Multer

Main·2024년 10월 4일
0

Node.js

목록 보기
14/20
post-thumbnail

Multer는 Node.js에서 파일 업로드를 처리하기 위해 주로 사용되는 미들웨어입니다. Express 또는 다른 Node.js 서버에서 클라이언트가 전송하는 파일을 서버에 저장하거나 처리하는 데 사용됩니다. Multer는 특히 multipart/form-data 형식(파일 업로드 시 주로 사용되는 형식)을 파싱하는 데 유용합니다.


Multipart form-data 형식

Multipart/form-data 형식은 텍스트 필드와 파일을 동시에 전송할 수 있도록 해주는 인코딩 타입입니다. 예를 들어, 사용자가 이름과 프로필 사진을 동시에 제출하는 경우 이 형식을 사용합니다.

이 이름에서 알 수 있듯이, multipart는 데이터가 여러 부분으로 나뉘어 전송됨을 의미합니다. 각 부분은 고유의 헤더와 내용을 가지며, 서로 다른 데이터 유형을 표현할 수 있습니다. 이러한 부분들은 boundary라는 구분자를 사용하여 구분됩니다.


Multipart/form-data 구조

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로 구분되며, 파트 헤더본문 내용으로 구성되어 있습니다.

  • 파트 헤더
    • Content-Disposition: 각 파트의 헤더로서 해당 파트의 데이터가 어떤 필드에 해당하는지를 설명합니다. 필드 이름(name)과 파일명(filename)이 포함됩니다.
    • Content-Type: 파일이 포함된 경우, 해당 파일의 MIME 타입이 지정됩니다.
  • 본문 내용 : 폼 필드에 입력된 데이터나 파일의 내용이 본문에 포함됩니다.

예시 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 : 모든 파트는 boundary로 구분됩니다. 이 예시에서는 "----WebKitFormBoundary7MA4YWxkTrZu0gW"가 사용됩니다. 각 파트의 시작과 끝에는 이 문자열이 포함되어 구분됩니다.
  • 파트 1 - 텍스트 데이터
    • "username" 필드는 일반 텍스트 데이터로, "홍길동"이라는 이름이 포함되어 있습니다.
  • 파트 2 - 파일 데이터
    • profile_pic" 필드는 파일 업로드를 포함합니다.
    • Content-Disposition 헤더는 name과 함께 filename 속성을 포함하며, 이는 클라이언트가 선택한 파일의 원래 이름입니다.
    • Content-Type 헤더는 파일의 MIME 타입을 나타냅니다. 이 예시에서는 image/jpeg로 설정되어 있습니다.
    • 파일의 실제 바이너리 데이터는 이 파트의 본문에 포함됩니다.

Mutipart/form-data의 동작 원리

  • 사용자가 HTML 폼을 통해 데이터를 입력하고 파일을 선택합니다.
  • 폼이 제출되면 브라우저는 양식의 모든 텍스트 필드와 파일을 하나의 HTTP 요청으로 묶습니다.
  • 이 HTTP 요청의 Content-Type은 multipart/form-data로 설정되며, 각 필드와 파일이 서로 다른 파트로 나뉘어 서버로 전송됩니다.
  • 서버는 요청을 받아 각 파트를 분석하여, 텍스트 필드와 파일을 개별적으로 처리합니다.

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로 변경해야 합니다.

Multer

위 에서 설명한 Mutipart/form-data는 body-parser로는 요청 본문 해석이 불가능합니다.

Mutipart/form-data를 해석하기 위해 사용되는 것이 Multer입니다.


Multer 설치하기

npm install multer

Multer의 주요 기능과 특징

  • 파일 업로드 처리: Multer는 HTTP 요청의 본문(body)에서 파일과 데이터를 파싱합니다. 업로드된 파일을 서버의 지정된 폴더에 저장하거나 메모리에 임시로 보관할 수 있습니다.
  • Form Data Handling: Multer는 파일뿐만 아니라 일반적인 form-data 필드도 함께 처리할 수 있습니다. 파일 업로드와 동시에 텍스트 필드도 함께 전송할 수 있는 점이 큰 장점입니다.
  • 스토리지 옵션: Multer는 두 가지 주요 저장 방식(storage)를 제공합니다:
    • DiskStorage: 파일을 서버의 파일 시스템에 저장합니다.
    • MemoryStorage: 파일을 메모리에 버퍼로 저장합니다. 이 방법은 파일을 직접 저장하지 않고 다른 처리를 위해 사용하려는 경우에 유용합니다.

Multer 사용법

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)과 일치해야 합니다.


저장 방식(Storage Options)

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 });

Multer 미들웨어

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
});

Multer 에러처리

파일 업로드 중 발생할 수 있는 오류를 처리하기 위해 일반적인 Express 에러 처리 방식을 사용할 수 있습니다.

app.post('/upload', upload.single('file'), (req, res, next) => {
  res.send('파일 업로드 성공');
}, (error, req, res, next) => {
  res.status(400).send({ message: error.message });
});

Multer 파일 업로드 예시 코드

아래는 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 포트에서 실행 중입니다.`);
});
profile
함께 개선하는 프론트엔드 개발자

0개의 댓글