프로젝트에서 사용자가 각자 직접 이미지 파일을 업로드하고 수정할 수 있는 게시물 업로드 방식을 구현하는데 s3를 사용했다. aws s3(Simple Storage Service)는 aws에서 제공하는 데이터를 저장할 수 있는 스토리지이다.
S3의 장점
- 높은 확장성(추후 cloudFront와 함께 사용할 수 있다), 안전성, 보안성(SSL을 통한 암호화)
- 빠른 속도(버킷을 생성할 때 지역을 선택하며 멀티 파트 업로드를 지원한다.)
- 저렴한 비용!
이 부분은 구글링하면 금방 나오는 부분이라 버킷 정책 결과만 첨부한다.
{
"Version": "2012-10-17",
"Id": "Policy1676469516841",
"Statement": [
{
"Sid": "Stmt1676469515636",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:DeleteObject",
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::panda-products/*"
}
]
}
s3에 파일을 업로드할 수 있는 방법은 여러가지이다.
나는 처음에 당연히 1번 방식을 생각했는데 사실 FormData를 서버로 넘기는? 부분에서 어려움을 겪어서🥲 다른 방법을 찾다가 두번째,세번째 방식도 알게되었다.
Presigned URL은 미리 서명된 권한으로 잠시 접근가능하게 해주는 임시 url을 발급해주는 것이다. 파일에 접근하고 업로드(GET, POST)등 HTTP메소드를 사용할 수 있고 발급된 url의 만료기간을 설정할 수 있다.
파일 업로드 절차
나는 Next js와 프리즈마 orm을 사용중이기 때문에 프리즈마 블로그에서 찾은 ‘Next js + prisma + typescript 이미지 업로드’부분을 참고했다! 여기서 나온 방식은 Presigned-POST방식이다.
Presigned POST방식은 Presigned URL하고는 약간 다른데 POST메서드만 사용할 수 있어서 업로드만 가능하다는 점이 다르다. 구현과정에서 우여곡절이 많았지만 업로드 성공한 코드를 올려본다.
// server
import { NextApiRequest, NextApiResponse } from "next";
import { S3Client } from "@aws-sdk/client-s3";
import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
import { credentials } from "../../../lib/credentials";
const uploadHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") {
try {
const s3 = new S3Client({
credentials: {
accessKeyId: credentials.accessKey,
secretAccessKey: credentials.secretKey,
},
region: credentials.region,
});
const post = await createPresignedPost(s3, {
Bucket: "panda-products",
Key: req.query.file,
Conditions: [
["content-length-range", 0, 50 * 1000 * 1000], // 0 ~ 50MB
["starts-with", "$Content-Type", "image/"], // only image],
],
Fields: {
acl: "public-read",
},
Expires: 600,
});
res.status(200).json(post);
} catch (err) {
res.status(400).json({message: 'error'})
}
}
};
export default uploadHandler;
createPresignedPost
가 presigned url을 발급해주는 메서드인데 게시물에서는 따로 패키지 설치에 대한 내용은 나와있지 않아서 그냥 쓰니까 오류가 나길래 찾아서 설치해주었다.
s3-presigned-post 패키지 설치
npm i @aws-sdk/s3-presigned-post
// client
const uploadImage = async (file: File, path: string) => {
const encodedName = Buffer.from(file.name).toString("base64");
const ext = file.type.split("/")[1];
const key = `${path}/${encodedName}.${ext}`;
const { data } = await axios.post(`/api/image?file=${key}`);
const formData = new FormData();
Object.entries(data.fields).forEach(([field, value]) => {
formData.append(field, value);
});
formData.append("Content-Type", file.type);
formData.append("file", file);
try {
await axios.post(data.url, formData);
} catch (err) {
console.log(err);
}
};
응답받은 데이터의 형태
클라이언트에서 바로 s3로 업로드하려면 CORS설정이 꼭 필요하다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"x-amx-server-side-encryption",
"x-amz-request-id",
"x-ams-id-2"
],
"MaxAgeSeconds": 3000
}
]
이렇게 써주었는데 이 부분은 조금씩 다른것 같으니 각자 맞는 방식으로 수정해야 한다.
Presigned url방식보다 더 간단한 방법으로 서버쪽 코드가 아예 없어도 된다. AWS SDK는 AWS 리소스에 액세스하기 위한 공식 소프트웨어 개발 키트로 자바스크립트 API를 제공한다. SDK를 사용하면 클라이언트측에서 직접 AWS서비스와 상호작용할 수 있고 SDK함수를 사용해서 파일을 업로드할 수 있다.
클라이언트에서 직접 상호작용하기 위해 액세스키 같은 자격 증명이 필요한데 노출되면 안되는 정보이기 때문에 환경변수 파일에 추가해서 관리해야한다.
aws-sdk패키지 설치
npm i aws-sdk
자격 증명 구성
NEXT_PUBLIC_AWS_KEY=MY_ACCESS_KEY_ID
NEXT_PUBLIC_AWS_SECRET=MY_SECRET_ACCES_KEY
NEXT_PUBLIC_AWS_REGION=MY_REGION
자격 증명은 AWS I AM 서비스 보안자격증명에서 생성할 수 있다.
(NEXT_PUBLIC은 서버, 브라우저 모두에서 환경변수를 사용하기 위해서 붙여주어야 한다.)
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
const s3Client = new S3Client({
region: processs.env.NEXT_PUBLIC_AWS_REGION,
credentials: {
accessKeyId: processs.env.NEXT_PUBLIC_AWS_KEY,
secretAccessKey: process.env.NEXT_PUBLIC_AWS_SECRET,
},
});
const uploadImage = async (file: File, path: string) => {
const encodedName = Buffer.from(file.name).toString("base64");
const ext = file.type.split("/")[1];
const key = `${path}/${encodedName}.${ext}`; // 경로(path)는 버킷이름!
const bucketParams = {
Bucket: "panda-products",
Key: key,
Body: file,
ContentType: "image/jpeg", // 지정하지 않으면 브라우저창에서 열지않고 다운로드 받는다!
ACL: "public-read",
};
try {
const response = await s3Client.send(new PutObjectCommand(bucketParams));
} catch (err) {
console.log("Error", err);
}
}
PutObjectCommand
가 버킷에 객체를 업로드하는 메서드이다.
key에 파일명을 인코딩해서 넣은 이유는 이미지가 저장될 때 파일명이 동일하면 기존 파일에 덮어써지기 때문이다. 파일명이 고유해야 하기 때문에 base64형식으로 변환해서 사용했다. (문서에서는 uuid를 활용해서 key를 지정하라고 안내되어있다.)
이렇게해서 s3에 업로드를 하고 프리즈마 쪽에도 이미지url을 문자로 저장해서 이미지를 가져올때 url로 접근할 수 있도록 했다.
export const createImageUrl = (file: File, path: string) => {
const ext = file.type.split("/")[1];
const encodedName = Buffer.from(file.name).toString("base64");
const key = `${path}/${encodedName}.${ext}`;
return`${process.env.NEXT_PUBLIC_AWS_BUCKET_NAME}/${key}`;
};
이렇게보니 짧지만 사실 presigned 방식을 구현하는 중에 계속 오류가 많이 생겼어서 이미지 업로드만 며칠 붙잡고있었다.. sdk방식을 찾아보니 자격 증명 설정해야하는것 말고 딱히 단점을 찾지 못해서 지금은 업로드기능만 구현했지만 수정, 삭제 할때도 제공되는 메서드를 사용하면 되지 않을까싶다.
Fullstack App With TypeScript, PostgreSQL, Next.js, Prisma & GraphQL: Image upload
AWS SDK for JavaScript
s3-presigned-post패키지 설치