먹고 기도하고 코딩하라

[ReactorKit] Reactor도 disposeBag이 있을까? 본문

앱/Swift

[ReactorKit] Reactor도 disposeBag이 있을까?

사과먹는사람 2024. 1. 3. 19:13
728x90
728x90

결론부터 얘기하면 있다.

 

ReactorKit의 Reactor 파일을 살펴보면 extension에서 disposeBag을 찾을 수 있다.

extension Reactor {
	fileprivate var disposeBag: DisposeBag {
    return MapTables.disposeBag.value(forKey: self, default: DisposeBag())
  }
}

 

하지만 fileprivate 레벨로 선언된 변수이기 때문에 커스텀해서 만드는 Reactor 내에서 직접 접근할 수는 없다. disposeBag의 존재 의의가 궁금해진다.

기원을 찾아 올라가기 위해 MapTables.disposeBag으로 이동한다. MapTables는 enum 타입이고, disposeBag 변수가 포함되어 있다.

private enum MapTables {
  static let disposeBag = WeakMapTable<AnyReactor, DisposeBag>()
}

 

MapTables.disposeBag은 WeakMapTable<AnyReactor, DisposeBag> 타입이다. WeakMapTable에 .value(forKey: self, default: DisposeBag())을 한 것을 return한 값이 Reactor의 disposeBag이라는 것이다.

이 코드의 의미를 정확히 이해하려면 WeakMapTable을 알아야 한다.

왠지 야크 털 깎기의 초입처럼 느껴진다. 간단히만 적자면 WeakMapTable은 NSMapTable과 비슷한 테이블이다. NSMapTable과의 차이점은 키 객체가 메모리 할당 해제되면 값 객체 역시 메모리에서 할당 해제된다는 데에 있다.

WeakMapTable의 value 메소드는 다음과 같이 구현되어 있다.

public func value(forKey key: Key, default: @autoclosure () -> Value) -> Value {
    let weakKey = Weak(key)

    self.lock.lock()
    defer {
      self.lock.unlock()
      self.installDeallocHook(to: key)
    }

    if let value = self.unsafeValue(forKey: weakKey) {
      return value
    }

    let defaultValue = `default`()
    self.unsafeSetValue(defaultValue, forKey: weakKey)
    return defaultValue
  }

 

축약하면 딕셔너리에서 해당 key의 value를 반환하는 것인데, 만약 value가 없다면 default로 주어진 DisposeBag()을 즉석으로 생성해서 value를 삼는 것이다.

다시 적지만 WeakMapTable는 Key object가 사라지면 Value object도 함께 사라지게 설계됐다. 그러니까 Reactor가 사라지면 DisposeBag도 함께 사라지고, DisposeBag이 사라지면서 Disposable들도 같이 소멸되는 것이다. 이게 Reactor 안에 DisposeBag이 있는 이유다.

 

그럼 이 DisposeBag에는 언제 Disposable들이 포함될까? Reactor를 사용하는 쪽에서는 그것이 명시적으로 드러나있지는 않다.

좀 더 파고 들어가보면 createStream() 메소드 내에서 disposeBag에 담는 작업이 있다.

쭉 보면… 리액터의 action을 관찰해 Mutation으로 변경하고, Mutation을 State로 바꾼 다음, 최종적으로 .replay 체이닝해서 ConnectableObservable<State>로 변경한다.

ConnectableObservable은 subscriber가 붙어 있어도 connect() 호출 전까지는 아이템을 방출하지 않는 옵저버블이다.

connect()의 결과로 Disposable이 반환되는데, 이 Disposable을 disposeBag에 넣는 코드가 있는 것을 볼 수 있다.

extension Reactor {
  public func createStateStream() -> Observable<State> {
    let action = self._action.observe(on: self.scheduler)
    let transformedAction = self.transform(action: action)
    let mutation = transformedAction
      .flatMap { [weak self] action -> Observable<Mutation> in
        guard let `self` = self else { return .empty() }
        return self.mutate(action: action).catch { _ in .empty() }
      }
    let transformedMutation = self.transform(mutation: mutation)
    let state = transformedMutation         
      .scan(self.initialState) { [weak self] state, mutation -> State in
        guard let `self` = self else { return state }
        return self.reduce(state: state, mutation: mutation)
      }
      .catch { _ in .empty() }
      .startWith(self.initialState)
    let transformedState = self.transform(state: state)
      .do(onNext: { [weak self] state in
        self?.currentState = state
      })
      .replay(1)
		// 바로 여기!
    transformedState.connect().disposed(by: self.disposeBag)
    return transformedState
  }
}

이 createStateStream()을 어디서 하는지 보면, Reactor가 private으로 갖고 있는 _state에 접근이 있을 때마다 사용한다.

extension Reactor {
	private var _state: Observable<State> {
    if self.isStubEnabled {
      return self.stub.state.asObservable()
    } else {
      return MapTables.state.forceCastedValue(forKey: self, default: self.createStateStream())
    }
  }
}

 

정확히 말하자면 매번 이 값을 사용하지는 않고, state를 가져오는데 타입 캐스팅이 실패했거나 하면 저 값을 쓰게 된다. (그래도 호출은 매번 하는 셈이다)

 

정리하자면,

  • Reactor는 disposeBag을 가진다. 하지만 Reactor의 disposeBag에 직접 접근하지는 못한다.
  • Reactor의 MapTables enum 값들(action, currentState, state, disposeBag, isStubEnabled, stub)은 Reactor가 deinit되면 함께 할당 해제된다.
  • disposeBag에 실제로 Disposable들이 담기는 시점은 state에 접근하는 시점으로 볼 수 있다.
  • Reactor.state 자체는 Observable<State>이다. State와 Reactor.state는 다르다. 뷰에서 사용하는 Reactor.state.map { $0 }의 state가 바로 이 Observable이다.
  • 이렇게 변형된 Observable<State>들이 Disposable로 변형된 것이 disposeBag에 담기게 된다.

 

한 가지 불분명한 게 있긴 하다. 외부에서 리액터의 state를 접근하는 때는 주로 뷰에서 리액터의 상태 값을 보고 뷰를 업데이트하는 상태 바인딩 시기이다.

createStateStream() 내부 구현을 보면, action을 관찰해서 Observable<Mutation>으로 변경, 그것을 reduce해서 Observable<State>로 변경하는데 그 과정에서 에러가 있거나 뭔가 잘못되면 .empty()가 반환된다. mutate 함수 등에서 생성하는 다른 API 콜 결과의 Observable들은 이 과정에 포함되어 함께 State로 퉁쳐져(?) disposeBag에 담기는 것 아닐까… 하는 생각을 해본다.

 

728x90
반응형
Comments