2025. 2. 21. 14:00ㆍDeveloper Basis/내가 보려고 정리하는 개발 용어 사전
iOS 개발을 하면서 데이터 바인딩 (Data Binding)이라는 용어를 정말 많이 사용하는 것 같아요.
메서드 이름도 setupDataBind()와 같이 만들고,
아키텍처를 설명하거나 어떤 특정한 UI 컴포넌트를 만들 때도 항상 데이터 바인딩은 어쩌구 저쩌구 수행한다고 소개했던 것 같고...
그런데 여러분은 iOS 개발에서 데이터 바인딩이 정확하게 무엇을 의미하는 용어인지 알고 사용하셨나요?정확하게 알고 계셨다면 다행이고요!
이번 글에서는 대략적으로 데이터 바인딩이라는 용어의 느낌은 알지만,
"정확하게 데이터 바인딩이 딱 무엇이다" 라고 공부해 본 적은 없는 분들을 위해 깔끔하게 개념 정리를 해볼 수 있는 글을 준비해 봤습니다!
데이터 바인딩 (Data Binding)이란?
bind = 묶다, (붕대 등으로) 감다 [싸다], 결속시키다, 엉키다 (출처 : 옥스퍼드 영문사전)
데이터 바인딩(Data+Binding)을 직역해보면 "데이터를 묶는다".
조금 더 나아가서 생각해 "데이터를 어떤 무엇과 결속시키고 엉키게 하는, 즉 연결의 의미"가 느껴지실 겁니다.
iOS 개발에서 사용되는 데이터 바인딩 (Data Binding)의 데이터와 연결되는 "어떤 무엇"은 UI를 의미합니다.
생각을 해봅시다.
우리가 사용하는 iOS 애플리케이션 화면은 버튼, 텍스트, 이미지 등 기본적인 UI 컴포넌트를 포함해서,
시트 (Sheet), 피커 (Picker), 리스트 (List), 테이블/컬렉션 뷰 등 상대적으로 복잡한 UI 컴포넌트까지 다양한 요소들이 합쳐져 만들어집니다.
그런데, 이 화면들을 만들기 위해 필수적으로 필요한 것이 바로 데이터 (Data) 이죠.
예시를 들어볼까요?
글자를 보여주는 텍스트, 라벨은 어떤 글자를 화면에 표출할지에 대한 String 값 데이터가 있어야 할 것이고,
리스트 화면도 각 리스트 셀을 구성하는 이미지 URL이나, 텍스트를 구성할 String 등 여러 개의 데이터가 있어야 그릴 수 있을 것입니다.
사용자로부터 입력을 받는 텍스트 필드 역시 사용자부터 어떤 입력 (Input)을 받으면 > 그 입력에 맞는 String이나 Int 등의 데이터를 추가해 > 화면에 최종적으로 표출하게 되는 것이죠.
이처럼 UI는 항상 데이터와의 연결을 기반으로 동작합니다.
그리고 너무나도 당연하게, 이 UI는 데이터가 변경될 때마다 함께 새로 업데이트가 되어야 할 거에요!
만약 사용자가 키보드로 새로운 숫자를 입력했는데 (= 데이터가 업데이트 되었는데)
화면은 데이터의 변화에 따라 새로 업데이트되지 않는다면? (= 입력된 새로운 데이터가 UI에 반영되지 않는다면?)
아무래도 사용자는 '해당 애플리케이션이 어딘가 문제가 있구나' 하며 앱을 끄거나, 지워버릴 가능성이 높을 겁니다.
즉, 데이터가 업데이트됨에 따라 UI, 즉 사용자가 보고 있는 화면도 동시에 업데이트가 이루어지는 과정이 필수라는 의미죠!
이러한 UI와 데이터와의 연결 관계를 우리는 동기화 (sync)라고 부릅니다.
*여기서의 동기화는 조화를 이룬다는 사전적 의미에서 더 나아가, 상태를 일치시키고 동일하게 유지한다는 "동기"의 의미로 사용된다고 보면 될 것 같아요.
💡 iOS 개발에서의 데이터 바인딩 (Data Binding)이란 데이터와 UI를 연결하여, 동기화하는 것을 의미합니다.
만약 데이터 바인딩이 없었다면?
데이터 바인딩 (Data Binding)에서 사용되는 UI와 데이터 간 연결 관계가 의미하는 것은,
단순한 연결 그 이상의 동기화가 이루어지는 관계를 의미한다고 볼 수 있습니다.
아래 코드를 한번 볼까요?
전통적인 iOS 개발 방식인 UIKit에서 UI (UILabel)와 데이터 (Int)가 어떻게 상호 동기화가 이루어지는지를 볼 수 있습니다.
- exampleLabel은 currentCount라는 Int 타입의 값을 참고하여, "Current Count: \(count)"라는 text를 화면에 표출하는 UILabel입니다.
- UILabel과 데이터는 setupDataBind()라는 이름의 메서드를 통해 연결됩니다. (viewDidLoad 부분)
- 버튼이 Tap 되면 데이터의 값이 변화 (currentCount += 1)됩니다.
자 위에서 말했듯이 데이터의 값이 변화된다면 -> 그에 따라 UILabel 값도 새롭게 업데이트되어야 할 겁니다.
UILabel의 값을 업데이트시키는 부분은 데이터 바인딩 메서드 (setupDataBind)에 구현되어있었고요.
그렇기 때문에 버튼의 탭 액션을 인식하는 buttonTapped(_) 메서드에
우리가 직접 setupDataBind 메서드를 호출함으로써, 데이터와 UI의 동기화를 직접 수동으로 구현해주게 된 것입니다.
final class ExampleViewController: UIViewController {
// UI
private let exampleLabel = UILabel()
private let exampleButton = UIButton()
// Data
private var cureentCount: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
setupDataBind()
}
@IBAction func buttonTapped(_ sender: UIButton) {
cureentCount += 1
setupDataBind() // 직접 수동으로 데이터 바인딩을 해줘야 한다.
}
func setupDataBind() {
label.text = "Current Count: \(count)"
}
}
그래 이렇게 하면 되잖아? 라고 생각할 수도 있지만,
만약 한 화면에 담긴 UI 요소가 몇 십개로 늘어나고
데이터의 변화가 단순한 버튼 탭 액션이 아니라, 다른 액션이나 네트워크 통신 등등 다양한 경로를 통해 이루어진다면...?
그럴 때마다 아마 UI를 갱신하는 메서드를 호출하는 것은 매우 불편하고 - 실수를 유발할 수 있는 일이 될 가능성이 매우 높겠습니다.
즉 이러한 문제점에서 시작된 것이
"데이터가 변경되면, UI는 자동으로 이를 감지하여 반영할 수 있도록 하면 어떨까?"라는 생각이었죠.
이것을 프로그래밍 용어로는 반응형 프로그래밍 (Reactive Programming)이라고 표현하는 것입니다.
*이 글에서는 반응형 프로그래밍의 개념에 대해 깊게 다루지는 않을 거예요!
**예전 글에서 설명했던 옵저버 패턴 (Observer Pattern)이 반응형 프로그래밍의 패러다임의 큰 틀을 따르기에, 참고할 수 있을 것 같네요!
💡 iOS 개발에서의 데이터 바인딩 (Data Binding)은 데이터의 변화를 감지하여, 이에 따라 자동으로 UI가 업데이트되는 반응형 프로그래밍 (Reactive Programming)의 특성을 가지고 있어야 합니다.
즉, 무슨 말이 하고 싶었던 것이냐면요!
위의 UILabel과 setupDataBind() 메서드로 데이터를 수동으로 동기화시키는 코드는
엄밀하게 말해 iOS 개발 관점에서 데이터 바인딩 (Data Binding)이 이루어졌다고 말할 수는 없다는 것입니다!
데이터가 바뀌었지만, 이에 따라 UI가 업데이트되는 과정이 "자동"으로 이루어지지 않고
직접 메서드를 호출하는 "수동"으로 이루어졌기 때문이죠.
SwiftUI에서의 데이터 바인딩
SwiftUI에서는 데이터 바인딩을 다양한 프로퍼티 래퍼 (Property Wrapper)를 기반으로 지원하고 있습니다.
특히 SwiftUI에서는 여러 개의 프로퍼티 래퍼를 지원함으로써,
개별적인 방식으로 데이터의 상태를 관리할 수 있고, SwiftUI 기반 View (= UI)와 프로퍼티 (= 데이터)의 상태를 동기화할 수 있다고 하는데요!
어떤 종류의 프로퍼티 래퍼들이 있는지 살펴보도록 하죠.
@State (단일 뷰의 상태 값)
View 내부의 상태를 관리하기 위한 용도로는 @State가 사용됩니다.
해당 프로퍼티의 값이 변경되면 View가 다시 그려지는 방식이죠.
주로, UI 요소의 활성화 여부 (isOn, isShowed)를 관리할 때 사용되구요.
View 내부에서만 사용되기 때문에 접근 제어를 private로 설정해서 사용하도록 Apple이 권장하고 있습니다. (외부에서 수정해서는 안됩니다.)
struct ContentView: View {
@State private var isOn = false
var body: some View {
Toggle("Switch", isOn: $isOn)
Text(isOn ? "On" : "Off")
}
}
@Binding (뷰 간 전달되는 값)
부모 View에서 자식 View로 전달되는 @State 값을 관리하기 위한 용도로는 @Binding이 사용됩니다.
앞선 @State는 private로 선언하여, 외부에서 수정을 할 수 없도록 한다고 했었죠.
@Binding으로 받은 @State 값은 자식 뷰에서 직접 수정할 수 있도록 만든다는 특징이 있습니다.
이는 자식 뷰에서 데이터를 직접 소유하도록 만드는 것이 아니라, 부모 뷰의 데이터 소스를 참조할 수 있게 되기 때문입니다.
이때 부모 뷰에서 자식 뷰로 값의 전달이 이루어질 때는 $ 접두사를 붙여 전달하게 된다는 점! 잊지 마세요!
struct ParentView: View {
@State private var isOn = false
var body: some View {
ChildView(isPlaying: $isOn)
}
}
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
Button(isOn ? "Pause" : "Play") {
isPlaying.toggle()
}
}
}
@StateObject (단일 뷰의 객체 상태)
@State에 Object가 추가된 이름입니다.
정말 말 그대로, View의 상태를 관리하는데 관리하는 상태가 객체 (Object)라는 의미죠!
관리하는 대상이 되는 객체는 ObservableObject를 채택하고 있어야 한다는 특징이 있습니다.
View가 처음 생성되면 동시에 ObservableObjcet 인스턴스도 만들어지는데, @StateObject는 이 만들어진 인스턴스를 직접 소유하고 관리합니다.
즉, View의 생명주기 동안 해당 객체를 유지하며, 계속 관찰하고 렌더링하게 되는 것이죠.
class Counter: ObservableObject {
@Published var count = 0
}
struct ContentView: View {
@StateObject private var counter = Counter()
var body: some View {
VStack {
Text("Count: \(counter.count)")
Button("Increment") {
counter.count += 1
}
}
}
}
@ObserverdObject (외부 소유의 객체 상태)
@ObservedObject는 @StateObject와 @Binding의 개념이 섞여있다고 생각하면 될 것 같아요.
관찰하고자 하는 객체 (ObservableObjcet)를 직접 소유하지 않고, 외부에서 주입 (Injection) 받아서 사용한다고 할 때 사용하기 때문이죠.
즉, @ObservedObject는 전달하는 대상이 @StateObject로 소유하고 있는 ObservableObject 객체를 전달한다는 점!
단, 부모뷰에서 자식뷰로 @StateObjcet를 전달할 때는 @State처럼 $ 접두사를 붙이지 않고 전달합니다.
class Counter: ObservableObject {
@Published var count = 0
}
struct ParentView: View {
@StateObject private var counter = Counter()
var body: some View {
ChildView(counter: counter)
}
}
struct ChildView: View {
@ObservedObject var counter: Counter
var body: some View {
VStack {
Text("Count: \(counter.count)")
Button("Increment") {
counter.count += 1
}
}
}
}
@EnvironmentObject
사용자가 정의한 데이터 모델 (ObservableObjcet)을 전역적으로 사용하기 위해서는 @EnvironmetObject가 사용됩니다.
여러 뷰에서 사용되는 데이터 객체가 있다고 할 때, 이를 매번 전달하는 과정은 매우 비효율적이겠죠?
즉, 부모 뷰에서 ObservableObject를 단 한 번만 생성하고, 하위 뷰에서는 자유롭게 사용할 수 있도록 만드는 프로퍼티 래퍼입니다.
class AppSettings: ObservableObject {
@Published var username = "Guest"
}
struct ParentView: View {
@StateObject private var settings = AppSettings()
var body: some View {
ChildView()
.environmentObject(settings)
}
}
struct ChildView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
VStack {
Text("Username: \(settings.username)")
Button("Change Username") {
settings.username = "NewUser"
}
}
}
}
@Environment (시스템 환경 값)
@Environment는 SwiftUI가 제공하는 시스템의 환경 값이나, 사용자가 정의한 환경 값을 접근하기 위해 사용되는 속성입니다.
주로, 라이트모드/다크모드 같은 설정이나, 화면의 방향과 같은 값들이 해당되겠죠.
@Environment로 접근할 수 있는 속성은 아래 공식문서를 통해 직접 확인하실 수 있습니다!
EnvironmentValues | Apple Developer Documentation
A collection of environment values propagated through a view hierarchy.
developer.apple.com
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
Text(colorScheme == .dark ? "Dark Mode" : "Light Mode")
}
}
지금까지 나온 SwiftUI의 프로퍼티 래퍼를 총 정리해 보면 아래와 같이 구분할 수 있겠군요!
그럼 UIKit에서는 데이터 바인딩을 어떻게 구현하는 건데?
SwiftUI에서는 이처럼 다양한 프로퍼티 래퍼를 지원함으로써, 데이터 바인딩을 구현할 수 있다는 것을 배웠는데요.
그렇다면 UIKit에서는 어떻게 데이터 바인딩을 구현할 수 있는 것일까요?
가장 먼저, 대표적으로 Combine이나 RxSwift 같은 반응형 프로그래밍을 지원하는 프레임워크가 떠오릅니다.
자세하게 Combine이 어떤 식으로 코드를 쓰고,
개념이 어떻게 되는지에 대한 내용은 예전 <Combine 진짜 알기 쉽게 정리해서 알려줄게> 시리즈 글로 대체하도록 할게요!
[Combine] Combine 진짜 알기 쉽게 정리해서 올려줄게 (1) - Combine 기초 개념 이해하기
⚠️ 이번 글은 Combine Framework가 처음 소개되었던 WWDC19의 두 세션 Introducing Combine과 Combine in Practice의 예제 을 직접 구현하면서 사용된 Combine 개념들에 대해 집중적으로 소개합니다.이 글에서 소개
mini-min-dev.tistory.com
반응형 프로그래밍을 지원하는 프레임워크를 사용하지 않는다고 하면,
과거 Objective-C 기반 방식의 KVO (Key-Value Observing)를 사용하는 방식도 있을 것이고요.
앞선 Observer Pattern을 설명할 때 사용한 예시를 들었던 NotificationCenter를 활용할 수도 있을 거고요.
간단한 데이터의 경우에는 Deletate Pattern과 Closure를 활용할 수도, 혹은 직접 didSet을 활용해서 커스텀 Observable 객체를 만드는 방법도 있을 것 같아요.
final class Observable<T> {
typealias Listener = (T) -> Void
var listener: Listener?
var value: T {
didSet {
listener?(value)
}
}
init(_ value: T) {
self.value = value
}
func bind(_ listener: @escaping Listener) {
self.listener = listener
listener(value)
}
}
아무튼 UIKit에서는 상황에 따라, 혹은 프로그래밍 실력에 따라 적절한 여러 방법으로 데이터 바인딩을 구현할 수 있다는 점이 중요합니다!
가장 선호되는 방법은 아무래도 최신 프레임워크 Combine을 사용하는 것이지만,
아직 다른 방법을 사용하는 경우도 모두 알아두면 좋겠죠?
이와 관련된 참고 글들을 아래에 모두 첨부하겠습니다! (RxSwift는 아쉽게도 제가 글을 쓰지는 않았습니다....)
직접 배우고 싶은 개념을 링크를 타고 들어가 확인해보시면 좋을 것 같네요 :)
[Swift] Closure 완전 정복하기: 일급 객체부터 작성법, 그리고 @escaping까지
1. 클로저(Closure)란? 솝트에서 서버 통신을 처음 배우다가 마주친 어려운 개념 2개가 있었다. 그중 하나가 Escaping Closure(탈출 클로저)였는데 (당연히, 클로저를 모르는데 탈출 클로저를 듣는다고
mini-min-dev.tistory.com
[Design Pattern] 내가 보려고 정리하는 Swift 디자인 패턴 (2) - Delegate Pattern
1️⃣ Delegate가 등장하게 된 이유 Delegate는 "대리자" "위임하다" 같은 뜻으로 번역되는 단어다. "위임하다"의 의미를 구체적으로 들어가보면, "당사자의 일방이 상대방에 대하여 '사무의 처리'를
mini-min-dev.tistory.com
[Swift] KVO (Key-Value Observing) 완전 정복하기 (feat. WKWebView progressBar)
예전 WKWebView를 구현하는 글의 마지막 부분이었던 "KVO를 사용해서 웹 페이지 로딩 상태 프로그레스바로 나타내기" 코드를 이번 글에서는 리팩토링하는 내용과 함께 KVO (Key-Value Observing) 개념에 대
mini-min-dev.tistory.com
[iOS] Clean Architecture + MVVM + Observable 사용해서 날씨앱 리팩토링하기
⚠️ 해당 글은 Robert C. Martin의 너무도 유명한 책과 Oleh Kudinov라는 분의 Medium 글 Clean Architecture and MVVM on iOS를 참고해서 작성했습니다.번역 또는 프로젝트의 목적, 그리고 저의 부족한 이해력 때문
mini-min-dev.tistory.com