안녕하세요, iOS 개발하는 루피입니다.
오늘은 공식문서를 바탕으로 Bindable에 대해 정리해보는 시간을 갖겠습니다.
바로 시작합니다.
Bindable이란?
@Bindable은 Observable 객체의 변경 가능한 속성들에 바인딩을 생성할 수 있게 해주는 프로퍼티 래퍼입니다.
이 프로퍼티 래퍼를 사용하면 Observable 프로토콜을 준수하는 데이터 모델 객체의 변경 가능한 속성에 바인딩을 만들 수 있습니다.
예를 들어, 아래 코드에서는 @Bindable로 book 입력을 감싸고 있습니다. 그런 다음 TextField를 사용해 책의 title 속성을 변경하고, Toggle을 사용해 isAvailable 속성을 변경합니다. 이때 $ 문법을 사용하여 각 속성에 대한 바인딩을 컨트롤에 전달합니다.
@Observable
class Book: Identifiable {
var title = "Sample Book Title"
var isAvailable = true
}
struct BookEditView: View {
@Bindable var book: Book
@Environment(\\\\.dismiss) private var dismiss
var body: some View {
Form {
TextField("Title", text: $book.title)
Toggle("Book is available", isOn: $book.isAvailable)
Button("Close") {
dismiss()
}
}
}
}
Bindable 프로퍼티 래퍼는 Observable 객체에 대한 프로퍼티와 변수에 사용할 수 있습니다.
전역 변수, SwiftUI 타입 외부에 존재하는 프로퍼티, 심지어 지역 변수에도 사용 가능합니다.
예를 들어, 뷰의 body 내에서 @Bindable 변수를 만들 수 있습니다.
struct LibraryView: View {
@State private var books = [Book(), Book(), Book()]
var body: some View {
List(books) { book in
@Bindable var book = book
TextField("Title", text: $book.title)
}
}
}
이 @Bindable 변수 book은 TextField와 book의 title 속성을 연결하는 바인딩을 제공하여 사용자가 모델 데이터를 직접 변경할 수 있게 합니다.
뷰의 Environment에 저장된 Observable 객체의 속성에 바인딩이 필요할 때도 같은 방식을 사용할 수 있습니다.
예를 들어, 다음 코드는 Environment 프로퍼티 래퍼를 사용해 Observable 타입인 Book의 인스턴스를 가져옵니다.
그런 다음 @Bindable 변수 book을 만들고 $ 문법을 사용해 title 속성에 대한 바인딩을 TextField에 전달합니다.
struct TitleEditView: View {
@Environment(Book.self) private var book
var body: some View {
@Bindable var book = book
TextField("Title", text: $book.title)
}
}
코드로 보는 Bindable 필요성
Bindable에 대해 잘 이해가 가시나요??
저는 한번에 알아보기 힘들더라고요 그래서 한번 코드로 알아보겠습니다.
State, Binding
아래 코드는 간단한 텍스트 필드를 입력받는 코드입니다.
struct ContentView: View {
@State private var query = ""
var body: some View {
TextField("Enter text", text: $query)
Text(query)
}
}
이 코드에서는 @State 프로퍼티 래퍼를 사용하여 문자열 상태를 관리하고, $ 접두사를 사용해 TextField에 바인딩을 전달합니다. 이는 기본적인 SwiftUI의 데이터 흐름 방식입니다.
StateObject, ObservableObject
struct ContentView: View {
@StateObject private var viewModel = SearchViewModel()
var body: some View {
TextField("Enter text", text: $viewModel.query)
Text(viewModel.query)
}
}
class SearchViewModel : ObservableObject {
@Published var query: String = ""
}
iOS 17 이전에는 클래스 기반 데이터 모델을 사용할 때 ObservableObject 프로토콜과 @Published 프로퍼티 래퍼를 사용했습니다.
이 방식은 여전히 유효하지만, iOS 17에서는 더 간단한 @Observable 매크로가 도입되었습니다.
@Observable
import SwiftUI
struct ContentView: View {
@State var viewModel = SearchViewModel()
var body: some View {
VStack {
SearchField(query: $viewModel.query)
Text(viewModel.query)
}
}
}
struct SearchField: View {
@Binding var query: String
var body: some View {
TextField("Search Query", text: $query)
.textFieldStyle(.roundedBorder)
.padding()
}
}
@Observable
class SearchViewModel{
var query: String = ""
}
@Observable 매크로를 사용하면 ObservableObject와 @Published를 사용하지 않고도 클래스의 속성 변경을 추적할 수 있습니다.
위 코드에서는 @State로 viewModel을 관리하고, $viewModel.query를 통해 바인딩을 전달합니다.
@Bindable이 필요한 상황
하지만 다음과 같은 상황에서 문제가 발생합니다.
import SwiftUI
struct ContentView: View {
@State var viewModel = SearchViewModel()
var body: some View {
VStack {
SearchView(viewModel: viewModel)
Text(viewModel.query)
}
}
}
struct SearchView: View {
let viewModel: SearchViewModel
var body: some View {
SearchField(query: $viewModel.query) // 컴파일 에러 발생!
}
}
struct SearchField: View {
@Binding var query: String
var body: some View {
TextField("Search Query", text: $query)
.textFieldStyle(.roundedBorder)
.padding()
}
}
@Observable
class SearchViewModel{
var query: String = ""
}
여기서 SearchView는 viewModel을 일반 속성으로 받고 있기 때문에 $viewModel.query와 같은 바인딩 접근이 불가능합니다.
컴파일러는 "Value of type 'SearchViewModel' has no member '$'" 같은 오류를 표시합니다.
해결 방법 1: 수동 바인딩 생성
이 문제를 해결하기 위한 한 가지 방법은 수동으로 바인딩을 생성하는 것입니다:
struct SearchView: View {
let viewModel: SearchViewModel
var body: some View {
SearchField(query: Binding(
get: { viewModel.query },
set: { newValue in viewModel.query = newValue }
))
}
}
이 방식은 작동하지만 코드가 장황해지고, 여러 속성에 대해 반복해야 한다면 매우 번거롭습니다.
해결 방법 2: @Bindable 사용
@Bindable을 사용하면 이 문제를 간단하게 해결할 수 있습니다:
struct SearchView: View {
@Bindable var viewModel: SearchViewModel
var body: some View {
SearchField(query: $viewModel.query) // 정상 작동!
}
}
또는 지역 변수로 선언할 수도 있습니다:
struct SearchView: View {
let viewModel: SearchViewModel
var body: some View {
@Bindable var viewModel = viewModel
SearchField(query: $viewModel.query) // 정상 작동!
}
}ㅂ
@Bindable은 Observable 객체에 대한 바인딩 접근자($)를 제공하여, 프로퍼티 래퍼로 감싸지 않은 일반 속성이나 지역 변수에서도 바인딩을 생성할 수 있게 해줍니다.
@Bindable의 장점과 활용
1. 코드 간결성
@Bindable을 사용하면 수동으로 바인딩을 생성하는 번거로운 코드를 작성할 필요가 없습니다. 특히 여러 속성에 바인딩이 필요한 경우 코드가 훨씬 간결해집니다.
2. 유연한 적용 범위
@Bindable은 다양한 상황에서 사용할 수 있습니다:
- 프로퍼티에 적용
- 지역 변수에 적용
- 함수 매개변수에 적용
- 전역 변수에 적용
3. 데이터 흐름의 명확성
@Bindable을 사용하면 데이터가 어디서 오는지, 어떻게 변경되는지 더 명확하게 표현할 수 있습니다. 특히 복잡한 뷰 계층에서 데이터 흐름을 추적하기 쉬워집니다.
4. 성능 최적화
@Observable과 @Bindable의 조합은 SwiftUI의 뷰 업데이트 메커니즘을 최적화합니다. 변경된 속성에 대해서만 뷰가 업데이트되므로 불필요한 렌더링을 방지할 수 있습니다.
실제 활용 예시
다음은 @Bindable을 활용한 좀 더 복잡한 예시입니다:
@Observable
class UserProfile {
var name: String = ""
var email: String = ""
var notificationsEnabled: Bool = true
var theme: Theme = .light
enum Theme: String, CaseIterable {
case light, dark, system
}
}
struct ProfileEditView: View {
@Bindable var profile: UserProfile
var body: some View {
Form {
Section("기본 정보") {
TextField("이름", text: $profile.name)
TextField("이메일", text: $profile.email)
}
Section("설정") {
Toggle("알림 활성화", isOn: $profile.notificationsEnabled)
Picker("테마", selection: $profile.theme) {
ForEach(UserProfile.Theme.allCases, id: \\\\.self) { theme in
Text(theme.rawValue.capitalized)
}
}
}
}
}
}
struct ContentView: View {
@State private var userProfile = UserProfile()
var body: some View {
NavigationStack {
ProfileEditView(profile: userProfile)
.navigationTitle("프로필 편집")
}
}
}
이 예시에서 ProfileEditView는 @Bindable을 사용하여 UserProfile 객체의 여러 속성에 바인딩을 생성합니다.
이를 통해 사용자가 폼을 통해 프로필 정보를 쉽게 편집할 수 있습니다.
결론
@Bindable은 iOS 17에서 도입된 SwiftUI의 새로운 프로퍼티 래퍼로, @Observable 매크로와 함께 사용하여 데이터 바인딩을 더 간결하고 유연하게 만들어줍니다. 특히 Observable 객체를 프로퍼티 래퍼 없이 전달받는 경우에 바인딩 접근을 가능하게 해주어 코드의 가독성과 유지보수성을 크게 향상시킵니다.
따라서, iOS 17 이전에는
값 타입은 @State, @Binding을 사용하고, 참조 타입은 @StateObject 와 @ObservedObject를 사용했으나
iOS 17 이후 Bindable이 나온 이후부터는
값타입은 @State 와 @ Binding, 참조타입은 @State 와 Bindable을 사용할 수 있게 되었습니다.
이로인해 우리는 StateObject와 ObservedObject에 대한 의존도를 많이 낮출수 있으나, 아직 현업에서는 iOS 17 미만을 지원하는 앱이 많기 때문에, StateObject와 ObservedObject에 대한 학습도 필요하다.
참고
https://developer.apple.com/documentation/swiftui/bindable
Bindable | Apple Developer Documentation
A property wrapper type that supports creating bindings to the mutable properties of observable objects.
developer.apple.com
https://www.youtube.com/watch?v=YgrnC1hIFEY
오늘도 화이팅입니다!
'iOS > SwiftUI' 카테고리의 다른 글
[SwiftUI] View & Modifiers (0) | 2025.05.13 |
---|---|
[SwiftUI] StateObject (0) | 2025.04.21 |
[SwiftUI] 상태 관리 - PropertyWrapper (2) (0) | 2025.04.17 |
[SwiftUI] 상태 관리 - PropertyWrapper (1) (0) | 2025.04.17 |
[SwiftUI] UI 상태 관리 (Managing user interface state) (0) | 2025.04.15 |