서론
상태 변화를 관리할 때, distinctUntilChanged 동일한 값이더라도 다른 컨텍스트에서 발생하는 이벤트를 구분해야 하는 상황이 발생했습니다. 예를 들어, 사용자가 동일한 데이터를 여러 번 입력하거나, 특정 액션이 반복될 때, distinctUntilChanged는 의도치 않게 중요한 이벤트까지 필터링하여 UI 업데이트가 누락되는 문제가 발생했습니당.
이러한 문제를 해결하기 위해 ReactorKit에서는 @Pulse 키워드를 도입하게 되었습니다.
@Pulse는 특정 이벤트를 별도의 스트림으로 분리하여 관리할 수 있는 강력한 도구로, 중복 필터링 없이 모든 이벤트를 정확하게 전달할 수 있도록 도와줍니다.
@Pulse 키워드
먼저, 공식문서를 살펴보겠습니다. 공식문서에는 @Pulse를 다음과 같이 소개하고 있습니다:
Pulse has diff only when mutated To explain in code, the results are as follows.
같은 값이든 다른 값이든 상관없이 값이 새롭게 할당되는 경우에 이벤트가 발생합니다.
또한, 제네릭 구조체의 PropertyWrapper 특성을 가지고 있습니다.
ReactorKit 공식문서 예제
// Reactor
private final class MyReactor: Reactor {
struct State {
@Pulse var alertMessage: String?
}
func mutate(action: Action) -> Observable<Mutation> {
switch action {
case let .alert(message):
return Observable.just(Mutation.setAlertMessage(message))
}
}
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case let .setAlertMessage(alertMessage):
newState.alertMessage = alertMessage
}
return newState
}
}
// View
reactor.pulse(\.$alertMessage)
.compactMap { $0 } // filter nil
.subscribe(onNext: { [weak self] (message: String) in
self?.showAlert(message)
})
.disposed(by: disposeBag)
// Cases
reactor.action.onNext(.alert("Hello")) // showAlert() is called with `Hello`
reactor.action.onNext(.alert("Hello")) // showAlert() is called with `Hello`
reactor.action.onNext(.doSomeAction) // showAlert() is not called
reactor.action.onNext(.alert("Hello")) // showAlert() is called with `Hello`
reactor.action.onNext(.alert("tokijh")) // showAlert() is called with `tokijh`
reactor.action.onNext(.doSomeAction) // showAlert() is not called
@Pulse는 distinctUntilChanged와 비슷하게 보이지만, 실제로는 조금 다른 동작을 하고 있었습니다
Pulse.swift 소스 코드 분석
@propertyWrapper
public struct Pulse<Value> {
public var value: Value {
didSet {
self.riseValueUpdatedCount()
}
}
public internal(set) var valueUpdatedCount = UInt.min
public init(wrappedValue: Value) {
self.value = wrappedValue
}
public var wrappedValue: Value {
get { return self.value }
set { self.value = newValue }
}
public var projectedValue: Pulse<Value> {
return self
}
private mutating func riseValueUpdatedCount() {
self.valueUpdatedCount &+= 1
}
}
Pulse는 제네릭 구조체로, PropertyWrapper 특성을 가지고 있습니다.
중요한 부분은 value의 didSet에서 valueUpdatedCount를 증가시킨다는 것입니다. 이는 값이 변경될 때마다 valueUpdatedCount가 증가함을 의미합니다.
Reactor+Pulse.swift 소스 코드 분석
extension Reactor {
public func pulse<Result>(_ transformToPulse: @escaping (State) throws -> Pulse<Result>) -> Observable<Result> {
return self.state.map(transformToPulse).distinctUntilChanged(\.valueUpdatedCount).map(\.value)
}
}
Reactor에 추가된 pulse 메서드는 상태를 변환하고, distinctUntilChanged를 사용하여 valueUpdatedCount가 변경된 경우에만 새로운 이벤트를 방출합니다. 이는 기존의 distinctUntilChanged가 필터링할 때 동일한 값을 가진 이벤트도 필터링할 수 있는 문제를 해결하는 방법으로 보입니다.
distinctUntilChanged의 문제점과 @Pulse의 해결책
distinctUntilChanged는 이전 값과 동일한 값이 연속으로 발생할 때 중복된 값을 필터링하여 불필요한 이벤트 처리를 줄이는 데 유용합니다. 그러나 ReactorKit에서 상태가 동일한 값이라도 다른 컨텍스트에서 발생하는 이벤트를 구분하지 못해 필요한 UI 업데이트가 누락되는 문제가 있었습니다.
예를 들어, 로그인 화면에서 사용자가 로그인 버튼을 여러 번 탭할 때, 동일한 Action이 연속으로 발생하면 distinctUntilChanged가 이를 필터링하여 실제로 필요한 UI 업데이트가 이루어지지 않는 상황이 발생할 수 있습니다.
이를 해결하기 위해 @Pulse를 도입하였습니다. @Pulse는 상태 변화 시 valueUpdatedCount를 증가시켜, 동일한 값이라도 이벤트가 발생할 때마다 고유한 valueUpdatedCount를 가지게 합니다. 이를 통해 distinctUntilChanged가 필터링하지 못했던 필요한 이벤트를 모두 처리할 수 있게 되었습니다.
결론
Pulse를 도입하기 전에는 distinctUntilChanged를 사용하여 중복된 상태 변화를 필터링하고 있었습니다. 그러나 특정 상황에서 필요한 이벤트가 필터링되어 UI 업데이트가 누락되는 문제가 발생했습니다. 예를 들어, 사용자가 로그인 버튼을 여러 번 탭할 때, 동일한 Action이 연속으로 발생하면 distinctUntilChanged가 이를 필터링하여 실제로 필요한 UI 업데이트가 이루어지지 않았습니다.
이를 해결하기 위해 ReactorKit의 @Pulse를 도입하여, 특정 이벤트를 별도의 스트림으로 분리하고 관리하였습니다.
@Pulse를 활용함으로써, 동일한 값이라도 이벤트가 발생할 때마다 고유한 valueUpdatedCount가 증가하게 되어, distinctUntilChanged가 필터링하지 않게 되었습니다
'iOS > ReactorKit' 카테고리의 다른 글
[ReactorKit] transform과 global state에 대하여 (1) | 2024.08.05 |
---|---|
[ReactortKit] ReactortKit 슥 알아보기 (1) | 2024.06.09 |