UIKIT

Firebase - Combine 프레임워크 사용 비동기 작업 처리 (저장) map VS flatmap

밤새는 탐험가89 2025. 1. 9. 11:24

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)
})

 

 

 

flatMapCombine에서 하나의 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 배열에 저장하여, 메모리 관리와 구독을 유지합니다

 

 

전체 흐름

  1. 랜덤 ID 생성 후 이미지를 JPEG 형식으로 압축하여 imageData로 준비합니다.
  2. StorageMetadata 객체를 생성하고, 이미지의 MIME 타입을 "image/jpeg"로 설정합니다.
  3. StorageManager.shared.uploadProfilePhoto 메서드를 호출하여 이미지를 Firebase Storage에 업로드합니다. 이 메서드는 AnyPublisher<StorageMetadata, FireStorageError>를 반환합니다.
  4. 업로드가 완료되면, 업로드된 이미지의 경로(metaData.path)를 사용하여 다운로드 URL을 가져오는 getDownloadURL 메서드를 호출합니다. 이 메서드는 AnyPublisher<URL, FireStorageError>를 반환합니다.
  5. sink를 사용하여 getDownloadURL이 반환한 URL을 받아 url 프로퍼티에 저장합니다. 만약 오류가 발생하면 error 프로퍼티에 오류 메시지를 저장합니다.
  6. 모든 작업은 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은 내부적으로 두 가지 중요한 작업을 처리합니다:

  1. 첫 번째 Publisher가 방출하는 값을 사용하여 새로운 Publisher를 생성합니다.
  2. 두 번째 Publisher가 방출하는 값을 구독하고, 두 번째 Publisher의 값을 receiveValue에서 처리할 수 있게 합니다.