미술 작품 거래 서비스(Decouvrir) 프로젝트를 할 때, 유저가 자신의 작품을 판매하기 위해 직접 상품을 등록할 수 있는 기능이 필요했다. 그 기능의 포인트는 이미지를 첨부파일로 받아서 DB에 상품 이름, 가격 등과 함께 저장하는 로직이었는데, 이를 위해서
- AWS S3에 이미지를 업로드
- 이미지 url을 클라이언트로 일단 응답
- 클라이언트에서는 그 경로와 함께 다른 텍스트 정보들을 서버에 전달
- 서버에서 상품 DB에 정보를 저장
위와 같은 방법으로 최종 정리되었다.
여러 서칭을 통해 AWS S3 버킷 세팅을 마쳤고 multer-s3를 이용해 파일을 업로드 하기 위한 imageRouter의 코드는 아래와 같이 작성했다.
import dotenv from "dotenv";
import AWS from "aws-sdk";
import multerS3 from "multer-s3";
import multer from "multer";
import { Router } from "express";
// loads .env file contents into process.env
dotenv.config();
const imageRouter = Router();
// aws s3 설정
AWS.config.update({
region: process.env.AWS_REGION,
apiVersion: "latest",
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
});
const s3 = new AWS.S3();
// multer를 이용해 이미지를 업로드하는 함수
const imageUpload = multer({
storage: multerS3({
//저장 공간 정보
s3: s3,
bucket: "decouvrir",
key: function (req, file, cb) {
// 확장자가 올바른지 확인
const extention = file.mimetype.split("/")[1];
if (!["png", "jpg", "jpeg", "gif", "bmp"].includes(extention)) {
return cb(new Error("이미지 파일을 등록해주세요."));
}
// 파일 이름 지정
cb(null, `${Date.now()}_${file.originalname}`);
},
}),
acl: "public-read",
});
// post 요청이 들어왔을 때
imageRouter.post(
"/upload",
// 하나의 이미지 파일 업로드
imageUpload.single("image"),
async (req, res, next) => {
try {
res.json({
// 이미지 경로를 res로 보내줌
message: "이미지 저장 성공",
imagePath: req.file.location,
});
} catch (err) {
next(err);
}
}
);
export { imageRouter, imageUpload };
이렇게 작성을 하고 postman을 통한 테스트는 완료되었는데, 프론트에서 API 요청을 보내면 계속 오류가 나는 것이다ㅠㅠ 함참동안 뭐가 잘못되었을까 고민했는데, 결국 문제점을 찾아냈다.
function addProduct(e) {
e.preventDefault();
const productName = productNameInput.value;
const content = contentInput.value;
const price = priceInput.value;
const category = categoryInput.value;
const formData = new FormData();
formData.append("image", photoFile.files[0]); // 파일 첨부
fetch(`/api/images/upload`, {
method: "POST",
body: JSON.stringify(formData),
})
.then((response) => response.json())
.then((data) => {
image = data.imagePath;
const newProductData = {
painterEmail,
painterName,
productName,
price,
content,
category,
categoryId,
image,
};
Api.post(`/api/product`, newProductData);
alert("상품 등록이 완료되었습니다!");
});
}
postBtn.addEventListener("click", addProduct);
imageRouter로 POST 요청을 보낼 때 body
를 그냥 formData
로 했어야 했는데 JSON.stringify(formData)
로 보내고 있었던 것이 원인이었다😭 내가 생성해준 formData 객체를 그냥 보내줬으면 됐는데 그걸 stringify 했으니 제대로 업로드되지 않았던 것.. 형식이 다른 데이터를 어떻게 보내야 하는지에 대한 고민이 부족했던 것이 느껴져서 많이 반성했다. (이렇게 간단한 걸 며칠동안 붙잡고 있었다니..)
더 알아보니 요청 헤더에 담기는 정보인 Content-type 중 multipart
자체가 폼에 이미지와 같은 파일과 제목, 설명과 같은 텍스트처럼 서로 다른 형식의 데이터가 함께 서버로 전달되어야 할 때 사용할 수 있는 타입이기 때문에 내가 했던 것 처럼 api 요청을 두 번 보낼 필요 없이 formData에 모든 정보를 담아서 한 번에 전달할 수 있다고 한다. 일단 S3에 이미지를 올리는 건 생각하지 말고, formData를 활용하는 방법을 더 익혀야겠다고 생각했다. (이미지가 다뤄지지 않는 웹서비스는 드물기 때문에 더더욱!💪)
참고 자료