
어제 고민했던 내용에 대해 어느정도 답을 얻어서, Model 클래스에 Relay 값 변경을 위한 메서드를 추가하였다.
import Foundation
import RxSwift
import RxCocoa
class SettingsModel {
private let disposeBag = DisposeBag()
private let themeModeRelay = BehaviorRelay<ThemeMode>(value: UserDefaults.standard.themeMode)
private let temperatureUnitRelay = BehaviorRelay<TemperatureUnit>(value: UserDefaults.standard.temperatureUnit)
private let petTypeRelay = BehaviorRelay<PetType>(value: UserDefaults.standard.petType)
init() {
// Relay의 값이 변경될 때마다 UserDefaults에 저장
themeModeRelay
.subscribe(onNext: { mode in
UserDefaults.standard.themeMode = mode
})
.disposed(by: disposeBag)
temperatureUnitRelay
.subscribe(onNext: { unit in
UserDefaults.standard.temperatureUnit = unit
})
.disposed(by: disposeBag)
petTypeRelay
.subscribe(onNext: { type in
UserDefaults.standard.petType = type
})
.disposed(by: disposeBag)
}
// Observable로 접근 제공
var themeModeObservable: Observable<ThemeMode> {
return themeModeRelay.asObservable()
}
var temperatureObservable: Observable<TemperatureUnit> {
return temperatureUnitRelay.asObservable()
}
var petTypeObservable: Observable<PetType> {
return petTypeRelay.asObservable()
}
// Relay 값 변경을 위한 메서드
func updateThemeMode(_ mode: ThemeMode) {
themeModeRelay.accept(mode)
}
func updateTemperature(_ unit: TemperatureUnit) {
temperatureUnitRelay.accept(unit)
}
func updatePetType(_ type: PetType) {
petTypeRelay.accept(type)
}
}
어제 고민했던 부분이 ‘이렇게 메서드를 추가하면 Relay가 internal일때와 별 차이 없는 것이 아닌가?’ 였는데,
생각해보니 과거에 private 프로퍼티에 접근할 수 있도록 만든 메서드들도 마찬가지였다.
internal일 때처럼 접근 가능하긴 하지만, 바로 접근 가능한 것과, 메서드를 거쳐서 접근하는 것은 차이가 있는 것이다.
internal은 외부에서 아무 제약 없이 접근 가능하기 때문에, accept를 통해 Relay의 값을 임의로 변경하면, 변경 과정을 추적하기 어렵고 예상치 못한 버그가 발생할 가능성이 높다.
반면 private으로 제한을 주고 메서드를 통해서만 접근하게 한다면 변경의 흐름을 추적하기 쉬워지고, 실수로 접근해서 값을 변경할 위험도 없앨 수 있다.
이것 때문에 캡슐화가 의미가 있는 것이었는데, 이 부분을 생각하지 못했던 것이다.
// ViewModel 클래스
func transform(_ input: Input) -> Output {
// Input 처리: View에서 받은 이벤트를 Model의 메서드를 호출하여 Relay 값을 업데이트
input.toggleMode
.subscribe(onNext: { [weak self] mode in
self?.settingsModel.updateThemeMode(mode) // 모드 변경
})
.disposed(by: disposeBag)
input.tapTemperature
.subscribe(onNext: { [weak self] unit in
self?.settingsModel.updateTemperature(unit) // 온도 단위 변경
})
.disposed(by: disposeBag)
input.tapPetType
.subscribe(onNext: { [weak self] type in
self?.settingsModel.updatePetType(type) // 동물 이미지 종류 변경
})
.disposed(by: disposeBag)
// Output 생성: Model의 Observable을 Driver로 변환하여 UI에서 사용
let themeMode = settingsModel.themeModeObservable
.asDriver(onErrorJustReturn: .light) // onErrorJustReturn: 에러 발생 시 기본값 반환
let temperatureUnit = settingsModel.temperatureObservable
.asDriver(onErrorJustReturn: .celsius)
let petType = settingsModel.petTypeObservable
.asDriver(onErrorJustReturn: .dog)
return Output(themeMode: themeMode, temperatureUnit: temperatureUnit, petType: petType)
}
ViewModel 클래스의 transform메서드도 수정해주었다.
Model의 Relay를 통해 UserDefaults의 값을 업데이트 해주기 위해 Model의 Relay 접근 메서드를 호출하도록 하였다.
이렇게 하면 데이터의 흐름을 다음과 같이 정리할 수 있다.
Input 구조체에 담아 ViewModel에 전달transform() 메서드에서 전달받은 Input을 구독updateThemeMode)에 전달하여 Relay 값 업데이트Relay를 Observable 형태로 구독하여 UI에서 사용할 Driver로 변환Output 반환BehaviorRelay로 상태 값 저장 (예: 테마 모드, 온도 단위 등)Relay 값이 변경될 때마다 UserDefaults에 저장Output 데이터를 구독하여 UI를 실시간으로 업데이트 (예: 테마 적용, 단위 변환된 값 표시)이렇게 하여 View → ViewModel → Model로 이어지는 Input 흐름과, Model → ViewModel → View로 이어지는 Output 흐름으로 데이터를 관리할 수 있게 되었다.
MVVM 아키텍처와 RxSwift에 더해 Input/Output 구조에 대해 공부하면서 작성하느라 어려울 거라고 생각했는데, 이해하고 나니 오히려 Input/Output 없이 그냥 작성할 때보다 데이터 흐름이 더 눈에 잘 들어오고 정돈된 느낌을 받았다.
거기에 더해 라이트 모드와 다크 모드 전환을 구현하느라 UIApplication.shared.windows 와 UIApplication.shared.windows 에 대해 찾아보면서, window와 Scene에 대해서도 알게 되었고,
UITraitCollection라는 객체, SnapKit의 remakeConstraints 같은 여러 메서드들이 더 있다는 것도 알게 되었다.
공부하며 구현하느라 많이 느렸긴 했는데, 덕분에 구현하는 재미를 오랜만에 느낀 것 같다.