Firebase - Combine 프레임워크 사용 비동기 작업 처리 (저장) map VS flatmap
https://explorer89.tistory.com/302
collectionUsers(retreive id: String)
https://explorer89.tistory.com/301 CreateUser 함수?https://explorer89.tistory.com/300 AnyPublisher?https://explorer89.tistory.com/299 Combine, FirebaseStore 데이터 저장https://explorer89.tistory.com/298" data-og-description="https://explorer89.ti
explorer89.tistory.com
import Foundation
import Combine
import FirebaseAuth
import FirebaseStorageCombineSwift
import FirebaseStorage
final class StorageManager {
static let shared = StorageManager()
let storage = Storage.storage()
func uploadProfilePhoto(with randomID: String, image: Data, metaData: StorageMetadata) -> AnyPublisher<StorageMetadata, Error> {
return storage
.reference()
.child("images/\(randomID).jpg")
.putData(image, metadata: metaData)
.print()
.eraseToAnyPublisher()
}
func getDownloadURL(for id: String?) -> AnyPublisher<URL, Error> {
guard let id = id else {
return Fail(error: FireStorageError.invalidImageID).eraseToAnyPublisher()
}
return storage
.reference(withPath: id)
.downloadURL()
.print()
.eraseToAnyPublisher()
}
}
이 StorageManager 클래스는 Firebase Storage와 연동하여 파일을 업로드하고 다운로드 URL을 가져오는 기능을 제공합니다. 이 클래스는 Combine 프레임워크를 사용하여 비동기적으로 작업을 처리합니다.
uploadProfilePhoto(with: image: metaData:)
이 함수는 주어진 randomID와 image 데이터를 Firebase Storage에 업로드하는 기능을 합니다.
동작 과정:
- randomID: 고유한 ID로, 업로드할 이미지 파일의 이름을 정의합니다. 이 ID는 Firebase Storage에서 해당 파일을 구분하는 데 사용됩니다.
- image: 업로드할 이미지 데이터 (Data)입니다.
- metaData: Firebase Storage에 저장될 메타데이터로, 이미지의 MIME 타입이나 기타 정보들을 담고 있습니다.
return storage
.reference()
.child("images/\(randomID).jpg") // 이미지 파일이 저장될 경로 (예: images/randomID.jpg)
.putData(image, metadata: metaData) // 이미지 데이터와 메타데이터를 Firebase Storage에 업로드
.print() // (옵션) 네트워크 요청 및 응답을 콘솔에 출력
.eraseToAnyPublisher() // 결과를 Publisher로 반환 (Combine 사용)
- storage.reference(): Firebase Storage의 루트 참조를 가져옵니다.
- .child("images/\(randomID).jpg"): images/ 폴더 내에 randomID.jpg라는 이름으로 파일을 업로드합니다.
- .putData(image, metadata: metaData): 이미지 데이터를 Firebase Storage에 업로드하고, 메타데이터를 함께 저장합니다.
- .print(): 요청 및 응답에 대한 디버깅 로그를 출력합니다. 네트워크 요청이 성공하거나 실패할 때, 이를 확인할 수 있습니다.
- .eraseToAnyPublisher(): 결과를 AnyPublisher로 반환하여, 클라이언트 코드에서 Combine을 사용하여 반응형 방식으로 처리할 수 있도록 합니다. 이는 업로드 완료 후에 StorageMetadata를 반환합니다.
getDownloadURL(for:)
이 함수는 Firebase Storage에 저장된 파일의 다운로드 URL을 가져오는 기능을 합니다. 이 URL을 사용하여 클라이언트는 업로드된 파일을 다운로드할 수 있습니다.
guard let id = id else {
return Fail(error: FireStorageError.invalidImageID).eraseToAnyPublisher()
}
return storage
.reference(withPath: id)
.downloadURL()
.print()
.eraseToAnyPublisher()
- 먼저 id가 nil인지 확인합니다. 만약 nil이라면, 오류를 반환합니다 (FireStorageError.invalidImageID).
- storage.reference(withPath: id): 주어진 id를 통해 Firebase Storage에서 해당 파일의 참조를 가져옵니다.
- .downloadURL(): 해당 파일의 다운로드 URL을 요청합니다.
- .print(): 네트워크 요청에 대한 디버깅 출력을 합니다.
- .eraseToAnyPublisher(): 결과를 AnyPublisher로 반환하여, 클라이언트 코드에서 Combine을 사용하여 비동기적으로 처리할 수 있게 합니다.
Combine을 사용한 이유
이 클래스는 Combine을 사용하여 비동기 작업을 처리합니다. AnyPublisher<StorageMetadata, Error>와 AnyPublisher<URL, Error> 타입을 반환하는 함수는 비동기적으로 Firebase Storage와의 상호작용 결과를 처리합니다. Combine의 Publisher를 사용하면 다음과 같은 이점을 얻을 수 있습니다:
- 반응형 프로그래밍: 데이터를 받을 때마다 자동으로 갱신할 수 있으며, 데이터 흐름을 선언적으로 처리할 수 있습니다.
- 오류 처리: 오류가 발생하면 Error 타입으로 명확하게 처리할 수 있습니다.
- 간편한 취소 및 조합: Publisher를 쉽게 조합하거나 취소할 수 있어, 코드의 유연성을 높여줍니다.
func uploadAvatar() {
let randomID = UUID().uuidString
guard let imageData = imageData?.jpegData(compressionQuality: 0.5) else { return }
let metaData = StorageMetadata()
metaData.contentType = "image/jpeg"
StorageManager.shared.uploadProfilePhoto(with: randomID, image: imageData, metaData: metaData)
.flatMap({ metaData in
StorageManager.shared.getDownloadURL(for: metaData.path)
})
.sink { [weak self] completion in
if case .failure(let error) = completion {
self?.error = error.localizedDescription
}
} receiveValue: { [weak self] url in
self?.url = url
}
.store(in: &subscriptions)
}
uploadAvatar() 함수는 Firebase Storage에 사용자의 아바타 이미지를 업로드하고, 업로드가 완료되면 해당 이미지의 다운로드 URL을 반환받는 기능을 합니다. 이 과정에서 Combine을 사용하여 비동기적으로 작업을 처리하고 있습니다.
let radomID = UUID().uuidString
randomID는 이미지 파일을 저장할 고유한 ID를 생성하는 부분입니다. UUID().uuidString을 사용하여 고유한 문자열을 생성합니다. 이 ID는 Firebase Storage에 업로드할 파일의 경로에 사용됩니다.
guard let imageData = imageData?.jpegData(compressionQuality: 0.5) else { return }
imageData는 업로드할 이미지의 원본 데이터입니다. 이 코드에서는 UIImage 타입의 imageData를 jpegData(compressionQuality:) 메서드를 사용해 JPEG 형식으로 변환하고 있습니다. 이때 압축 품질은 0.5로 설정하여 이미지 크기를 절반으로 압축합니다.
let metaData = StorageMetadata()
metaData.contentType = "image/jpeg"
Firebase Storage에 업로드하는 데이터와 함께 메타데이터도 저장할 수 있습니다. StorageMetadata 객체를 생성하고, contentType을 "image/jpeg"로 설정하여 업로드할 파일이 JPEG 이미지임을 명시합니다.
StorageManager.shared.uploadProfilePhoto(with: radomID, image: imageData, metaData: metaData)
이 부분은 Firebase Storage에 이미지를 업로드하는 코드입니다. StorageManager.shared.uploadProfilePhoto 메서드를 호출하고, 이전에 생성한 randomID, imageData, metaData를 전달합니다.
uploadProfilePhoto 메서드는 Combine을 사용하여 AnyPublisher<StorageMetadata, FireStorageError>를 반환합니다. 업로드가 완료되면 StorageMetadata가 반환됩니다.
.flatMap({ metaData in
StorageManager.shared.getDownloadURL(for: metaData.path)
})
flatMap은 Combine에서 하나의 Publisher로부터 다른 Publisher로 변환하는 연산자입니다. uploadProfilePhoto가 성공적으로 업로드를 완료하면, 반환된 metaData의 path를 사용하여 이미지의 다운로드 URL을 가져옵니다.
getDownloadURL(for:) 메서드는 Firebase Storage에서 해당 경로(metaData.path)에 대한 다운로드 URL을 요청합니다. 이 메서드는 AnyPublisher<URL, FireStorageError>를 반환하므로, flatMap을 통해 이 Publisher를 반환하고, 이어지는 sink에서 결과를 받을 수 있습니다.
.sink { [weak self] completion in
if case .failure(let error) = completion {
self?.error = error.localizedDescription
}
} receiveValue: { [weak self] url in
self?.url = url
}
.store(in: &subscriptions)
sink는 Combine의 구독자(Subscriber)로, Publisher가 방출하는 값을 처리합니다.
completion 블록에서는 업로드 또는 다운로드 과정에서 발생할 수 있는 오류를 처리합니다. 오류가 발생하면 error 프로퍼티에 오류 메시지를 저장합니다.
- completion의 값은 Publisher가 완료되었을 때 발생하며, failure일 경우에는 오류 메시지를 error에 저장합니다.
receiveValue 블록에서는 정상적으로 다운로드 URL을 받았을 때 해당 URL을 url 프로퍼티에 저장합니다.
store(in: &subscriptions)는 이 Publisher를 subscriptions 배열에 저장하여, 메모리 관리와 구독을 유지합니다
전체 흐름
- 랜덤 ID 생성 후 이미지를 JPEG 형식으로 압축하여 imageData로 준비합니다.
- StorageMetadata 객체를 생성하고, 이미지의 MIME 타입을 "image/jpeg"로 설정합니다.
- StorageManager.shared.uploadProfilePhoto 메서드를 호출하여 이미지를 Firebase Storage에 업로드합니다. 이 메서드는 AnyPublisher<StorageMetadata, FireStorageError>를 반환합니다.
- 업로드가 완료되면, 업로드된 이미지의 경로(metaData.path)를 사용하여 다운로드 URL을 가져오는 getDownloadURL 메서드를 호출합니다. 이 메서드는 AnyPublisher<URL, FireStorageError>를 반환합니다.
- sink를 사용하여 getDownloadURL이 반환한 URL을 받아 url 프로퍼티에 저장합니다. 만약 오류가 발생하면 error 프로퍼티에 오류 메시지를 저장합니다.
- 모든 작업은 Combine을 사용하여 비동기적으로 처리되며, subscriptions 배열에 구독을 저장하여 메모리에서 관리합니다.
flatMap을 사용하는 이유와 map과의 차이
flatMap vs map
- map:
- map은 Publisher가 방출하는 값을 변환하는 연산자입니다. 반환값은 동일한 타입의 Publisher를 반환합니다.
- 예를 들어, Publisher가 String을 방출하면 map을 사용해 그 값을 다른 String으로 변환할 수 있습니다. map은 단일 값을 변환합니다.
- flatMap:
- flatMap은 map과 비슷하지만 중요한 차이점이 있습니다. flatMap은 새로운 Publisher를 반환할 수 있는 연산자입니다. 즉, flatMap을 사용하면 하나의 Publisher에서 또 다른 Publisher를 만들고, 이 내부 Publisher를 플랫하게(평평하게) 병합하여 결과를 반환합니다.
- flatMap은 내부적으로 Publisher를 반환할 때 유용합니다. 예를 들어, 하나의 이벤트가 발생했을 때 추가적인 비동기 작업을 더 할 때 유용합니다.
uploadAvatar()에서 flatMap 사용 이유
.flatMap({ metaData in
StorageManager.shared.getDownloadURL(for: metaData.path)
})
1. 두 번째 비동기 작업을 처리하기 위해서
- uploadProfilePhoto 메서드는 이미지를 Firebase Storage에 업로드하고 업로드 완료 후 StorageMetadata를 반환합니다. 이 StorageMetadata에는 업로드된 파일의 경로(path)와 같은 정보가 포함됩니다.
- 그 이후, 다운로드 URL을 가져오는 또 다른 비동기 작업이 필요합니다. getDownloadURL(for:) 메서드는 StorageMetadata의 path를 사용하여 해당 파일의 다운로드 URL을 반환하는 작업을 비동기적으로 처리합니다. 이 작업은 또 다른 Publisher를 반환하는 비동기 작업입니다.
2. flatMap으로 두 Publisher를 이어붙이기
- flatMap은 첫 번째 Publisher에서 방출된 값을 기반으로 또 다른 Publisher를 반환할 수 있게 해줍니다. 그래서 uploadProfilePhoto 메서드에서 반환되는 StorageMetadata를 받아서, 이를 기반으로 두 번째 Publisher인 getDownloadURL(for:)를 호출할 수 있습니다.
- 즉, flatMap을 사용하면 첫 번째 Publisher의 결과(metaData)를 가지고 두 번째 Publisher를 이어서 처리할 수 있습니다. map은 이런 처리가 불가능하고, 항상 동일한 타입의 값을 변환하기 때문입니다.
map을 사용할 수 없는 이유
- map은 단일 값의 변환만 가능하고, 변환된 값이 Publisher가 아닌 단일 값일 때 사용됩니다. 즉, map을 사용하면 두 번째 비동기 작업을 처리할 수 없습니다.
- 예를 들어, map을 사용한다고 가정하면, 첫 번째 Publisher인 uploadProfilePhoto의 StorageMetadata를 map으로 처리한 후, 그 값을 변환한 새로운 단일 값만 반환할 수 있게 됩니다. 이 새로운 값은 getDownloadURL(for:)처럼 또 다른 비동기 작업을 포함할 수 없기 때문에 map을 사용할 수 없습니다.
flatMap의 내부 작동 방식
flatMap은 내부적으로 두 가지 중요한 작업을 처리합니다:
- 첫 번째 Publisher가 방출하는 값을 사용하여 새로운 Publisher를 생성합니다.
- 두 번째 Publisher가 방출하는 값을 구독하고, 두 번째 Publisher의 값을 receiveValue에서 처리할 수 있게 합니다.