S3 presigned url 을 통한 사진 업로드

이경택·2023년 8월 19일

백엔드

목록 보기
4/5
post-thumbnail

S3 PresignedUrl

presigned url 이란?

presigned url 은 단어 그대로 미리 서명된 url을 받는 것이다.

AWS에서 제공하는 S3 스토리지에 Bucket Policy나 acl에 관계없이 자격증명이 된 사용자가 접근할 수 있도록 url을 만들어 제공하는 것이다.

presigned url이 필요한 이유?

일단 S3 스토리지에 올라가는 프로세스를 확인 해 보자.

  1. 프론트에서 file 업로드 (백엔드에게 파일 전달)
  2. 백엔드에서 파일 받아서 예외 처리 후 S3 스토리지에 업로드
  3. S3 스토리지 성공적으로 업로드하면 프론트에 성공 response 응답
  4. 프론트에서 성공 response 받으면 결과 표시

위 과정에서 문제점

  1. 느리다.
  2. 저장하지도 않을 무거운 이미지 파일을 서버로 전송해야 한다.

프로세스

  1. 이미지 업로드 요청 시 서버 api 호출

(나의 경우에는 업로드 요청 시 fileType을 뽑아내서 서버에 요청했다. 파일 타입 검증을 위해)

  1. 서버에서 타입 확인 후 AWS S3에 presigned url 요청
  2. AWS S3에서 presigned url 반환
  3. 서버에서 프론트로 presigned url 전달
  4. 프론트에서 presigned url로 이미지 업로드

프론트 코드

  • 첫 번째 코드
    const addProduct = async () => {
        try {
          if (files) {
            const fileTypes = files.map((file) => {
              return file.type
            })
            const presignedData = await getPresignedUrl(fileTypes, 'product')
    
            const result = await Promise.all(
              files.map((file, idx) => {
    // 새로운 formData를 만들어주고 presigned로 받은 데이터들 추가
                const formData = new FormData()
                const { presigned } = presignedData[idx] //
    
                for (const key in presigned.fields) {
                  formData.append(key, presigned.fields[key])
                  console.log(formData.get(key))
                }
                formData.append('Content-Type', file.type)
                formData.append('file', file)
                return axios.post(presigned.url, formData)
              })
            )
            /**
             * @Todo: 받은 url로 사진 보낼 때 403 에러 해결해야함 -
             * @Done: presigned url 받아오는 것 까진 문제가 없음
             * @Todo: files를 map 돌렸을 때 같은 값이 formData에 들어가고 있다. 왜?
             * @해결: formData 를 map 돌릴때마다 새로 만들어줘야 하는데 map 부분 위에서 만들어서 중복 추가 되는거였다.
             */
          }
        } catch (error) {
          console.error(error)
        }
    첫 번째 방법으로 받아온 presigned 값

첫 번째 presigned 값

  • 두 번째 코드

    const addProduct = async () => {
    	try {
    	  if (files) {
    	    const fileTypes = files.map((file) => {
    	      return file.type
    	    })
    	    const presignedData = await getPresignedUrl(fileTypes, 'product')
    // 아래와 같은 presigned 값의 배열이 오게 되는데 물음표 전 까지의 url만 뽑아내서 쓰면된다
    	    const res = await Promise.all(
    	      files.map((file, idx) => {
    	// url을 split을 통해 뽑아낸다
    	        const url = presignedData[idx].signenUrlPut.split('?')[0]
    	        return axios.put(url, file, {
    	          headers: { 'Content-Type': file.type },
    	        })
    	      })
    	    )
    		}
    	} catch (error) {
          console.error(error)
        }
    }

    두 번째 방법으로 받아온 presigned 값

    "https://kt-first-bucket.s3.ap-northeast-2.amazonaws.com/product/ba3df5b1-5f2e-41aa-9ddd-81f6fcbb2043.png
    ?X-Amz-Algorithm=AWS4-HMAC-SHA256
    &X-Amz-Credential=AKIAXZ4K2VPPHG6IQ5HU%2F20230817%2Fap-northeast-2%2Fs3%2Faws4_request
    &X-Amz-Date=20230817T073135Z&X-Amz-Expires=3600
    &X-Amz-Signature=9f5cfca82c47497316c280c5f884690fcac7c79de352a5baaaf749cc18763c17
    &X-Amz-SignedHeaders=host"

백엔드 코드

  • 첫 번째 방법
    // aws.controller.ts
    @Post('/presignedurl')
    async getPresignedUrl(@Body() { types }: { types: string[] }) {
      return this.awsService.getPresignedUrl(types);
    }
    
    // aws.service.ts
    export class AwsService {
    	private readonly bucketName: string;
    	private readonly s3: S3;
      constructor(private configService: ConfigService) {
        this.bucketName = this.configService.get('AWS_BUCKET_NAME');
    		// s3 환경 설정
        this.s3 = new S3({
          accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
          secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
          region: this.configService.get('AWS_BUCKET_REGION'),
        });
      }
    	// 파일 타입들을 받아서 key를 만들어 createPresigned 함수의 키로 넘겨준다
      async getPresignedUrl(fileTypes: string[]) {
        return Promise.all(
          fileTypes.map(async (fileType) => {
            const extension = fileType.split('/')[1]; // 파일 확장자 ex) png
            const imageKey = `${randomUUID()}.${extension}`; // image 이름
            const key = `product/${imageKey}`; // 경로 및 image 이름
            const presigned = await this.createPresigned(key);
            return { imageKey, presigned };
          }),
        );
    // key를 인수로 받아 Fields에 설정 및 Bucket, 만료 시간, conditions(파일에 대한 정보) 작성
    	async createPresigned(key) {
        return this.s3.createPresignedPost({
          Bucket: this.bucketName,
          Fields: {
            key,
          },
          Expires: 60 * 60,
          Conditions: [
            ['content-length-range', 0, 20 * 1000 * 1000], // 0 ~ 20MB
            ['starts-with', '$Content-Type', 'image/'],
          ],
        });
      }
    }
  • 두 번째 방법
async getPresignedUrl(fileTypes: string[], bucket: string) {
	const result = await Promise.all(
	  fileTypes.map(async (fileType) => {
	    const extension = fileType.split('/')[1];
	    const imageKey = `${randomUUID()}.${extension}`;
	    const key = `${bucket}/${imageKey}`;
	    const signenUrlPut = await this.s3.getSignedUrlPromise('putObject', {
	      Bucket: this.bucketName,
	      Key: key,
	      Expires: 60 * 60,
	    });
	    return signenUrlPut;
	  }),
	);
    return result;
}
/**
 *  'getObject': 객체를 가져오는 작업을 나타냅니다. 이 작업은 S3 버킷의 객체를 읽어오는 데 사용됩니다.
    'putObject': 객체를 업로드하는 작업을 나타냅니다. 이 작업은 파일을 S3 버킷에 업로드하는 데 사용됩니다.
    'deleteObject': 객체를 삭제하는 작업을 나타냅니다. 이 작업은 S3 버킷에서 객체를 삭제하는 데 사용됩니다.
    'listObjects': 객체 목록을 나열하는 작업을 나타냅니다. 이 작업은 S3 버킷 내의 객체들의 목록을 가져오는 데 사용됩니다.
    'copyObject': 객체를 복사하는 작업을 나타냅니다. 이 작업은 S3 버킷 내에서 객체를 복사하는 데 사용됩니다.
//  */
profile
한 줄로 소개 할 수 없는 개발자

0개의 댓글