[UIKit] UI 업데이트 "해줘", 데이터 바인딩

iosSwiftUIKitData Binding
avatar
2025.04.11
·
8 min read

앱을 만들다 보면 종종 아래와 같은 상황을 겪게 된다.

버튼을 눌렀을 때 값을 어디에 알려줘야 하지?”

“ViewModel에서 가져온 데이터를 어떻게 View에 알려주지?”

“상태가 바뀌었는데, View는 왜 가만히 있지...?”

이럴 때 필요한 게 바로 데이터 바인딩으로, 데이터가 바뀌면 UI에 반영되도록 연결해주는 일종의 "파이프 라인"이다.

여기에서는 UIKit 환경에서 사용할 수 있는 대표적인 바인딩 방법을 알아보도록 하겠다.


Delegate

"너 무슨 일 생기면 나한테 알려줘!"

Delegate는 한 객체가 다른 객체의 행동을 위임받아 처리하는 방식으로, UIKit에서 자주 사용되는 방법이다. TableView, CollectionView, TextField 등 UIKit 컴포넌트들이 많이 사용한다. 예를 들어, 버튼을 눌렀을 때 특정 액션을 처리하는 데 사용된다.

protocol MyDelegate: AnyObject {
    func didTapButton()
}

class MyView: UIView {
    weak var delegate: MyDelegate?
    
    func buttonTapped() {
        delegate?.didTapButton()
    }
}

장점

  • 구조가 명확하고, 간단하다.

  • 메모리 관리에 익숙한 패턴이다. (weak delegate)

단점

  • 한 번에 하나의 이벤트만 처리 가능하다. 즉, 여러 컴포넌트가 같은 이벤트를 받아야 한다면 다른 방식이 필요하다.

  • 프로토콜을 만들고 연결하는 게 귀찮다.

  • weak로 참조해야 메모리 누수를 방지할 수 있다.

Closure

"끝나면 이 함수 호출해줘!"

Closure는 일회성 이벤트 처리를 위한 간단한 방법이다. 메서드나 함수에서 바로 인라인으로 사용할 수 있다. 버튼 클릭이나 네트워크 응답 처리 등에 사용된다.

class MyView: UIView {
    var onTap: (() -> Void)?
    
    func buttonTapped() {
        onTap?()
    }
}

장점

  • Delegate보다 간단하고 깔끔하다.

  • 클로저 내에서 상태를 쉽게 캡처할 수 있다.

  • 일회성 이벤트 전달에 좋다.

단점

  • 메모리 관리에 주의해야 한다.

  • 복잡한 데이터 흐름을 관리하기엔 한계가 있다.

NotificationCenter

"내가 한 일 듣고 싶은 사람 다 들어!"

NotificationCenter는 여러 객체에 이벤트를 전달하고자 할 때 사용하는 방식이다. 이벤트를 발생시킨 객체와 수신 객체가 서로를 알지 못해도 된다. 전역적으로 상태 변경이 필요할 때, 예를 들어, 앱 테마 변경이나 로그인 상태 변경 등을 처리할 때 유용하다.

NotificationCenter.default.post(name: .didLogin, object: nil)

NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleLogin),
    name: .didLogin,
    object: nil
)

장점

  • N:1N:1 또는 N:NN:N 구조를 지원한다. 즉, 여러 객체가 동일한 이벤트를 수신할 수 있다.

  • Loose coupling(느슨한 결합)을 지원한다. 발행자와 구독자가 서로를 모른 채 통신할 수 있다.

단점

  • 코드가 분산되어서 추적하기 어려운 경우가 있다.

  • 잘못 사용하면 앱이 복잡해져서 관리가 어려워질 수 있다.

  • 발행자와 구독자 간에 타입 검사가 컴파일 시점에 이루어지지 않기 때문에 타입 안정성이 떨어진다.

Property Observer (didSet, willSet)

"값 바뀌면 바로 반응해!"

속성 값이 변경될 때 자동으로 실행되는 코드 블록이다. 상태 변경을 감지하고 즉시 반응할 수 있다. 단일 객체 내에서 상태를 추적하거나 UI를 즉시 업데이트할 때 유용하다.

class MyClass {
    var score: Int = 0 {
        willSet {
            print("점수가 곧 \(newValue)점으로 바뀔 예정입니다.")
        }
        didSet {
            print("점수가 \(oldValue)점에서 \(score)점으로 바뀌었습니다!")
        }
    }
}

장점

  • 아주 간단하게 사용할 수 있다. 별도의 메서드나 설정없이 값을 변경할 수 있다.

단점

  • 외부 객체와의 바인딩에는 부적합하고, 같은 클래스 내에서만 효과적이다.

Reactive Programming (Combine, RxSwift)

"데이터가 바뀌면, 자동으로 따라갈게!"

반응형 프로그래밍은 선언형 프로그래밍 패러다임으로, 데이터 스트림과 구독을 통해 데이터를 자동으로 처리할 수 있다. 복잡한 비동기 이벤트 처리, 상태 변화 추적, 또는 여러 데이터를 조합해야 할 때 사용한다.

// Combine
let name = CurrentValueSubject<String, Never>("")
let cancellable = name.sink { newName in
    print("이름이 \(newName)으로 바뀌었습니다!")
}
name.send("춘장")
// RxSwift
let name = BehaviorSubject<String>(value: "")
name.subscribe(onNext: { newName in
    print("이름이 \(newName)으로 바뀌었습니다!")
}).disposed(by: disposeBag)
name.onNext("춘장")

장점

  • 복잡한 데이터 흐름을 선언적으로 관리할 수 있다.

  • 비동기 작업, 이벤트 스트림을 간결하게 처리할 수 있다.

단점

  • 학습에 시간이 걸릴 수 있다. (용어부터 헷갈리 수도..)

  • 작은 프로젝트에서는 오히려 오버 엔지니어링이 될 수 있다.







- 컬렉션 아티클