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)
}
이 함수가 하는 일:
- email과 password 값이 유효한지 확인:
- guard let을 사용해 nil인 경우 함수가 바로 종료됩니다.
- AuthManager.shared.registerUser 호출:
- 이메일과 비밀번호로 회원을 생성하는 Publisher를 반환합니다.
- 이 Publisher는 성공하면 User 객체를 방출합니다.
- sink를 사용해 Publisher에서 방출된 이벤트 처리:
- sink의 두 클로저:
- 첫 번째 클로저 ({ _ in }):
- 완료 이벤트나 오류를 처리할 수 있습니다.
- 현재는 생략되어 아무 작업도 하지 않습니다.
- 두 번째 클로저 ({ user in }):
- 성공적으로 User 객체가 방출되었을 때 실행됩니다.
- 방출된 User 객체를 ViewModel의 user 프로퍼티에 저장합니다.
- 첫 번째 클로저 ({ _ in }):
- sink의 두 클로저:
- 구독 저장 (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를 방출.
- AnyPublisher<User, Error>:
Auth.auth().createUser(withEmail:password:)
- Firebase의 createUser 메서드를 호출하여 사용자를 생성.
- 반환값: AuthDataResult라는 객체의 Publisher.
- AuthDataResult는 사용자가 생성된 후의 정보를 포함하며, 다음과 같은 프로퍼티를 가짐:
- user: 생성된 User 객체.
- additionalUserInfo: 추가 정보.
- credential: 인증 정보.
- AuthDataResult는 사용자가 생성된 후의 정보를 포함하며, 다음과 같은 프로퍼티를 가짐:
.map(\.user)의 역할
.map(\.user)는 Combine의 Operator 중 하나로, 방출된 데이터를 변환하는 역할을 합니다.
- 원래 데이터:
- AuthDataResult 타입의 데이터가 Publisher로 방출됩니다.
- 변환:
- AuthDataResult 객체의 user 프로퍼티만 추출하여 방출합니다.
- 이 코드에서 \.user는 Swift의 KeyPath 표현식으로, AuthDataResult 객체 내부의 user 프로퍼티에 접근합니다.
- 결과적으로, User 타입의 데이터만 방출하게 됩니다.
변경 전:
Publisher<AuthDataResult, Error>
변경 후:
Publisher<User, Error>
.eraseToAnyPublisher()의 역할
.eraseToAnyPublisher()는 Combine에서 사용되는 타입 지우기(Type Erasure) 기법입니다.
- 반환값의 타입을 AnyPublisher로 고정하여, 구현 세부사항을 감춥니다.
- 이로 인해 함수의 반환 타입이 간단해지고, 외부에서 Combine 내부 구현을 알 필요 없이 사용할 수 있습니다.
왜 사용하는가?
- 유연성: 반환 타입을 AnyPublisher로 고정함으로써 구현 세부사항이 변경되어도 함수의 시그니처를 유지할 수 있습니다.
- 예: 내부적으로 map, flatMap 등 연산이 추가되더라도 외부에는 영향을 주지 않음.
- 캡슐화: 내부 구현을 숨겨 외부에서 의존성을 줄임.
- 가독성: 복잡한 타입 대신 간단한 AnyPublisher를 사용.
변경 전:
MapPublisher<AuthDataResult, User>
변경 후:
AnyPublisher<User, Error>
최종 데이터 흐름
- Firebase 호출:
Auth.auth().createUser(withEmail:password:)는 Publisher<AuthDataResult, Error>를 방출합니다. - 데이터 변환:
.map(\.user)를 통해 AuthDataResult에서 user만 추출하여 방출합니다.
결과: Publisher<User, Error> - 타입 변환:
.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 객체를 방출.
- Auth.auth().createUser는 Publisher<AuthDataResult, Error>를 반환.
- .map(\.user)의 역할:
- 방출된 AuthDataResult 객체에서 user라는 프로퍼티만 추출하여 방출.
- 결과적으로, Publisher<User, Error>가 됨.
'UIKIT' 카테고리의 다른 글
Combine, FirebaseStore 데이터 저장 (0) | 2025.01.08 |
---|---|
eraseToAnyPublisher(), Set<AnyCancellable> (0) | 2025.01.08 |
viewModel.$user에서 user 앞에 $를 붙이는 이유 (0) | 2025.01.08 |
ViewModel을 사용하는 목적 (0) | 2025.01.08 |
UITabBarAppearance (0) | 2025.01.05 |