multer? S3? 이미지 업로드!

Gaeun·2023년 2월 12일
1

wecode TIL

목록 보기
24/24

2차 프로젝트 중, 사용자가 본인의 상품을 팔기 위해 웹사이트에 올릴 때, 상품의 사진 또한 업로드 하는 것을 생각하며 쿼리를 작성하였다.

종범님께서는 이러한 피드백을 주셨는데 이때 나는 머리를 댕~하고 맞은 기분이었다. 그동안 나는 DB에 있는 사진을 가져오는 경우만 생각했고, 사용자가 직접 업로드하는 것을 생각해본 적이 없었기에 그냥 평소처럼 쿼리문을 작성하면 그만이라고 생각했었다. 이 피드백을 받은 이후부터 multer가 무엇인지, 어떻게 사용자 로컬에 있는 사진을 내 DB에 옮겨 담을 것인지 고민하기 시작했다.

우선, 종범님께서 주신 힌트, multer에 대해 찾아보았다.

1. multer

Multer는 파일 업로드를 위해 사용되는 multipart/form-data 를 다루기 위한 node.js 의 미들웨어 입니다. 효율성을 최대화 하기 위해 busboy 를 기반으로 하고 있습니다.

주: Multer는 multipart (multipart/form-data)가 아닌 폼에서는 동작하지 않습니다.

multer 깃허브에 있는 설명이다. 일단 파일 업로드를 위해서 사용되는 것이라는 건 알았고, multipart 폼을 사용한다는 것은 알았다. 근데, multipart는 무엇이고 또 form-data는 무엇인데?라는 생각이 꼬리를 물었다. 그래서... multipart/form-data는 또 뭔데?하면서 찾아보았다.

1-1. multipart/form-data

It is one of the two ways of encoding the HTML form. It is specifically used when file uploading is required in HTML form. It sends the form data to server in multiple parts because of large size of file.
(출처: https://www.geeksforgeeks.org/define-multipart-form-data/)

이외에도 여러 사이트에서 multipart/form-data를 찾아보았는데 아래와 같이 이해하였다.

  • 웹 클라이언트가 요청을 보낼 때 HTTP 프로토콜의 바디 부분에 데이터를 여러 부분으로 나눠서 보내는 것
  • 파일을 전송할 때 사용하는 방법
  • 파일을 여러 개를 담아 전송하면 body 부분에 파일이 여러 개의 부분으로 연결되어 전송된다. 이렇게 여러 부분으로 나누어 전송하는 것을 multipart 데이터라고 한다.
  • FormData 객체는 fetch 등의 네트워크 메서드를 통해 HTML 폼을 보내는데 사용
  • Content-Type 속성은 multipart/form-data로 지정된 후 전송
  • 서버 관점에선 FormData를 사용한 방식과 일반 폼 전송 방식에 차이가 없다.

2. S3

Simple Storage Service의 약자로 파일 서버의 역할을 하는 서비스다. 일반적인 파일서버는 트래픽이 증가함에 따라서 장비를 증설하는 작업을 해야 하는데 S3는 이와 같은 것을 대행한다. 트래픽에 따른 시스템적인 문제는 걱정할 필요가 없어진다. 또 파일에 대한 접근 권한을 지정 할 수 있어서 서비스를 호스팅 용도로 사용하는 것을 방지 할 수 있다.
(출처: https://dev.classmethod.jp/articles/for-beginner-s3-explanation/)

3. multer와 S3를 이용하여 이미지 업로드하기

그래서... 일단 AWS S3와 IAM을 설정한 뒤 코드를 이렇게 작성해보았다.

// utils/multer.js
const AWS = require("aws-sdk");
const multer = require("multer");
const multerS3 = require("multer-s3");
const path = require("path");

AWS.config.update({
  region: "ap-northeast-2",
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});

const s3 = new AWS.S3();

const allowedExtentions = [".png", ".jpg", ".jpeg", ".bmp"];

const uploadImages = multer({
  storage: multerS3({
    s3: s3,
    bucket: "wehicle-s3-bucket",
    key: (req, file, callback) => {
      const uploadDirectory = req.query.directory ?? "";
      const extension = path.extname(file.originalname);
      if (!allowedExtentions.includes(extension.toLowerCase())) {
        return callback(new Error("WRONG_EXTIENTION"));
      }
      callback(null, `${Date.now()}_${file.originalname}`);
    },
    acl: "public-read-write",
  }),
}).array("images", 5);

module.exports = uploadImages;

Access Key ID와 secret key는 환경변수로 관리해주었고, png, jpg, jpeg, bmp 확장자를 가지고 있는 파일만 받을 수 있도록 작성하였다. 허나, 가끔 대문자로(ex. ㅇㅇㅇ.JPG) 되어있는 확장자가 있기 때문에 extention.toLowerCase()를 통해 그 파일 또한 받을 수 있게 하였다.

이 파일의 uploadImages라는 함수가 실행되면 wehicle-s3-bucket에 파일이 업로드되고, 해당 파일은 현재 시각_파일 원 이름으로 업로드되게 된다.

// routes/biddingRouter.js
const express = require("express");
const router = express.Router();

const biddingController = require("../controllers/biddingController");
const uploadImages = require("../utils/multer");
const { validateToken } = require("../utils/auth");


router.post("/sell", validateToken, uploadImages, biddingController.createSell);

module.exports = { router };

multer.js에서 export한 uploadeImages를 가져오고, validateToken이라는 미들웨어를 거쳐 해당 사용자의 JWT를 통해 유저를 검증한 뒤, uploadeImages라는 미들웨어를 거쳐 사용자가 업로드한 이미지를 버킷에 저장하였다.

// controllers/biddingController.js
const createSell = asyncErrorHandler(async (req, res) => {
  const images = req.files.map(({ location }) => location);

  const { . . . } = req.body;
  if (images.length === 0)
    throwCustomError("MISSING_SELLING_PRODUCT_IMAGES", 400);
  
  . . .

  await biddingService.createSell(
    req.userId, . . ., images);

  return res.status(201).json({ message: "SellPriceCreated!" });
});

클라이언트에서 이미지를 업로드하고 서버로 여러 정보를 담아 form-data를 보낼 시, 이미지는 S3 버킷을 거쳐 저장된 후 이미지 url이 req.files에 location에 담겨 오게 된다. 이 경로(url)를 DB에 저장하기 위해 이후 service, dao는 평소 내가 작성했던 것처럼 코드를 작성하면 된다!

나는 이미지를 여러 개를 받아야했기 때문에 req.files를 map을 돌려 images라는 변수를 선언해 배열로 저장하였고, 배열의 길이가 0일 때의 커스텀 에러 또한 분기처리 해주었다!

여기까지 작성하는데에 거의 하루 이상을 소모하였다. 처음해보는 것이라 어떻게 코드를 써내려가야할지 많은 고민이 있었지만, 공식 문서를 보고, 여러 블로그들을 보며, 같이 고민하며 코드를 써내려간 동기분들 덕분에(특히나 승기님!) 완성해낼 수 있었다. 역시 동기사랑 나라사랑!

profile
🌱 새싹 개발자의 고군분투 코딩 일기

0개의 댓글