(RxSwift를 사용하지 않고)MVVM 패턴을 사용할 때, 특정 객체의 변화를 감지하고 대응하기 위해 커스텀 Observable 클래스를 사용하곤 한다.
Observable의 동작에 대해 간단히 설명하자면, 특정 값이 bind라는 메서드를 통해 closure라는 함수 타입의 프로퍼티에 전달이 되고, 그 값이 바뀔때 즉, didSet될 때 어떤 동작을 정의할 수 있다는 것인데.........
여기서 bind라는 함수가 존재하는 이유가 궁금해졌다.
뒤에서 코드를 더 자세히 보겠지만, 사실 bind함수 없이 closure라는 프로퍼티 하나만으로도 충분히 의도한 동작을 할 수 있기 때문이다.
일단 Obsevable에 대해 간단히 알아보겠다.
Observable의 컨셉과 구조
MVVM 패턴에 대한 설명은 더 잘 정리된 블로그들이 있으니 생략하고, Observable 클래스의 컨셉과 코드에 대해 살펴보겠다.
Observable의 가장 큰 컨셉은, 특정 값이 변경되었을 때 ViewModel에서 데이터의 가공을 하고, View에 값이 변경되었다고 바인딩을 해주는 것이다.
이러한 데이터 바인딩에 쓰이는 객체가 Observable이라는 클래스이다.
기본적이 구조는 다음과 같다.
class Observable<T> {
// 값이 변경된다면 실행할 메서드가 들어갈 프로퍼티
private var closure: ((T) -> Void)?
// didSet을 통해 value가 변하면 closure의 메서드가 실행됨
var value: T {
didSet {
closure?(value)
}
}
init(_ value: T) {
self.value = value
}
// 클로저로 들어온 메서드를 closure프로퍼티에 전달
func bind(closure: @escaping (T) -> Void) {
closure(value)
self.closure = closure
}
// 클로저로 들어온 메서드를 실행시키 않을 때 사용(초기값 설정 X)
func lazyBind(closure: @escaping (T) -> Void) {
self.closure = closure
}
}
동작을 살펴보면, Observable로 들어온 value가 변경될 때(didSet) 어떤 행동(메서드 실행)을 할건지 정의해주는거라고 생각하면 쉽다.
즉, 다음과 같이 사용하면
// ViewModel
// ...
struct Input {
// 뷰컨에서 사용자에 의해 받아온 값 그 자체
// 실시간으로 받아온 데이터를 didSet으로 대응하기 위해서 Observabl 사용
var field: Observabl<String?> = Observabl(nil)
}
struct Output {
// VC의 레이블에 보여줄 최종 텍스트
var text = Field("")
}
// ...
// 데이터 가공을 위한 메서드
func transform() {
input.field.bind { text in // text는 VC의 TextField.text의 값이 value를 통해 들어온 것
print("inputField", text ?? "")
self.validation()
}
}
private func validation() {
// input으로 들어온 text라는 value에 대한 유효성 검사
// ...
output.text = "유효성 검사를 마친 텍스트"
}
// ViewController
// ...
override func viewDidLoad() {
// viewModel에서 output으로 나온 text를 특정 Label에 표시
viewModel.output.text.bind { text in
print("outputText", text)
self.formattedLabel.text = text
}
}
이런식으로 Observable이라는 객체를 이용해서, 값의 변경을 감지하고 특정 행동을 할 수 있는 것인데...
Observable의 구조를 보면 사실 bind라는 메서드를 사용하지 않고, 바로 clousure라는 프로퍼티를 통해 같은 동작을 할 수 있다.
bind 메서드의 역할은 그저 closure에 받은 메서드를 넘겨주는 역할밖에 하지 않기 때문이다.
bind 메서드를 사용하는 이유
bind 메서드를 사용하는 이유에 대해선 다양한 의견이 있을 수 있다.
먼저, 내가 생각했을때는 bind함수 내에서 클로저로 들어온 함수를 먼저 실행해주면서 초기값을 다룰 수 있는 동작을 한다는 점이 이유라고 생각했다.
func bind(closure: @escaping (T) -> Void) {
closure(value) // 클로저로 들어온 함수를 먼저 실행함
self.closure = closure
}
이렇게 하면 closure를 통해서 값이 변경되었을 때 뿐 아니라, 값이 변경되기 이전의 value도 다룰 수 있다는 점이었다.
그러나, 사실 초기 실행이야 굳이 여기서 할 필요도 없기도 하고 크게 와닿지 않는 이유였다.
이 외에도 다른 의견들 중에는 코드의 정리를 위해서라는 의견도 있고, 캡슐화와 은닉화를 위해서라는 의견도 있었다.
어느정도 다 맞는 말이지만, 제일 와닿는 이유는 다음과 같았다.
closure라는 프로퍼티를 private 접근제어를 통해 외부에서 접근하지 못하게 하기 위함이라는 이유이다.
// private을 통해 Observable 클래스 밖에서 접근하지 못하게 함
private var closure: ((T) -> Void)?
그럼 외부에서 이 프로퍼티에 접근하지 못하면 어떤 이점이 있을까?
사실... 엄청난 이유가 있는건 아니고, 메모리적인 측면에서의 이점이 있기 때문이다.
만약 closure 프로퍼티가 private이 아니라면, ViewController의 입장에서 언제든지 이 프로퍼티에 접근할 준비를 하고있는 것이고, private으로 애초에 접근하지 못하게 하면 이러한 리소스를 아낄 수 있고 메모리적으로도 이점이 있다고 한다.
메모리 측면에서의 이점.. 다 좋은데 이번엔 이런 의문이 들었다.
"그럼 bind 메서드에 접근하는것도 마찬가지 아닌가?"
이는 함수의 생명주기와도 관련이 있었다.
bind의 역할을 살펴보면, 매개변수로 받은 메서드를 closure라는 프로퍼티에 전달을 하는게 전부이다.
이 역할을 마친 bind는 여기서 생명주기를 마치게 되고 메모리에서 내려간다.
반면 closure라는 프로퍼티에 접근하는것은 언제 메모리에서 내려갈지 보장이 되어있지 않고, 이러한 이유 때문에 끝나는 시점이 보장되어있는 bind 메서드에 접근하는것이 더 효율적이라는 결론을 얻었다.
사실 이러한 결론을 내리면서 캡슐화와 은닉화, 그리고 메모리 구조에 대해서 더 공부를 할 필요가 있다는 생각을 했다.
당연하게 쓰는 코드가 아니라, 이유를 알고 쓰기 위해 조금 더 공부하고 다시 한 번 더 정리를 해야겠다.
※ 참고자료
[iOS/Swift] MVVM 패턴의 Data Binding에 대해서 알아보자! (Closure, Observable, Combine)
MVVM 란? Model-View-ViewModel로 구성된 아키텍처 패턴 중 하나로, 데이터를 처리하는 모델(Model), 사용자에게 보여지는 UI인 뷰(View), 뷰에 바인딩되어 모델과 뷰 사이를 이어주는 뷰-모델(View Model)로 분
ios-daniel-yang.tistory.com
https://velog.io/@jeon0976/iOSUIKit-Custom-Reactive-Programming-Make-Observable-In-UIkit
[iOS/UIKit] Reactive Programming - Custom Observable In UIkit
주제: Observable과 Subscribe 만들기
velog.io
https://medium.com/@geun2121/swift-mvvm-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-c9a9f9e48631
[SWIFT] MVVM 디자인 패턴
I. MVVM Design Pattern 의 탄생배경
medium.com
'iOS > Swift' 카테고리의 다른 글
| [iOS/Swift] MVVM의 한계에 대한 고찰 - Massive View Model 해결법 (0) | 2025.02.11 |
|---|---|
| [iOS/Swift] ARC - Swift의 메모리 관리 기법 그런데 이제 weak을 곁들인 (0) | 2025.02.08 |
| [iOS/Swift] WMO와 Method Dispatch - class에 final을 붙이는 진짜 이유 (0) | 2025.01.25 |
| [iOS/Swift] GCD 골든벨 - 다양한 상황에서의 동기/비동기처리 출력결과 예측하기 (0) | 2025.01.22 |
| [iOS/Swift] @escaping - 네트워킹 코드에서 함수의 반환값을 사용하지 않는 이유 (0) | 2025.01.19 |