Front-end에서 유저가 로컬에서 업로드한 이미지를 저장하는 방법은 여러가지가 있습니다.
Server에 이미지를 저장하는 폴더를 만들어 클라이언트로부터 요청받은 파일을 저장하는 것이 있고,
이미지를 따로 저장하는 서버를 만들어 저장할 수 있습니다.
또한, DB에 Blob 타입으로 저장할 수 있습니다.
위의 경우, 사용하는 서버와 DB의 상태 또는 이를 구동하는 PC의 성능에 따라 퍼포먼스가 달라질 수 있습니다.
그래서, AWS S3 버킷에 이미지 파일을 저장하고, DB엔 그 버킷의 이미지 파일 경로(이미지 주소)를 저장하고, 서버는 이 경로를 클라이언트로 응답합니다.
이렇게 하게 되면 PC의 성능을 고려하지 않아도 되며, 사용한 만큼 비용을 지불하는 것으로 보다 쾌적하게 백엔드를 구성할 수 있습니다.
이를 Multer-S3
와, AWS-SDK
모듈을 사용하여 구현할 수 있습니다.
npm install multer multer-s3 aws-sdk --save
다음과 같이 설치합니다.
백엔드를 구성하기 전에, 클라이언트는 아래와 같이 구현했습니다.
...
function image() {
const requestImg = async (event) => {
// form tag를 사용하지 않아도 formdata를 만들 수 있습니다.
let formData = new FormData();
formData.append('image', event.target.files[0]);
// 생성한 폼 데이터에 파일 객체를 할당하고, 서버에 요청을 보냅니다.
try {
const imageRes = await axios.post(`localhost:4000/image`, formData);
} catch (error) {
console.log(error);
alert('server error');
}
};
return (
<Wrap>
<div id='imageEdit'>
<input
type='file'
id='image_uploads'
name='image'
accept='image/*'
onChange={requestImg}
></input>
</div>
</Wrap>
);
}
...
input
태그의 type
은 file
이 되어야하며, 이미지 타입의 파일만 선택하기 위해 accept
속성을 추가했습니다. 업로드 되면, onChange
이벤트가 발생하여 requestImg
함수가 실행되어 서버로 axios
요청이 가게 됩니다.
백엔드에선 클라이언트로부터 요청받은 파일을 처리해야합니다. 그 전에, Route
설정 및 AWS-SDK
를 설정해야합니다.
{
"accessKeyId": "YOUR AWS ACCESS KEY",
"secretAccessKey": "YOUR AWS ACCESS SECRET KEY",
"region": "ap-northeast-2"
}
위 json
파일의 첫번째와 두번째는 AWS에서 할당받은 액세스 키와 액세스 암호 키 입니다. 세번째는 S3 버킷을 생성한 지역입니다. 보통 서울 리전에서 생성하므로, ap-northeast-2
가 되겠습니다.
이 액세스 키는 AWS 콘솔의 IAM 메뉴에서 생성할 수 있습니다.
IAM 메뉴로 이동하고, 사용자 탭에서 사용자 추가를 선택합니다.
이름은 자유롭게 짓고, 자격 증명 유형은 액세스 키를 선택하고 다음
을 누릅니다.
다음으로, 기존 정책 직접 연결
을 선택하고, AmazonS3FullAccess
를 선택하고 다음
을 누릅니다.
세번째인 키, 값
태그 설정은 필요하지 않다면 할 필요 없이 다음
으로 이동합니다.
다음
을 누르고, 이제 사용자 만들기
를 클릭하여 사용자를 생성합니다.
이제 생성하면 액세스 키 ID와 액세스 암호 키를 부여받습니다. 이를 개인적으로 메모해두고, 외부엔 절대로 노출되어선 안됩니다.
코드 상으론 환경변수로 관리하거나, gitignore
로 블락처리 해야합니다. 공개될 경우, 폭탄 과금의 우려가 있다고 하네요...
이렇게 만들어진 키를 위 s3.json
에 입력합니다.
저는 modules
라는 폴더를 새로 만들고, 위와 같은 이름의 파일을 만들었습니다.
이 파일은 IAM 액세스 키와 비밀 키를 가지고 유저의 S3 버킷에 접근하고, 업로드 하기 위한 로직을 가진 파일입니다.
const multer = require('multer');
const multerS3 = require('multer-s3');
const aws = require('aws-sdk');
aws.config.loadFromPath(__dirname + '/../config/s3.json');
const s3 = new aws.S3();
const upload = multer({
storage: multerS3({
s3: s3,
bucket: 'YOUR BUCKET NAME',
acl: 'public-read',
contentType: multerS3.AUTO_CONTENT_TYPE,
key: function (req, file, cb) {
cb(null, `${Date.now()}_${file.originalname}`);
},
}),
});
module.exports = upload;
이 파일의 upload
라는 함수가 실행되면, 설정한 이름의 Bucket에 파일을 업로드할 수 있습니다.
key
속성은 업로드하는 파일이 어떤 이름으로 버킷에 저장되는가에 대한 속성입니다.
위 속성대로라면, 버킷에 업로드되는 파일(객체)의 이름은 현재 시각_유저가 업로드한 파일의 이름.이미지 확장자
가 됩니다.
구현이 모두 완료되면, 위와 같이 버킷에 담기는 것을 확인할 수 있습니다.
이제 routes
로 분기하고, multer.js
에 작성한 upload
함수를 routes
에 담아야 합니다.
const express = require('express');
const router = express.Router();
const upload = require('../modules/multer');
const { controller } = require('../controllers');
router.post('/image', upload.single('image'), controller.image.post);
module.exports = router;
multer.js
에서 export
하는 upload
를 가져오고, 이미지 저장을 처리하는 routes
의 두번째 인자로 전달합니다.
여기서 두번째 인자인 upload.single()
의 파라미터로는 Front-End에서 Formdata
에 append
할 때의 키 이름으로 설정해야합니다.
이제 Front-End로부터 요청받은 파일을 S3 버킷에 저장하고, DB에 이 버킷의 파일 경로를 저장합니다. 그리고 응답으로 이 경로를 클라이언트로 응답하면 끝입니다!
const { img } = require('../../models');
module.exports = {
post: async (req, res) => {
...
img.imageURL = req.file.location;
await img.save();
res.status(200).json({ img: req.file.location });
},
};
클라이언트에서 이미지를 업로드하고, 서버로 페이로드에 폼데이터를 담아 보내면, 유저의 AWS S3 버킷에 업로드한 파일(객체)를 저장합니다. 이렇게 저장된 이미지의 메타데이터를 req.file
로 받을 수 있습니다.
req.file
은 객체이며, 이 객체의 속성 중, S3 버킷에 저장된 이미지 파일의 경로(이미지 주소)가 req.files.location
이라는 키로 전달됩니다.
이 경로를 DB에 저장하고, 서버는 이 이미지 주소를 응답하면, Front-End에서 이 이미지를 핸들링할 수 있게 됩니다.
프로젝트를 진행하면서 느꼈던 다양한 어려움 중에 가장 막막하게 다가왔던 어려움들이 있습니다.
사용할 땐 대수롭지 않게 너무 당연하게 사용했던 기능들이 직접 구현하려니 너무 어려웠다는 것이었습니다...
그 중에 하나가 바로 이미지를 업로드하고, 이를 어떻게 핸들링할까... 서버에 저장할까? DB에 저장할까? 너무 많은 고민이었고, 어려움이었습니다.
수많은 구글링과 삽질 끝에, multer-s3
와 aws-sdk
를 적극 사용하기로 했고, 성공적으로 구현하여 굉장히 뿌듯했습니다!
제 글이 이 기능을 구현하기 위해 구글링하는 다른 분들에게 많은 도움이 되길 바라며 이 글을 썼습니다. 긴 글 읽어주셔셔 감사드립니다! 😆
preSignUrl을 사용해보세요~