본문 바로가기
iOS📱/Common

[디자인패턴] Data Binding ( MVVM )

by 텅빈비니 2025. 2. 11.
반응형

안녕하세요🐶

빈지식 채우기의 비니🙋🏻‍♂️ 입니다.

 

이전에! MVVM에서의 필수적인 첫 번째 요소인 Command 패턴에 대해 알아보았습니다.

이번 시간에는 두 번째 필수 요소인 Data Binding에 대해 알아보도록 하죠!

 

아래 포스팅을 보고 오시면 더욱 이해가 쉬울 수 있습니다!

 

[디자인패턴] MVC, MVP, MVVM

안녕하세요🐶빈지식 채우기의 비니🙋🏻‍♂️ 입니다. 바로 오늘의 주제에 대해 아래 대화로 알아보도록 하죠! 👨🏻‍💼 : 안녕하세요 여러분~ 우리가 유지보수와 개발 효율 상승을 위해

beanistory.tistory.com

 

 

[디자인패턴] 커맨드 패턴 ( Command Pattern )

안녕하세요🐶빈지식 채우기의 비니🙋🏻‍♂️ 입니다. 오늘도 역시 학생과 선생님의 대화로 주제를 알아보도록 하겠습니다. 👨🏻‍💼 : 비니 학생, 혹시 MVVM 패턴에서 가장 중요한 요소에

beanistory.tistory.com


1. Data Binding

두 개의 데이터 소스 ( View, View Model ) 를 함께 연결하고 동기화 상태를 유지하는 일반적인 기술

 

바로 예제를 통해 알아볼건데,

여기서는 View Model (데이터 변경), View ( 데이터 변경 체크 및 업데이트 ) 로 만들 예정입니다.

 

여러 방법 중 Closure, Observable, Combine 를 이용한 예제를 준비했습니다.


2. 시계 예제

MVVM 패턴으로 간단한 시계를 만들어 봅니다.

Closure, Observable, Combine 세 가지 방법으로 Data Binding 을 알아보도록 하겠습니다.


2-1. 기본 설정

프로젝트 구조

 

프로젝트 구조는 다음과 같습니다.

  • Controller : View Model 데이터 변화를 감지해 업데이트를 진행하는 View Controller 
  • View Model : Model과 함께 데이터 변화를 담당
  • Model : Data Binding에 사용되는 데이터 모델 정의
  • Observable : Observable 사용하기 위한 관찰자 정의

뷰 설정

// 모델 (Model)
class Clock {
    static var currentTime: (() -> String) = {
        let today = Date()
        
        let hours = Calendar.current.component(.hour, from: today)
        let minutes = Calendar.current.component(.minute, from: today)
        let minStr = String(format: "%02d", minutes)
        let seconds = Calendar.current.component(.second, from: today)
        let secStr = String(format: "%02d", seconds)
        return "\(hours):\(minStr):\(secStr)"
    }
}

 

// 뷰 컨트롤러
class ClockViewController: UIViewController {
    @IBOutlet weak var closureLobel: UILabel!
    @IBOutlet weak var ObservableLabel: UILabel!
    @IBOutlet weak var combineLabelPupblished: UILabel!
    @IBOutlet weak var combieLabelSubject: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        startTimer()
    }
    
    // 매 초마다 시간을 업데이트
    private func startTimer() {
        
        // 매 초마다 뷰 모델 CheckTime 함수 호출
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            
        }
    }
}

2-2. Closure

먼저 Closure을 사용하여 가장 간단한 Data Binding을 구현해보도록 하겠습니다.

//
//  ClockViewModel.swift
//

import Foundation

// 뷰 모델 정의
class ClockViewModel {
    
    // 프로퍼티 옵저버
    // 매 초마다 CheckTime 호출 -> closureTime 값 저장
    // 저장될떄마다 didSet 호출
    var closureTime: String {
        didSet {
            didChangeTime?(self)
        }
    }
    
    // 생성 시 초기 시간을 노출
    init() {
        closureTime = Clock.currentTime()
    }
    
    // 매 초마다 호출 -> closureTime 값 저장
    func checkTime() {
        closureTime = Clock.currentTime()
    }
}
  • 매 초마다 CheckTime() 이 호출 되면서 closureTime 값이 변경된다.
  • 프로퍼티 옵저버가 이를 인지해서 뷰의 데이터를 업데이트 한다. ( Data Binding )
//
//  ClockViewController.swift
//

import Foundation
import UIKit

class ClockViewController: UIViewController {
    @IBOutlet weak var closureLobel: UILabel!
    @IBOutlet weak var ObservableLabel: UILabel!
    @IBOutlet weak var combineLabelPupblished: UILabel!
    @IBOutlet weak var combieLabelSubject: UILabel!
    
    // 뷰 모델 객체 정의
    private var viewModel = ClockViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setBindings()
        startTimer()
    }
    
    // 매 초마다 시간을 업데이트
    private func startTimer() {
        
        // 매 초마다 뷰 모델 CheckTime 함수 호출
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.viewModel.checkTime()
        }
    }
    
    func setBindings() {
        
        // 뷰 모델 클로저 변수에 수행될 동작을 저장
        viewModel.didChangeTime = { [weak self] viewModel in
            self?.closureLobel.text = viewModel.closureTime
        }
    }
}
  • setBindings 를 통해 didChangeTime을 정의한다 ( 수행되는 동작을 정의 )

2-3. Observable

변화를 관찰하며 동작하는 Observable 클래스를 생성한다.

//
//  Observable.swift
//

import Foundation

class ClockObservable<T> {

    typealias Obs = (T) -> Void
    
    var value: T? {
        didSet {
            self.observable?(value!)
        }
    }
    
    // 클로저를 통해 동작을 담아줄 변수를 생성
    private var observable: (Obs)?
    
    init(_ value: T?) {
        self.value = value
    }
    
    func bind(_ listener: @escaping Obs) {
        self.observable = listener
    }
}
  • value 값이 변경하면 프로퍼티 옵저버를 이용하여 listener(observable) 동작 수행
//
//  ClockViewModel.swift
//

import Foundation
import Combine

// 뷰 모델 정의
class ClockViewModel {
    
    var observableTime: ClockObservable<String> = ClockObservable("Observable")
    
    // 생성 시 초기 시간을 노출
    init() {
        observableTime.value = Clock.currentTime()
    }
    
    // 매 초마다 호출 -> closureTime 값 저장
    func checkTime() {
        observableTime.value = Clock.currentTime()
    }
}
  • 매 초마다 observable 클래스의 value 값이 변경된다.
  • ClockObservable 클래스의 프로퍼티 옵저버가를 통해 listener(observable) 동작을 수행한다
//
//  ClockViewController.swift
//

import Foundation
import UIKit

class ClockViewController: UIViewController {
    @IBOutlet weak var closureLobel: UILabel!
    @IBOutlet weak var ObservableLabel: UILabel!
    @IBOutlet weak var combineLabelPupblished: UILabel!
    @IBOutlet weak var combieLabelSubject: UILabel!
    
    // 뷰 모델 객체 정의
    private var viewModel = ClockViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setBindings()
        startTimer()
    }
    
    // 매 초마다 시간을 업데이트
    private func startTimer() {
        
        // 매 초마다 뷰 모델 CheckTime 함수 호출
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.viewModel.checkTime()
        }
    }
    
    func setBindings() {
      
        // 클로저 내부 동작을 Observable에 저장
        viewModel.observableTime.bind { [weak self] time in
            self?.ObservableLabel.text = time
        }
    }
}
  • 매 초마다 checkTime() 함수를 호출하여 ClockObservable 클래스의 value 값을 변경한다.
  • bind 를 통해 수행되는 동작을 정의한다.

2-4. Combine

Combine 을 통해서도 Data Binding 을 구현해볼 수 있다.

아래 포스팅을 참고하면 이해하기 더 쉬울 것이다.

 

[Combine] Combine 이란?

안녕하세요🐶 빈지식 채우기의 비니🙋🏻‍♂️입니다. 요즘 RxSwift 공부를 통해 비동기 프로그래밍에 대해 블로그도 작성하고 있는데, First-Party 인 Combine 이라는 것이 있더라구요! . 마찬가지

beanistory.tistory.com

Combine 을 작업할 때,

Published 방식과 Subject 방식 두 방법으로 작업을 해보았다.

//
//  ClockViewModel.swift
//

import Foundation
import Combine

// 뷰 모델 정의
class ClockViewModel {

    // Subject를 통해 데이터를 제공
    var combineTimeSubject = CurrentValueSubject<String, Never>("Combine")
    
    // @Published를 통해 데이터를 제공
    // 연산자 $을 통해 Published 속성에 접근
    @Published var combineTimePublished: String = "Combine"
    
    // 생성 시 초기 시간을 노출
    init() {
        combineTimeSubject.value = Clock.currentTime()
        combineTimePublished = Clock.currentTime()
    }
    
    // 매 초마다 호출 -> closureTime 값 저장
    func checkTime() {
        combineTimeSubject.value = Clock.currentTime()
        combineTimePublished = Clock.currentTime()
    }
}
  • Subject 와 Published 가 적용된 변수를 각각 선언한다.
//
//  ClockViewController.swift
//  

import Foundation
import UIKit
import Combine

class ClockViewController: UIViewController {
    @IBOutlet weak var closureLobel: UILabel!
    @IBOutlet weak var ObservableLabel: UILabel!
    @IBOutlet weak var combineLabelPupblished: UILabel!
    @IBOutlet weak var combieLabelSubject: UILabel!
    
    // Combine Published 취소 저장 객체 정의
    private var cancellables: Set<AnyCancellable> = []
    
    // Combine Subject Sink 동적 정의
    var combineSink: AnyCancellable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setBindings()
        startTimer()
    }
    
    // 매 초마다 시간을 업데이트
    private func startTimer() {
        
        // 매 초마다 뷰 모델 CheckTime 함수 호출
        Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.viewModel.checkTime()
        }
    }
    
    func setBindings() {
        
        // Combine 로직 수행 ( Subject 사용 )
        combineSink = viewModel.combineTimeSubject.sink { [weak self] value in
            self?.combieLabelSubject.text = value
        }
        
        // Combine 로직 수행 ( Publihsed 사용 )
        viewModel.$combineTimePublished
            .compactMap { String($0) }     // compactMap 을 통해 데이터 가공을 한다 ( 옵셔널 제거 )
            .assign(to: \.text, on: combineLabelPupblished)   // assign 방식은 keyPath 형식으로 객체 접근, Label에 값 없데이트
            .store(in: &cancellables)      // 취소할 동적을 저장
    }
}
  • Subject의 경우, sink 라는 메서드로 값의 변화를 감지해 Closure 로 정의된 동작을 수행한다.
    • 해당 소스의 경우 combineSink 라는 변수를 선언했는데, sink로 선언된 동작을 저장하기 위해서이다.
  • Published 의 경우, compactMap 과 assign 을 통해 데이터를 가공하여 뷰를 업데이트를 한다.

2-5. 결과

결과

다음과 같이 결과가 나오는 것을 확인할 수 있습니다.


이 세 가지의 모든 방법의 공통점은 아래와 같다.

직접 View 가 View Model의 값에 접근하지 않고 값의 변화를 감지해 유동적으로 업데이트 한다.
이것이 Data Binding 이다.

 

감사합니다.


참고

 

반응형