AWS/S3

Presigned URL을 이용하여 이미지 업로드할 때 용량 제한하기

Cog Factory 2022. 4. 20. 17:41

백엔드(NestJS)

import AWS from 'aws-sdk';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';

import { GetPreSignedUrlQueryDTO, GetPreSignedUrlResDTO } from '@dtos';
import AwsConfig from '@config/variables/aws.config';

@Injectable()
export class UploadService {
  constructor(
    @Inject(AwsConfig.KEY)
    private readonly awsConfig: ConfigType<typeof AwsConfig>
  ) {}

  private readonly s3 = new AWS.S3({
    accessKeyId: this.awsConfig.accessKey,
    secretAccessKey: this.awsConfig.secretAccessKey,
    region: this.awsConfig.region,
    signatureVersion: 'v4'
  });

  private readonly bucket = this.awsConfig.s3Bucket;

  async getPreSignedUrl(
    input: GetPreSignedUrlQueryDTO
  ): Promise<GetPreSignedUrlResDTO> {
    try {
      const { fileName } = input;

      const params = {
        Bucket: this.bucket,
        Conditions: [
          { bucket: this.bucket },
          ['content-length-range', 1048576, 10485760]
        ],
        Expires: 60 * 30,
        Fields: {
          key: fileName
        }
      };

      const { fields, url } = await this.s3.createPresignedPost(params);

      return {
        url,
        fields
      };
    } catch (err) {
      throw new BadRequestException(err);
    }
  }
}

여기서 이용할 method는 new AWS.S3().createPresignedPost() 다. AWS 설정 값을 주고 Conditions['content-length-range', 1048576, 10485760] 를 옵션을 준다. 두 번째 인자에 파일의 Minimum 크기, 세 번째 인자에 Maximum 크기를 준다.

await this.s3.createePresignedPost(params)를 요청하면 fields와 url 값을 받는데, 이 값을 프론트에게 reponse로 전달한다.

프론트엔드(React)

export const Form = () => {
  const [selectedFile, setSelectedFile] = useState<File>(null);

  const uploadService = new UploadService();

  const onSubmit = async (e) => {
    const fileNameSplit = selectedFile.name.split('.');
    const fileExt = fileNameSplit.pop();

    const processedFileName = getS3KeyPath({
      intialFolder: 'initial',
      id: 33,
      fileExt
    });

    const { url, fields } = await uploadService.getPreSignedUrl({
      fileName: processedFileName
    });

    try {
      await uploadService.uploadToS3({ url, fields }, { file: selectedFile });
    } catch (error) {
      console.error(error);
      alert('이미지 크기는 최대 10mb 이하입니다.');
      throw new Error('이미지 크기는 최대 10mb 이하입니다.');
    }
  };
  
  const handleFileInput: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    const files = e.target.files;
    setSelectedFile(files[0]);
  }

  return <input type="file" className="input" onChange={handleFileInput} />
}

여기서는 다 무시하고 봐야할 것은 uploadService 부분의 getPreSignedUrl 메소드와 uploadToS3 메소드다. getPreSignedUrl()로 서버로부터 url과 fiels 값을 받는다. 해당 값과 uploadToS3()를 이용해서 s3에 파일을 업로드한다.

uploadToS3 코드

 async uploadToS3(
    { url, fields }: UploadToS3UrlDTO,
    { file }: UploadToS3BodyDTO
  ): Promise<void> {
    const form = new FormData();
    Object.entries(fields).forEach(([field, value]) => {
      form.append(field, value);
    });
    form.append('file', file);

    await axios.post<any, AxiosResponse<any>, FormData>(url, form);
  }

fiels는 배열 값인데, 값을 줄 때 POST method를 이용하고 form data를 이용해야 한다. 특히 주의할 점은 반드시 key는 file로 파일을 줘야 하고 fields에 있는 값들을 다 form-data에 추가한 후 맨 마지막에 file을 추가해야 한다.
    그렇지 않으면 AWS S3에서 에러를 일으킨다.