은도미
은도미 개발일지
은도미
전체 방문자
오늘
어제
  • 분류 전체보기 (46)
    • DAILY (0)
    • BOOKS (0)
    • STUDY (17)
      • 면접준비 (3)
      • 모던 자바스크립트 Deep Dive (10)
      • Git (1)
    • PROGRAMING (14)
      • HTML (0)
      • CSS (0)
      • JS (0)
      • TYPESCRIPT (7)
      • NODE.JS (2)
      • Express.js (1)
      • React (3)
      • Nextjs (1)
    • ALGORITHM (5)
      • JS (1)
      • PYTHON (3)
      • 백준 (0)
      • 프로그래머스 (0)
      • 기타 (0)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • ㅂ

최근 댓글

최근 글

티스토리

은도미

은도미 개발일지

PROGRAMING/NODE.JS

[Node.js] Multer-S3를 이용한 이미지 업로드

2022. 2. 24. 14:00

Front-End에서 유저가 이미지를 업로드 하게 될 경우, 이미지를 저장하는 방법은 많지만

아마존 AWS S3버킷에 이미지 파일을 저장하고 DB에는 이미지 파일경로(이미지 주소)를 저장하여 보여주는 방식을 설명하겠습니다.

 

이는 Multer-S3와, AWS-SDK 모듈을 사용하여 구현할 수 있습니다.

multer의 주요 메소드 single, array, fields에 대해 알아보겠습니다.

 

https://www.npmjs.com/package/multer

 

multer

Middleware for handling `multipart/form-data`.. Latest version: 1.4.4, last published: 3 months ago. Start using multer in your project by running `npm i multer`. There are 3145 other projects in the npm registry using multer.

www.npmjs.com

 

multer 설치

npm install --save multer

 

multer를 이용할 파일에 multer 가져오기

const multer = require('multer');
const upload = multer({ destination: 'uploads/' });

destion에는 저장하고 싶은 디렉토리를 적으면 됩니다.

1. multer.single(fieldName) : 하나의 파일을 받아서 저장

router.post('/', upload.single('image'), (req, res) => {
 
    console.log(req.file); // 파일에 대한 정보가 req.file에 FILE 객체로 저장되어 있습니다. 

})
  • upload.single('image')에서 'image'는 field명으로 클라이언트에서 지정해주는 field명을 뜻합니다. 약속된 field명을 적어주면 됩니다.
  • req.file내에는 아래의 정보들이 담겨져 있습니다.
더보기
{
fieldname: 'img',
originalname: '스크린샷 2020-08-11 오후 4.05.51.png',
encoding: '7bit',
mimetype: 'image/png',
size: 18472,
bucket: 'project-portfolio-upload',
key: 'uploads/1597667031103_스크린샷 2020-08-11 오후 4.05.51.png',
acl: 'public-read',
contentType: 'image/png',
contentDisposition: null,
storageClass: 'STANDARD',
serverSideEncryption: null,
metadata: { fieldName: 'img' },
location: 'AWS-S3 URL',
etag: '"22cdfa150f11b3d125853746e5a7a65c"',
versionId: undefined
}

 

2. multer.array(fieldName [, maxCount] ) : 하나의 필드명을 가지는 여러개의 파일을 받아서 저장

router.post('/', upload.array('photos', 4), (req, res) => {

    console.log(req.files); 
    console.log(req.files[0]); // 파일의 인덱스로 접근

})

 

3. fields([{fieldname, maxCount}, {fieldname, maxCount}, ...]) : 여러개의 필드명을 가지는 여러개의 파일을 받아서 저장

router.post('/',
	upload.fields([
      { name: 'mainImage', maxCount: 1 }, 
      { name: 'subImages', maxCount: 5 }
    ]), (req, res) => {
  
    console.log(req.files); 
    console.log(req.files['접근하려는 fieldname']); 

})

 

❓실제로 어떻게 사용했을까요?

backend/.env

액세스 키, 액세스 암호키, S3버킷을 생성한 지역은 환경변수파일로 관리하였습니다.

{
  "accessKeyId": "YOUR AWS ACCESS KEY",
  "secretAccessKey": "YOUR AWS ACCESS SECRET KEY",
  "region": "ap-northeast-2"
}

middlewares/upload.js

middleware를 생성해주었습니다.

//이미지를 받고 오브젝트 스토리지에 올리기 위한 모듈 import
const multer = require("multer");
const multerS3 = require("multer-s3");
const aws = require("aws-sdk");

const recipeStorage = multerS3({
  s3: s3,
  bucket: "yorijori-recipes", //The bucket used to store the file
  contentType: multerS3.AUTO_CONTENT_TYPE, //업로드되는 파일의 memetype
  acl: "public-read",
  metadata: function (req, file, cb) {
    cb(null, { fieldName: file.fieldname }); //metadata onject to be sent to S3
  },
  key: function (req, file, cb) {
    cb(null, `uploads/${Date.now().toString()}_${file.originalname}`);
  }, // The name of the file

  //파일 최대크기 설정(5MB)
  limits: { fileSize: 5 * 1024 * 1024 },
});

exports.recipeUpload = multer({ storage: recipeStorage });

recipeStorage라는 함수가 실행되면, "yorijori-recipes"에 파일이 업로드 됩니다.

 

routes/post.js

이미지 field 가 2개여서 fields 를 사용하였습니다.

//레시피 작성
router.post(
  "/",
  isLoggedIn,
  recipeUpload.fields([{ name: "thumbnail", maxCount: 1 }, { name: "copyImage" }]),
  asyncHandler(async (req, res, next) => {
    const {
      recipeName,
      desc,
      ingredient,
      seasoning,
      process,
      category,
      condition,
      material,
      cook,
      servings,
      time,
      diffic,
    } = req.body;
    const { id: userId } = req.user || req.cookies;
    
    //thumbnail 이미지 location DB에 넣기
    thumbnail = req.files.thumbnail[0].location;

    //copyImage DB에 넣기
    let arrCopyImage = [];
    let CopyImageContents = req.files.copyImage;
    for (i = 0; i < CopyImageContents.length; i++) {
      arrCopyImage.push(CopyImageContents[i].location);
    }
    copyImage = arrCopyImage;
    //이미지의 key값 입력
    thumbnailKey = req.files.thumbnail[0].key;

    //copyImage 의 key 값 찾기
    let arrCopyImageKey = [];
    let CopyImageContentsKey = req.files.copyImage;
    for (i = 0; i < CopyImageContentsKey.length; i++) {
      arrCopyImageKey.push(CopyImageContentsKey[i].key);
    }
    copyImageKey = arrCopyImageKey;

    const posts = await Post.create({
      userId,
      recipeName,
      desc,
      ingredient,
      seasoning,
      process,
      thumbnail,
      thumbnailKey,
      copyImage,
      copyImageKey,
      category,
      condition,
      material,
      cook,
      servings,
      time,
      diffic,
    });
    
    //process내부 processImage의 location, key값 부여
    for (i = 0; i < process.length; i++) {
      posts.process[i].processImage = copyImage[i];
      posts.process[i].processImageKey = copyImageKey[i];
    }
    await posts.save();

    await User.updateOne({ _id: userId }, { $inc: { numPosts: 1 } });
    res.status(201).json({ message: "레시피등록이 완료되었습니다." });
  })
);

copyImage와 processImage가 있는데,

프론트엔드단에서 서버의 요청을 한번에 받아오기 위하여 process배열 스키마 안에 processImage가 있어서,

그 배열 스키마 안의 배열로 실제 값을 넣는게 쉽지않았습니다.

그래서 copyImage라는 가상의 스키마를 생성하여 이부분을 processImage에 값을 새로 배정해주는 방식을 사용하였습니다.

 

아래에 post schemas도 펼치기 하시면 볼 수 있습니다.

더보기

 

schemas/post.js

post스키마입니다.

const mongoose = require("mongoose");
const { Schema } = require("mongoose");

const PostSchema = new mongoose.Schema(
  {
    //레시피명
    recipeName: {
      type: String,
      required: true,
    },
    //요리 소개
    desc: {
      type: String,
      required: true,
    },
    //재료소개
    ingredient: [
      {
        ingreName: { type: String, required: true },
        ingreCount: { type: String, required: true },
      },
    ],
    //양념소개
    seasoning: [
      {
        ingreName: { type: String, required: true },
        ingreCount: { type: String, required: true },
      },
    ],
    process: [
      {
        explain: { type: String, required: true },
        processTime: {
          min: { type: Number },
          sec: { type: Number },
        },
        processImage: { type: String, default: null },
        processImageKey: { type: String, default: null },
      },
    ],

    //조리과정 받는 곳
    copyImage: [{ type: String, required: true }],
    copyImageKey: [{ type: String, required: true }],
    // // 썸네일
    thumbnail: { type: String, required: true },
    thumbnailKey: { type: String, default: null },

    //종류별
    category: {
      type: String,
      required: true,
    },
    //상황별 :
    condition: {
      type: String,
      required: true,
    },
    //재료별
    material: {
      type: String,
      required: true,
    },
    //방법별
    cook: {
      type: String,
      required: true,
    },
    //인원수
    servings: {
      type: String,
      required: true,
    },
    //요리 시간
    time: {
      type: String,
      required: true,
    },
    //난이도
    diffic: {
      type: String,
      required: true,
    },
    //작성글을 유저와 연결합니다
    userId: {
      type: Schema.Types.ObjectId,
      ref: "User",
      required: true,
    },

    //DB에는 삭제안되지만, 게시물 조회할때 useYN:true 인 값만 노출되어주는 컬럼
    useYN: {
      type: Boolean,
      default: true,
    },

    //게시글을 본 횟수
    numViews: {
      type: Number,
      default: 0,
    },

    //게시글이 받은 좋아요 수
    numLikes: {
      type: Number,
      default: 0,
    },
  },
  { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }
);

//게시글에 달린 댓글 수를 받습니다.
PostSchema.virtual("numComments", {
  ref: "Comment",
  localField: "_id",
  foreignField: "postId",
  count: true,
});

PostSchema.index({ recipeName: "text" }); // text index 등록

module.exports = PostSchema;

 

이 방식을 이용하면 이미지파일을 웹 서버에 업로드 자유자재로 가능하며, 수정 삭제또한 post.js라우터에 추가 해 놨다 ! :)

처음 이 방식을 고민하며,

field에 대해 잘 나와있는것이 없어 조금 힘들었으니 고민을 하다보니 성공적으로 구현할 수 있었다 !

 

'PROGRAMING > NODE.JS' 카테고리의 다른 글

Node.js 비동기 코딩  (0) 2021.12.01
    'PROGRAMING/NODE.JS' 카테고리의 다른 글
    • Node.js 비동기 코딩
    은도미
    은도미
    아름다움은 매일 있어.
    hELLO. 티스토리 스킨을 소개합니다.
    제일 위로

    티스토리툴바