V-logue

[Node.js] Express.multer, Multer-S3 본문

발자취/Node.js

[Node.js] Express.multer, Multer-S3

보그 2022. 6. 13. 19:39

Node와 FE에서 사용자가 이미지를 올리는 방법은 여러가지가 있는데,

개인 서버에 static 폴더를 만들어서 클라이언트로부터 요청받은 파일을 저장하는 방법과

db에 저장하는 방법이 있고,

마지막으로 AWS S3 버킷을 이용해서 이미지 파일을 저장하고 db에 버킷의 경로를 받아서

서버가 이 경로로 클라이언트에 응답하는 방법이 있다.

찾아보니, axios로 form태그를 사용해 이미지를 넘기려면 multer를 사용해야 한다고 하니

어처피 나는 이 multer에 대해서 배웠어야 했다.

 

이 AWS S3를 사용하면 개인 서버에 저장하는 것에서 고려도는 PC 성능을 고려하지 않아도 되고,

또 이 S3  버킷이라는게 내가 사용하는 만큼 비용을 지불하는 것이기 때문에

비용적인 부담도 줄어들어 아주 효율적인 서버를 구성할 수 있게 된다.

 

그리고 AWS S3를 사용하는 방법은 multeraws-sdk 모듈을 통해 구현된다.

 

npm install multer multer-s3 aws-sdk --save

 

위 코드를 사용해 multer와 asw-sdk 패키지를 설치하고,

 

S3 버킷을 만들어준다.

자세한건 AWS S3 버킷 생성링크를 눌러서 확인해주시기 바랍니다.

 

그 다음엔 IAM을 설정해야 하는데,

 

IAM이란 AWS 리소스에 대한 엑세스를 안전하기 제어할 수 있는 웹 서비스다.

이를 통해 리소스를 사용하도록 인증하거나 권한 부여된 대상을 제어한다. 참고한 블로그

  • AWS 서비스에서 IAM을 선택하고, 사용자 - 사용자 추가를 선택합니다. - 세부 정보에서 사용자 이름을 지정하고, 엑세스 유형은 프로그래밍 방식 엑세스를 선택합니다.
  • 권한 설정에서 기본 정책 직접 연을 선택하고 AmazonFullAccess를 체크합니다.
  • 태그, 검토 사용자 만들기 순으로 클릭하면 끝
  • 성공했다면, 엑세스 키 ID와 비밀 엑세스 키 값이 나오는데 이건 awsconfig.json 설정에서 필요하므로 반드시 메모해야 합니다.

 

그 다음엔 awsconfig.json 파일을 생성해야 한다.

accessKeyId, secretAccessKey는 IAM에서 생성한 2가지 키 값을 입력해주면 된다.

region은 S3 지역인데, 이는 접속 URL을 통해 확인이 가능하다.

이 awsconfig.json파일에 입력되는 키값은 일종의 시크릿 키이기 때문에 .gitignore파일에서

업로드 되지않게 따로 설정을 해줘야 한다.

 

이제 ubuntu 서버에서 데이터를 비교해가며 업로드 되는지 확인을 해봐야 하는데,

 

multer-s3와 asw-sdk의 버전이 일치하지 않아,client.send is not a function이라는 에러가 나타났다.

TypeError: this.client.send is not a function
    at Upload.__uploadUsingPut (/home/ubuntu/Desklet/node_modules/@aws-sdk/lib-storage/dist-cjs/Upload.js:47:25)
    at Upload.__doConcurrentUpload (/home/ubuntu/Desklet/node_modules/@aws-sdk/lib-storage/dist-cjs/Upload.js:91:39)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Promise.all (index 0)
    at async Upload.__doMultipartUpload (/home/ubuntu/Desklet/node_modules/@aws-sdk/lib-storage/dist-cjs/Upload.js:141:9)
    at async Upload.done (/home/ubuntu/Desklet/node_modules/@aws-sdk/lib-storage/dist-cjs/Upload.js:37:16)

package.json 파일을 확인해보니 multer-s3의 버전이 3.대고 asw-sdk의 버전은 2.x.x라는 형식이라고 되있었는데,

 "multer-s3": "^3.0.1",
    "aws-sdk": "^2.1148.0",

같이 프로젝트를 진행하는 팀원분의 말을 들어보니 multer-s3의 버전을 2점대 버전으로

npm i multer-s3@^2 --save // multer 2버전으로 다운그레이드

강제로 다운그레이드하면 돌아간다고 해서 실행하니 에러가 해결됐다.

 

그리고 이 문제를 해결하니,

AccessControlListNotSupported: The bucket does not allow ACLs라는 에러가 등장했다.

AccessControlListNotSupported: The bucket does not allow ACLs
    at Request.extractError (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/services/s3.js:711:35)
    at Request.callListeners (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:686:14)
    at Request.transition (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /home/ubuntu/Desklet/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:688:12)
    at Request.callListeners (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/sequential_executor.js:116:

찾아보니 이 에러와 관련된 글이 상당히 많아서  해결하는데 어려움이 없지는 않았다.

일단 먼저, AWS S3로가서 권한 탭을 눌러보면,

이제 이렇게 ACL(액세스 제어 목록)이라는 탭이 있을텐데 보면 오른쪽에 편집버튼을 누르고 acl을 편집해야한다.

아마도 이게, ACL(비활성화)가 아마 선택되있을 것이다.

이걸 인터넷에서 찾아서 대충 활성시켜주면 해결된다.

 

그리고 한가지 멍청한 실수를 했는데

InvalidAccessKeyId: The AWS Access Key Id you provided does not exist in our records.
    at Request.extractError (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/services/s3.js:711:35)
    at Request.callListeners (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:686:14)
    at Request.transition (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /home/ubuntu/Desklet/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/request.js:688:12)
    at Request.callListeners (/home/ubuntu/Desklet/node_modules/aws-sdk/lib/sequential_executor.js:116:18)

AWS access Key Id가 존재하지 않는다고 뜨길래, 설정해 놨는데 왜 안뜨는거지..?? 라는 의문이 들때쯤

찾아보니 정말로 멍청한 실수인게

처음에 IAM을 설정할 때 이제 이 태그키랑 태그 값이 access Key Id인줄 알고 사용했는데,

알고보니 

여기에 나와있는 엑세스 키 ID값이었다. 참고로, 비밀키는 생성시에만 보거나 다운로드 할 수 있다고 써 있으니

생성시에 반드시 따로 보관하도록 하자.

 

그 이후는 순조롭게 코드를 작성하고 시험해볼 수 있었는데, 따로 폴더를 만들어서 끌어오려다가

.귀찮아서 routes/posts폴더에 다 넣어버렸다. 나중에 정리를 할 예정,

const express = require("express");
const router = express.Router();
const auth = require("../middlewares/auth-middleware");
const Post = require("../schemas/post");
const Comment = require("../schemas/comment");

const aws = require("aws-sdk");
const multer = require("multer");
const multerS3 = require("multer-s3");

const path = require("path");
aws.config.loadFromPath(__dirname + "/awsconfig.json"); // 사용자 인증 keyId, Secret KeyId
const s3 = new aws.S3();

const upload = multer({
  storage: multerS3({
    s3: s3, // 사용자 인증권한이 담긴다.
    bucket: "desklet", // 버킷이름
    acl: "public-read-write", // 액세스 제어 목록( Access control for the file)
    key: function (req, file, cb) {
      const url = path.extname(file.originalname);
      cb(null, Date.now() + url);
    },
  }),
  limits: {
    fileSize: 1000 * 1000 * 10,
  },
});

// 게시물 작성
// upload.single('postImage')에서 'image'는 변수명
// auth추가
// const field = upload.fields([{ name: "title"}, {name: "content"}, {name: "postImage"}]);

router.post("/", auth, upload.single("postImage"), async (req, res) => {
  //posts
  try {
    console.log("req.file:", req.file);
    console.log(req.body);
    // req.file내에는 fieldname, originalname,
    //encoding, destination, filename 등의 정보가 저장
    // 저장 성공시 asw s3 버킷에 저장
    const imageUrl = req.file.location;
    const createdAt = new Date().toLocaleString();

    const { userId, nickName } = res.locals.user;
    const { title, content } = req.body; // userId 추가해야합니다.
    const postExist = await Post.find().sort("-postId").limit(1);
    let postId = 0;

    if (postExist.length) {
      postId = postExist[0]["postId"] + 1;
    } else {
      postId = 1;
    }
    await Post.create({
      title,
      content,
      nickName,
      imageUrl,
      userId,
      createdAt,
      postId,
    });

    res.json({ success: "msg" });
  } catch (err) {
    console.log(err);
    response(res, 500, "서버 에러");
  }
});

//전체 게시물 조회
router.get("/", async (req, res) => {
  //posts

  const post = await Post.find().sort("-postId");

  res.send({ post: post });
});

//postId=6297c444c14824e8f5a484ff
//상세 페이지 조회
router.get("/:postId", async (req, res) => {
  //posts/:postId
  const { postId } = req.params;
  const post = await Post.findOne({ postId: postId });
  const comments = await Comment.findOne({ postId: postId });
  console.log(post);
  res.json({ post, comments }); //comments
});

//게시글 삭제
router.delete("/:postId", auth, async (req, res) => {
  const { postId } = req.params;
  const { userId } = res.locals.user;

  const existPost = await Post.find({ postId: postId });
  const [existImage] = await Post.find({ postId: postId });
  const Image = existImage.imageUrl;
  console.log("이미지:", Image);
  const DeleteS3 = Image.split("/")[3];
  console.log("딜리트:", DeleteS3);

  const existComment = await Comment.find({ postId: postId });

  if (userId === existPost[0]["userId"]) {
    if (existPost && existComment) {
      await Post.deleteOne({ postId: postId });
      await Comment.deleteMany({ postId: postId });
      s3.deleteObject(
        {
          Bucket: "desklet",
          Key: DeleteS3,
        },
        (err, data) => {
          if (err) {
            throw err;
          }
        }
      );
      res.send({ result: "success" });
    } else if (existPost) {
      await Post.deleteOne({ postId: postId });
      s3.deleteObject(
        {
          Bucket: "desklet",
          Key: DeleteS3,
        },
        (err, data) => {
          if (err) {
            throw err;
          }
        }
      );
      res.send({ result: "success" });
    }
  } else {
    res.status(401).send({ result: "fail" });
  }
});

//게시글 수정
router.put("/:postId", auth, upload.single("postImage"), async (req, res) => {
  ///posts/:postId
  const { postId } = req.params;
  const user = res.locals.user;
  const userId = user.userId;
  const { title, content } = req.body;

  const existPost = await Post.findOne({ postId: postId });
  const [existUrl] = await Post.find({ postId: postId });
  const UrlImage = existUrl.imageUrl;
  console.log("URL입니다:", UrlImage);
  const DeleteS3 = UrlImage.split("/")[3];
  console.log("DELETE입니다:", DeleteS3);
  s3.deleteObject( // 먼저 s3 이미지를 지워주고 업데이트 해야한다.
    {
      Bucket: "desklet",
      Key: DeleteS3,
    },
    (err, data) => {
      if (err) {
        throw err;
      }
    }
  );

  const imageUrl = req.file.location;
  console.log(imageUrl);

  if (!imageUrl) {
    return res.status(400).send({
      msg: "사진을 추가해 주세요",
    });
  }

  if (userId === existPost.userId) {
    if (existPost) {
      await Post.updateOne(
        { postId: postId },
        { $set: { title, content, imageUrl } }
      );
      res.send({ result: "success" });
    } else {
      res.status(400).send({ result: "fail" });
    }
  } else {
    res.send({ result: "fail " });
  }
});

module.exports = router;

multer - s3를 이용해 S3클라우드에 데이터를 업로드하고

 

수정, 삭제하는 기능까지 모두 구현이 끝났다.

 

이게 참고로 수정기능을 구현할 때 주의할 점이, 뭐냐면 s3.deleteObject로 먼저 S3내에 보관되있는

 

파일을 지워주고 업데이트를 해야한다는 점이다. 

 

아무래도 하나의 파일에 모든 기능을 몰아넣어서 지저분하다고 느낄 수 있지만, 흐름을 한눈에

 

이해할 수 있어서 초보자에겐 더 유용하지 않을까?라는 생각이 들었다.

 

Comments