AWS 시작하기

iOS 애플리케이션 구축

AWS Amplify를 사용하여 단순한 iOS 애플리케이션 생성

모듈 5: 이미지 저장 기능 추가

이 모듈에서는 앱에서 이미지를 메모에 연결하는 기능과 스토리지를 추가합니다.

소개

이제 메모 앱이 작동하므로 각 메모에 이미지를 연결하는 기능을 추가해보겠습니다. 이 모듈에서는 Amplify CLI와 라이브러리를 사용하여 Amazon S3 기반의 스토리지 서비스를 생성합니다. 마지막으로, 이미지 업로드, 가져오기 및 렌더링을 지원하도록 iOS 앱을 업데이트합니다.

배우게 될 내용

  • 스토리지 서비스 생성
  • IOS 앱 업데이트 - 이미지 업로드 및 다운로드 로직
  • IOS 앱 업데이트 - 사용자 인터페이스

주요 개념

스토리지 서비스 - 이미지, 동영상 등의 파일을 저장하고 쿼리하는 기능은 대부분의 애플리케이션에서 기본적으로 요구됩니다. 이 기능을 구현하는 방법 중 하나는 파일을 Base64로 인코딩하고 문자열로 전송해 데이터베이스에 저장하는 것입니다. 이 방법은 인코딩된 파일의 크기가 원본 바이너리보다 커지고, 작업을 수행하는 데 컴퓨팅 리소스가 많이 소요되며, 적절하게 인코딩하고 디코딩하는 데 따른 복잡성이 가중된다는 단점이 있습니다. 다른 방법으로, 파일 저장용으로 특별히 구축되고 최적화된 스토리지 서비스를 이용할 수 있습니다. Amazon S3와 같은 스토리지 서비스는 이 기능을 최대한 높은 성능으로 쉽고 저렴하게 구현하기 위해 제공됩니다.

 완료 시간

10분

 사용되는 서비스

구현

  • 이미지 스토리지 기능을 추가하려면 Amplify 스토리지 카테고리를 사용합니다.

    amplify add storage
    
    ? Please select from one of the below mentioned services: accept the default Content (Images, audio, video, etc.) and press enter
    ? Please provide a friendly name for your resource that will be used to label this category in the project: type image and press enter
    ? Please provide bucket name: accept the default and press enter
    ? Who should have access: accept the default Auth users only and press enter
    ? What kind of access do you want for Authenticated users? select all three options create/update, read, delete using the space and arrows keys, then press enter
    ? Do you want to add a Lambda Trigger for your S3 Bucket? accept the default No and press enter

    잠시 후 다음 메시지가 표시됩니다.

    Successfully added resource image locally
  • 방금 생성한 스토리지 서비스를 배포하려면 터미널로 이동하여 다음 명령을 실행합니다.

    amplify push

    Y를 눌러 확인합니다. 잠시 후 다음 메시지가 표시됩니다.

    ✔ Successfully pulled backend environment amplify from the cloud.
  • 코드 작업을 실행하기 전에 Amplify 스토리지 라이브러리를 프로젝트의 종속성에 추가합니다. Podfile 파일을 열고 AmplifyPlugins/AWSS3StoragePlugin을 사용하여 해당 줄을 추가하거나 아래의 전체 파일을 복사해 붙여 넣습니다.

    # you need at least version 13.0 for this tutorial, more recent versions are valid too
    platform :ios, '13.0'
    
    target 'getting started' do
      # Comment the next line if you don't want to use dynamic frameworks
      use_frameworks!
    
      # Pods for getting started
      pod 'Amplify', '~> 1.0'                             # required amplify dependency
      pod 'Amplify/Tools', '~> 1.0'                       # allows to call amplify CLI from within Xcode
    
      pod 'AmplifyPlugins/AWSCognitoAuthPlugin', '~> 1.0' # support for Cognito user authentication
      pod 'AmplifyPlugins/AWSAPIPlugin', '~> 1.0'         # support for GraphQL API
      pod 'AmplifyPlugins/AWSS3StoragePlugin', '~> 1.0'   # support for Amazon S3 storage
    
    end

    터미널에서 다음 명령을 실행합니다.

    pod install

    이 명령은 완료하는 데 다소 시간이 걸립니다. 다음 명령이 표시됩니다. 실제 버전 번호는 다를 수 있습니다.

    Analyzing dependencies
    Downloading dependencies
    Installing AWSS3 (2.14.2)
    Installing AmplifyPlugins 1.0.4
    Generating Pods project
    Integrating client project
    Pod installation complete! There are 5 dependencies from the Podfile and 12 total pods installed.
  • Xcode로 돌아가 Backend.swift를 열고 Amplify 초기화 시퀀스의 private init() 메서드에 줄을 추가합니다. 완성된 코드 블록은 다음과 같습니다.

    // initialize amplify
    do {
       try Amplify.add(plugin: AWSCognitoAuthPlugin())
       try Amplify.add(plugin: AWSAPIPlugin(modelRegistration: AmplifyModels()))
       try Amplify.add(plugin: AWSS3StoragePlugin())
       try Amplify.configure()
       print("Initialized Amplify");
    } catch {
       print("Could not initialize Amplify: \(error)")
    }
  • Backend.swift를 엽니다. backend 클래스의 아무 위치에나 다음 메서드를 추가합니다.

    // MARK: - Image Storage
    
    func storeImage(name: String, image: Data) {
    
    //        let options = StorageUploadDataRequest.Options(accessLevel: .private)
        let _ = Amplify.Storage.uploadData(key: name, data: image,// options: options,
            progressListener: { progress in
                // optionlly update a progress bar here
            }, resultListener: { event in
                switch event {
                case .success(let data):
                    print("Image upload completed: \(data)")
                case .failure(let storageError):
                    print("Image upload failed: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
            }
        })
    }
    
    func retrieveImage(name: String, completed: @escaping (Data) -> Void) {
        let _ = Amplify.Storage.downloadData(key: name,
            progressListener: { progress in
                // in case you want to monitor progress
            }, resultListener: { (event) in
                switch event {
                case let .success(data):
                    print("Image \(name) loaded")
                    completed(data)
                case let .failure(storageError):
                    print("Can not download image: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
                }
            }
        )
    }
    
    func deleteImage(name: String) {
        let _ = Amplify.Storage.remove(key: name,
            resultListener: { (event) in
                switch event {
                case let .success(data):
                    print("Image \(data) deleted")
                case let .failure(storageError):
                    print("Can not delete image: \(storageError.errorDescription). \(storageError.recoverySuggestion)")
                }
            }
        )
    }
  • 이제 백엔드 함수를 사용할 수 있게 되었으니 API 호출이 반환되면 이미지를 로드해보겠습니다. 이 동작을 추가할 중앙의 위치는 앱이 API에 의해 반환된 NoteData에서 노트 UI를 구성하는 위치입니다.

    ContentView.swift를 열고 노트의 이니셜라이저를 업데이트합니다(8~17번 줄 추가):

    // add a publishable's object property
    @Published var image : Image?
    
    // update init's code
    convenience init(from data: NoteData) {
        self.init(id: data.id, name: data.name, description: data.description, image: data.image)
    
        if let name = self.imageName {
            // asynchronously download the image
            Backend.shared.retrieveImage(name: name) { (data) in
                // update the UI on the main thread
                DispatchQueue.main.async() {
                    let uim = UIImage(data: data)
                    self.image = Image(uiImage: uim!)
                }
            }
        }
        // store API object for easy retrieval later
        self._data = data
    }

    노트의 인스턴스에 이미지 이름이 표시되면 코드가 retrieveImage를 호출합니다. retrieveImage는 비동기 함수로, 이미지가 다운로드될 때 호출할 함수를 받습니다. 이 함수는 이미지 UI 객체를 생성하여 노트의 인스턴스에 할당합니다. 이 할당 동작은 사용자 인터페이스 업데이트를 트리거하므로 애플리케이션의 기본 스레드에서 DispatchQueue.main.async를 사용하여 실행됩니다.

  • 먼저 이미지 캡처를 지원할 일반 코드를 추가합니다. 이 코드는 다양한 애플리케이션에 재사용할 수 있으며, 사용자가 이미지 라이브러리에서 이미지를 선택할 수 있는 이미지 선택기를 표시합니다.

    Xcode에서 새 Swift 파일을 생성합니다(⌘N을 입력한 후 Swift 선택). 파일 이름을 CaptureImageView.swift로 지정하고 다음 코드를 추가합니다.

    import Foundation
    import UIKit
    import SwiftUI
    
    struct CaptureImageView {
    
      /// MARK: - Properties
      @Binding var isShown: Bool
      @Binding var image: UIImage?
    
      func makeCoordinator() -> Coordinator {
        return Coordinator(isShown: $isShown, image: $image)
      }
    }
    
    class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
      @Binding var isCoordinatorShown: Bool
      @Binding var imageInCoordinator: UIImage?
      init(isShown: Binding<Bool>, image: Binding<UIImage?>) {
        _isCoordinatorShown = isShown
        _imageInCoordinator = image
      }
      func imagePickerController(_ picker: UIImagePickerController,
                    didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
         guard let unwrapImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage else { return }
         imageInCoordinator = unwrapImage
         isCoordinatorShown = false
      }
      func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
         isCoordinatorShown = false
      }
    }
    
    extension CaptureImageView: UIViewControllerRepresentable {
        func makeUIViewController(context: UIViewControllerRepresentableContext<CaptureImageView>) -> UIImagePickerController {
            let picker = UIImagePickerController()
            picker.delegate = context.coordinator
    
            // picker.sourceType = .camera // on real devices, you can capture image from the camera
            // see https://medium.com/better-programming/how-to-pick-an-image-from-camera-or-photo-library-in-swiftui-a596a0a2ece
    
            return picker
        }
    
        func updateUIViewController(_ uiViewController: UIImagePickerController,
                                    context: UIViewControllerRepresentableContext<CaptureImageView>) {
    
        }
    }
  • 노트가 생성되면 Backend에서 스토리지 메서드를 호출해보겠습니다. ContentView.swift를 열고 AddNoteView를 수정하여 ImagePicker 구성 요소를 추가합니다.

    // at the start of the Content View struct 
    @State var image : UIImage? // replace the previous declaration of image
    @State var showCaptureImageView = false
    
    // in the view, replace the existing PICTURE section
    Section(header: Text("PICTURE")) {
        VStack {
            Button(action: {
                self.showCaptureImageView.toggle()
            }) {
                Text("Choose photo")
            }.sheet(isPresented: $showCaptureImageView) {
                CaptureImageView(isShown: self.$showCaptureImageView, image: self.$image)
            }
            if (image != nil ) {
                HStack {
                    Spacer()
                    Image(uiImage: image!)
                        .resizable()
                        .frame(width: 250, height: 200)
                        .clipShape(Circle())
                        .overlay(Circle().stroke(Color.white, lineWidth: 4))
                        .shadow(radius: 10)
                    Spacer()
                }
            }
        }
    }

    노트뿐만 아니라 이미지도 저장하도록 Create Note 섹션을 수정합니다.

    Section {
        Button(action: {
            self.isPresented = false
    
            let note = Note(id : UUID().uuidString,
                            name: self.$name.wrappedValue,
                            description: self.$description.wrappedValue)
    
            if let i = self.image  {
                note.imageName = UUID().uuidString
                note.image = Image(uiImage: i)
    
                // asynchronously store the image (and assume it will work)
                Backend.shared.storeImage(name: note.imageName!, image: (i.pngData())!)
            }
    
            // asynchronously store the note (and assume it will succeed)
            Backend.shared.createNote(note: note)
    
            // add the new note in our userdata, this will refresh UI
            withAnimation { self.userData.notes.append(note) }
        }) {
            Text("Create this note")
        }
    }
  • 정상적으로 작동하는지 확인하기 위해 프로젝트를 빌드하고 실행합니다. 제품(Product) 메뉴에서 Run(실행)을 선택하거나 ⌘R을 입력합니다. 오류가 발생하지 않아야 합니다.

    아직 로그인되어 있다면 앱이 하나의 노트로 목록에서 시작됩니다. + 기호를 다시 사용하여 노트를 생성합니다. 이번에는 로컬 이미지 스토어에서 선택한 사진이 추가됩니다.

    전체 흐름은 다음과 같습니다.

결론

AWS Amplify를 사용하여 iOS 애플리케이션을 구축했습니다! 사용자가 가입하고 로그인하고 계정을 관리할 수 있도록 앱에 인증을 추가했습니다. 이 앱에는 Amazon DynamoDB를 사용하여 구성된 확장형 GraphQL API도 추가되어 사용자가 메모를 생성하고 삭제할 수 있습니다. 또한 Amazon S3를 사용한 파일 스토리지도 추가하여 사용자가 이미지를 업로드하고 앱에서 볼 수 있게 했습니다.

마지막 섹션에서는 방금 생성한 백엔드를 재사용하거나 삭제할 때의 지침을 알아봅니다.

  • Amplify를 사용하면 여러 프런트엔드 애플리케이션 간에 단일 백엔드를 손쉽게 공유할 수 있습니다.

    터미널에서 다른 프로젝트 디렉터리로 이동하고 다음 명령을 실행합니다.

    mkdir other-project
    cd other-project
    
    amplify pull
    
    ? Do you want to use an AWS profile? accept the default Yes and press enter
    ? Please choose the profile you want to use select the profile you want to use and press enter
    ? Which app are you working on? select the backend you want to share and press enter
    ? Choose your default editor: select you prefered text editor and press enter
    ? Choose the type of app that you're building select the operating system for your new project and press enter
    ? Do you plan on modifying this backend? most of the time, select No and press enter. All backend modifications can be done from the original iOS project.

    몇 초 후에 다음 메시지가 표시됩니다.

    Added backend environment config object to your project.
    Run 'amplify pull' to sync upstream changes.

    구성 파일 2개를 가져온 것을 알 수 있습니다. 'Do you plan on modifying this backend?'라는 질문에 'Yes'라고 답하면 amplify 디렉터리도 표시됩니다.

    ➜  other-project git:(master) ✗ ls -al
    total 24
    drwxr-xr-x   5 stormacq  admin   160 Jul 10 10:28 .
    drwxr-xr-x  19 stormacq  admin   608 Jul 10 10:27 ..
    -rw-r--r--   1 stormacq  admin   315 Jul 10 10:28 .gitignore
    -rw-r--r--   1 stormacq  admin  3421 Jul 10 10:28 amplifyconfiguration.json
    -rw-r--r--   1 stormacq  admin  1897 Jul 10 10:28 awsconfiguration.json
  • 테스트 또는 프로토타입용 백엔드를 생성하거나 학습용 백엔드를 생성하는 경우 이 자습서를 따를 때와 마찬가지로 생성된 클라우드 리소스를 삭제해야 합니다.

    이 자습서의 맥락에서 리소스를 사용할 때는 프리 티어가 적용되지만 클라우드에서 사용되지 않는 리소스를 정리하는 것이 모범 사례입니다.

    amplify 프로젝트를 정리하려면 터미널에서 다음 명령을 실행합니다.

    amplify delete

    잠시 후에 모든 클라우드 리소스가 삭제되었음을 확인하는 아래의 메시지가 표시됩니다.

    ✔ Project deleted in the cloud
    Project deleted locally.

이 자습서를 끝까지 진행해 주셔서 감사합니다. 아래의 도구 또는 Github 리포지토리에 대한 pull 요청을 사용하여 피드백을 보내 주십시오.

이 모듈이 유용했습니까?

축하합니다!

AWS에서 웹 애플리케이션을 구축했습니다. 다음 단계로, 특정 AWS 기술을 자세히 살펴보고 애플리케이션의 수준을 한 차원 높여보겠습니다.