[Flutter] Presigned URL 이용해서 S3 버킷에 이미지 업로드하기

[Flutter] S3 Presigned URL 발급받기 | AWS Lambda & API Gateway

Presigned URL을 발급 반는 과정은 이전 게시글에서 확인할 수 있다. 이제 Presigned URL을 사용해서 최종적으로 S3 버킷에 이미지를 업로드해 보자!

 

image_picker로 이미지 선택하기

final ImagePicker picker = ImagePicker(); // ImagePicker 초기화
XFile? image; // 사용자가 선택한 이미지
String contentType = ''; // 이미지 파일의 타입
bool isImageUpdated = false; // 이미지 업로드 여부

Future getImage(ImageSource imageSource) async {
  final XFile? pickedFile = await picker.pickImage(source: imageSource);

  if (pickedFile == null) {
    return;
  }

  setState(() {
    image = XFile(pickedFile.path);
    contentType = lookupMimeType(pickedFile.path)
    isImageUpdated = true;
  });
}

우선 사용자가 올린 이미지를 image_picker를 통해 가져오고, Presigned URL을 얻고, 이 Presigned URL을 사용해서 S3에 이미지를 업로드하는 흐름으로 진행된다.

 

 

S3 버킷에 이미지 업로드하기

final bytes = await _image!.readAsBytes();

// S3 버킷에 이미지 업로드 요청
final uploadImageResponse = await http.put(
  Uri.parse(presignedUrl),
  headers: {
    'Content-Type': _contentType,
  },
  body: bytes,
);

Presiend URL에 `Content-Type` 헤더, 이미지 바이트와 함께 `put` 요청을 한다. 이 http 요청이 responstype `200`을 반환하면 이미지 업로드에 성공한 것이다.

 

 

업로드한 이미지 URL 얻기

final bucketUrl = "https://test.s3.ap-northeast-2.amazonaws.com";
String objectKey = ''; // Presigned URL 생성 시 filename 저장 예정
final String objectUrl = '$bucketUrl/$objectKey';

S3 버킷에 업로드한 이미지 URL은 http 응답에서 제공되지 않기 때문에 직접 생성해야 한다. 하지만 정해진 형식이 있기 때문에 어렵지 않게 만들 수 있다.

`bucketUrl`은 `"https://test.s3.ap-northeast-2.amazonaws.com"`에서 `test` 부분을 자신의 버킷 이름으로 바꿔주면 된다. `objectKey`는 Presigned URL을 생성할 때 쿼리 파라미터로 전달했던 `filename`을 그대로 사용하면 된다. 이 두 가지를 조합해서 이미지의 URL이 생성된다.

 

 

403 에러 해결: S3 버킷 권한 설정하기

HTTP request failed, statusCode: 403

지금 상태로 S3에 이미지를 업로드한다면 아마 다음과 같은 에러를 마주할 것이다. 이를 해결하기 위해 버킷 권한 탭에서 두 가지를 설정해주어야 한다.

 

먼저 버킷 정책을 편집한다. 버킷 정책 편집 페이지에서 정책 생성기 버튼을 클릭한다.

 

  • Select Type of Policy: `S3 Bucket Policy`
  • Effect: `Allow`
  • Principal: `*`
  • Actions: `GetObject`, `PutObject` 2개 선택
  • Amazon Resource Name (ARN): 버킷 정책 페이지에서 `버킷 ARN` 복사

 

항목을 모두 입력한 후 `Add Statement` 버튼을 클릭하면 정책을 최종적으로 확인할 수 있고, 모두 올바르게 입력했다면 `Generate Policy` 버튼을 클릭하고 화면에 뜨는 JSON 코드를 복사한다. 복사한 JSON 코드를 버킷 정책 페이지에 붙여 넣고 변경 사항을 저장한다.

 

그리고 추가적으로 CORS까지 설정해 주었다.

 [
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "GET",
            "PUT"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

 

 

전체 코드

Future getImage(ImageSource imageSource) async {
  final XFile? pickedFile = await picker.pickImage(source: imageSource);

  if (pickedFile == null) {
    return;
  }

  setState(() {
    image = XFile(pickedFile.path);
    contentType = lookupMimeType(pickedFile.path)
    isImageUpdated = true;
  });
}

Future<void> _getPresignedUrl() async {
  try {
    // Generate a unique filename by appending a UUID
    final String uniqueFilename =
        '${const Uuid().v4()}.${_contentType.split('/').last}';
    final String filename = Uri.encodeComponent(uniqueFilename);

    final url = Uri.parse(
        'https://qznvjmzymb.execute-api.ap-northeast-2.amazonaws.com/dev/getPresignedUrl?filename=$filename&contentType=$_contentType');
    final response = await http.get(url); // AWS API에 Presigned URL 만드는 요청

    // decode the top-level JSON response
    final Map<String, dynamic> responseData = json.decode(response.body);

    if (responseData['statusCode'] == 200) {
      // decode the nested 'body' field which is also a JSON string
      final Map<String, dynamic> bodyData = json.decode(responseData['body']);
      final String presignedUrl = bodyData['url'];

      _objectKey = filename; // 이미지 URL 얻을 때 필요한 filename

      await _uploadImageToS3(presignedUrl);
      print('Succeeded to get presigned URL: $presignedUrl');
    } else {
      print('Failed to get presigned URL: ${response.body}');
    }
  } catch (e) {
    print('Failed to get presigned URL: $e');
  }
}


Future<void> _uploadImageToS3(String presignedUrl) async {
  try {
    final bytes = await _image!.readAsBytes();

	// S3 버킷에 이미지 업로드 요청
    final uploadImageResponse = await http.put(
      Uri.parse(presignedUrl),
      headers: {
        'Content-Type': _contentType,
      },
      body: bytes,
    );

    if (uploadImageResponse.statusCode == 200) {
      // Construct the object URL based on the bucket URL and the object key
      final String objectUrl = '$bucketUrl/$_objectKey';
      await _createOrUpdateData(objectUrl);
    } else {
      print('Failed to upload a image: ${uploadImageResponse.body}');
    }
  } catch (e) {
    print('Failed to upload a image: $e');
  }
}

 

 

참고 문서