이번 프로젝트에서도 이미지 업로드를 구현하게 되었다. 이전에 프로젝트에서는 백엔드에 formData를 전달해 백엔드가 S3로 업로드하는 방식으로 진행했었다. 그렇게 진행한 이유로는 잘 몰라서가 가장 크다. 그때 당시에는 AWS에 대해서 아예 무지했기 때문에 팀원 분들께서 이미지 업로드 서버가 따로 있어 해당 부분에 업로드를 해야한다 정도만 알려주셨기 때문에 그럼 당연히 백엔드가 업로드를 해주어야겠구나. 생각을 했었다. 이제 그 이미지 업로드를 S3에 한다는 것을 알고 프론트는 S3에 배포를 하는 것을 알았기 때문에 그러면 충분히 프론트엔드도 이미지 업로드가 가능한 거 아닐까? 싶어서 찾아보니 역시나 가능했던 것이다. 그래서 원래 계획은 백엔드에서 이미지 파일을 전달받고 업로드하는 것이었지만, 이번에는 프론트엔드에서 이미지 파일을 업로드하는 것으로 수정하게 되었다.
구현하는 방법은 프론트엔드에서 온전히 업로드를 담당하는 것과 백엔드로부터 통신한 다음 프론트엔드가 업로드를 하는 방법이 있다. 백엔드와 통신하지 않고 프론트엔드에서 업로드하기 위해 해당 방법으로 바꾼 것이기 때문에 의문이 들 수 있다. 하지만, 클라이언트는 보안에 상대적으로 취약하기 때문에 프론트에서만 이미지 업로드를 담당하기엔 이슈가 있다. 프론트에서 이미지 업로드를 전부 구현하는 부분에 대해서는 다루지 않고, 백엔드로부터 통신한 뒤 업로드하는 방법에 대해 정리한다.
클라이언트가 직접 S3에 접근하려면, AWS Access Key와 Secret Key 등의 자격 증명을 가지고 있어야 한다. 즉, 안전하게 S3에 원하는 파일만 업로드하기 위해서는 PresignedURL을 사용하면 된다. S3 버킷에 대한 직접적인 쓰기 권한을 사용자가 가지지 않고, 서버에서 발급한 제한된 시간 동안만 PresignedURL을 통해 업로드가 가능하다. 즉, 사용자가 파일을 업로드할 수 있는 기간과 업로드 가능한 파일의 범위가 제한된다.

위와 같은 순서로 이미지 업로드를 진행하게 되는 것이다.
const hanldeImage = async (e) => {
if (!e.target.value) return;
const image = e.target.files[0];
const validMimeTypes = ['image/png', 'image/jpeg']; // PNG, JPG, JPEG MIME 타입
const maxSizeMB = 10; // 10MB
if (!validMimeTypes.includes(image.type)) {
alert('프로필 이미지는 png, jpg/jpeg 파일만 가능합니다.');
return;
}
if (image.size > maxSizeMB * 1024 * 1024) {
alert('프로필 이미지는 10MB 이하의 파일만 가능합니다.');
return;
}
const imageName = uuidv4() + image.name; // 파일 이름 중복되지 않기 위함
try {
let response = await axios.post(
`${process.env.NEXT_PUBLIC_BASE_URL}/image`,
{ fileName: imageName }
);
const uploadUrl = response.data.uploadUrl;
try {
response = await axios.put(uploadUrl, image, {
headers: {
'Content-Type': image.type,
},
});
const imageUrl = `https://${process.env.NEXT_PUBLIC_IMAGE_BUCKET}.s3.amazonaws.com/${imageName}`;
setModifyInfo({ profileImage: imageUrl });
} catch (error) {
// 에러 처리
}
} catch (error) {
// 에러 처리
} finally {
e.target.value = ''; // 같은 이미지 연속으로 업로드 가능하도록 값을 비움
}
};
해당 코드는 파일을 불러온 다음 실행되는 코드이다. 해당 파일의 type과 size가 조건에 만족할 경우, 업로드될 이미지 파일의 이름을 uuid+name으로 변경해준다. 파일명은 중복될 가능성이 매우 높기 때문에 중복을 피하기 위해 uuid를 붙여주어야 한다. /image로 변경된 이미지 파일명을 전달한다. 이에 대한 반환값인 URL이 업로드 가능한 주소가 되는 것이다. 해당 주소로 put 요청과 함께 이미지 파일을 보내주면 S3에 이미지 파일이 업로드가 되게 된다.
업로드된 이미지를 렌더링하기 위해서는 이미지 URL이 필요하다. 이미지 URL은 따로 반환되지는 않는다. 찾아보니 해당 URL을 직접 받아오기 위해서는 파일을 어디에 저장을 시킬지 백엔드와 사전에 결정해주어야 한다. 우리는 따로 폴더를 구분하지 않기로 했기 때문에 이미지 URL은 https://{버킷이름}.s3.amazonaws.com/{이미지파일이름}이 될 것이다. 만약 특정 폴더로 하게 되면 /{특정폴더}/{이미지파일이름}와 같은 식으로 될 것이다.
이미지 URL을 클릭했을 때 AccessDenied 에러가 발생할 수 있다. 그때는 버킷 정책과 CORS 설정을 아래와 같이 수정해주면 된다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::<버킷이름>/*"
}
]
}
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"http://localhost:3000"
],
"ExposeHeaders": []
}
]