본문 바로가기

UIKIT

Combine을 활용한 함수

https://explorer89.tistory.com/296

 

viewModel.$user에서 user 앞에 $를 붙이는 이유

https://explorer89.tistory.com/295 ViewModel을 사용하는 목적https://explorer89.tistory.com/294 ViewModelhttps://explorer89.tistory.com/82 ObservableObject와 @Published개체가 변경되기 전에 내보내는 게시자가 있는 개체 형식

explorer89.tistory.com

 

func createUser() {
    guard let email = email,
          let password = password else { return }
    
    AuthManager.shared.registerUser(with: email, password: password)
        .sink { _ in
            // 완료나 오류 처리 (현재 생략)
        } receiveValue: { [weak self] user in
            // Publisher가 User를 방출했을 때 실행
            self?.user = user
        }
        .store(in: &subscription)
}

 

이 함수가 하는 일:

  1. email과 password 값이 유효한지 확인:
    • guard let을 사용해 nil인 경우 함수가 바로 종료됩니다.
  2. AuthManager.shared.registerUser 호출:
    • 이메일과 비밀번호로 회원을 생성하는 Publisher를 반환합니다.
    • 이 Publisher는 성공하면 User 객체를 방출합니다.
  3. sink를 사용해 Publisher에서 방출된 이벤트 처리:
    • sink의 두 클로저:
      • 첫 번째 클로저 ({ _ in }):
        • 완료 이벤트나 오류를 처리할 수 있습니다.
        • 현재는 생략되어 아무 작업도 하지 않습니다.
      • 두 번째 클로저 ({ user in }):
        • 성공적으로 User 객체가 방출되었을 때 실행됩니다.
        • 방출된 User 객체를 ViewModel의 user 프로퍼티에 저장합니다.
  4. 구독 저장 (store(in:)):
    • 구독을 subscription(Set<AnyCancellable>)에 저장하여 ViewModel이 해제되면 구독도 자동으로 해제되도록 합니다.

 

반환값에 대한 이해

  • AuthManager.shared.registerUser는 AnyPublisher<User, Error>를 반환합니다.
    • 성공 시: User 객체를 방출.
    • 실패 시: Error를 방출.
  • .sink는 이 반환값을 처리하는 핵심입니다:
    • 성공 시 receiveValue에서 User를 받아 작업.
    • 완료나 실패 이벤트는 첫 번째 클로저에서 처리.

 

.sink는 완료 되었을 때, 에러 나왔을 때 따로 처리하고 추가로 receiveValue로 받는건가? 그냥 receiveValue는 완료되었을 떄랑 같이 처리하면 안되나?

 

receiveValue와 완료/에러 처리는 별도로 설계된 이유는 Combine의 Publisher가 데이터 스트림을 처리하는 방식 때문입니다. 이를 이해하려면 sink의 역할과 데이터 처리 흐름을 더 깊이 살펴보아야 합니다.

 

 

sink의 두 가지 클로저

(1) 완료 및 에러 처리 클로저 (completion 클로저):

이 클로저는 Publisher가 더 이상 이벤트를 방출하지 않을 때 호출됩니다.

  • 성공적인 완료: .finished
  • 에러 발생: .failure(Error)

(2) receiveValue 클로저:

이 클로저는 Publisher가 데이터를 방출할 때마다 호출됩니다.

 

 

왜 receiveValue와 완료/에러 처리가 분리되었을까?

Combine에서 데이터를 스트리밍하는 방식은 "이벤트 기반"입니다.
Publisher는 여러 번 데이터를 방출할 수도 있고, 방출을 완료하거나 에러를 내면서 종료될 수 있습니다.

완료/에러와 데이터 방출의 차이:

  • 데이터 방출 (receiveValue):
    • Publisher가 데이터를 내보낼 때 호출됩니다.
    • 여러 번 호출될 수 있습니다. (e.g., PassthroughSubject, Timer)
  • 완료/에러 (completion):
    • Publisher가 작업을 모두 끝냈거나 실패했음을 알릴 때 호출됩니다.
    • 단 한 번 호출됩니다.

 

예시: 데이터 스트리밍과 완료/에러의 분리

let subject = PassthroughSubject<Int, Error>()

let cancellable = subject
    .sink { completion in
        switch completion {
        case .finished:
            print("Stream completed successfully.")
        case .failure(let error):
            print("Stream failed with error: \(error)")
        }
    } receiveValue: { value in
        print("Received value: \(value)")
    }

// 방출 시뮬레이션
subject.send(1) // "Received value: 1"
subject.send(2) // "Received value: 2"
subject.send(completion: .finished) // "Stream completed successfully."

 

왜 분리되었는가?

  • receiveValue는 중간 데이터 이벤트를 처리하고,
  • completion은 전체 작업의 상태(성공/실패)를 알리는 데 사용됩니다.

receiveValue와 완료 처리가 합쳐져 있다면 데이터 스트림 중간에 값을 받을 때마다 완료 상태를 확인해야 하는 불편함이 생깁니다.

 

 

func registerUser(with email: String, password: String) -> AnyPublisher<User, Error> {
    return Auth.auth().createUser(withEmail: email, password: password)
        .map(\.user)
        .eraseToAnyPublisher()
}

 

  • Input:
    • email: 사용자가 입력한 이메일 주소.
    • password: 사용자가 입력한 비밀번호.
  • Output:
    • AnyPublisher<User, Error>:
      • Firebase에서 생성한 User 객체를 방출하는 Combine의 Publisher.
      • 에러가 발생할 경우 Error를 방출.

 

Auth.auth().createUser(withEmail:password:)

  • Firebase의 createUser 메서드를 호출하여 사용자를 생성.
  • 반환값: AuthDataResult라는 객체의 Publisher.
    • AuthDataResult는 사용자가 생성된 후의 정보를 포함하며, 다음과 같은 프로퍼티를 가짐:
      • user: 생성된 User 객체.
      • additionalUserInfo: 추가 정보.
      • credential: 인증 정보.

 

.map(\.user)의 역할

.map(\.user)는 Combine의 Operator 중 하나로, 방출된 데이터를 변환하는 역할을 합니다.

  1. 원래 데이터:
    • AuthDataResult 타입의 데이터가 Publisher로 방출됩니다.
  2. 변환:
    • AuthDataResult 객체의 user 프로퍼티만 추출하여 방출합니다.
    • 이 코드에서 \.user는 Swift의 KeyPath 표현식으로, AuthDataResult 객체 내부의 user 프로퍼티에 접근합니다.
    • 결과적으로, User 타입의 데이터만 방출하게 됩니다.

변경 전:
Publisher<AuthDataResult, Error>

변경 후:
Publisher<User, Error>

 

 

.eraseToAnyPublisher()의 역할

.eraseToAnyPublisher()는 Combine에서 사용되는 타입 지우기(Type Erasure) 기법입니다.

  • 반환값의 타입을 AnyPublisher로 고정하여, 구현 세부사항을 감춥니다.
  • 이로 인해 함수의 반환 타입이 간단해지고, 외부에서 Combine 내부 구현을 알 필요 없이 사용할 수 있습니다.

왜 사용하는가?

  1. 유연성: 반환 타입을 AnyPublisher로 고정함으로써 구현 세부사항이 변경되어도 함수의 시그니처를 유지할 수 있습니다.
    • 예: 내부적으로 map, flatMap 등 연산이 추가되더라도 외부에는 영향을 주지 않음.
  2. 캡슐화: 내부 구현을 숨겨 외부에서 의존성을 줄임.
  3. 가독성: 복잡한 타입 대신 간단한 AnyPublisher를 사용.

변경 전:
MapPublisher<AuthDataResult, User>

변경 후:
AnyPublisher<User, Error>

 

 

최종 데이터 흐름

  1. Firebase 호출:
    Auth.auth().createUser(withEmail:password:)는 Publisher<AuthDataResult, Error>를 방출합니다.
  2. 데이터 변환:
    .map(\.user)를 통해 AuthDataResult에서 user만 추출하여 방출합니다.
    결과: Publisher<User, Error>
  3. 타입 변환:
    .eraseToAnyPublisher()를 사용해 반환 타입을 AnyPublisher<User, Error>로 고정합니다.

 

예시 사용

let email = "test@example.com"
let password = "password123"

AuthManager.shared.registerUser(with: email, password: password)
    .sink { completion in
        switch completion {
        case .finished:
            print("User registered successfully.")
        case .failure(let error):
            print("Error registering user: \(error.localizedDescription)")
        }
    } receiveValue: { user in
        print("Registered User ID: \(user.uid)")
    }
    .store(in: &subscriptions)

 

 

.map새로운 배열을 만드는 것이 아닙니다. 대신, Combine에서 사용되는 .map은 Publisher가 방출한 값을 변환(transform) 하는 역할을 합니다. Swift의 Array.map과는 다르게 Combine의 .map은 비동기 스트림에서 각 값을 변환하는 데 초점이 있습니다.

 

 

1. Array.map

  • 배열의 각 요소를 변환하고, 변환된 새 배열을 반환
let numbers = [1, 2, 3]
let squared = numbers.map { $0 * $0 } // [1, 4, 9]

 

2. Publisher.map (Combine의 .map)

 

  • Publisher가 방출한 값을 변환하여, 새로운 값을 방출하는 Publisher를 생성.
  • 데이터의 흐름을 실시간으로 변경. 
let publisher = [1, 2, 3].publisher
let squaredPublisher = publisher.map { $0 * $0 }
squaredPublisher.sink { print($0) }
// 출력:
// 1
// 4
// 9

 

 

 

Auth.auth().createUser(withEmail: email, password: password)
    .map(\.user)

 

 

  • 원래 Publisher:
    • Auth.auth().createUser는 Publisher<AuthDataResult, Error>를 반환.
      • 이 Publisher는 AuthDataResult 객체를 방출.
  • .map(\.user)의 역할:
    • 방출된 AuthDataResult 객체에서 user라는 프로퍼티만 추출하여 방출.
    • 결과적으로, Publisher<User, Error>가 됨.