일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 앱 비교 프로젝트
- react
- 제앱소
- apple developer academy 후기
- ObservableObject
- sqoop
- StateObject
- 애플 아카데미 후기
- SWIFT
- ObservedObject
- 운영체제
- 애플 디벨로퍼 아카데미 후기
- 네이버 부스트캠프
- 데이터베이스 공부
- 숭실대
- Swift 기능
- swift문법
- 데이터베이스
- 애플 디벨로퍼 아카데미
- Swift 문법
- useReducer
- iOS 개발 오류
- 네이버 치지직
- 소프트웨어분석및설계
- global soop
- OS
- 애플 디벨로퍼 아카데미 21주차 회고
- Apple Developer Academy @ POSTECH
- Swift 디자인패턴
- 치지직
- Today
- Total
사과하는 제라스
[Swift 지식] @Bindable 딥다이브(였던 것) feat. 사실 Observable 딥다이브... 본문
[Swift 지식] @Bindable 딥다이브(였던 것) feat. 사실 Observable 딥다이브...
Xerath(제라스) 2024. 4. 7. 02:09목차
서론
안녕하세요! 개발자 제라스입니다! 👋🏻🤖👋🏻
요즘 제가 상당히 아주 고민이 많았습니다.
바로 한달 전쯤에 적었던 Bindable에 대한 것 때문인데요...!
아무리 아무리 생각해도 도무지 @Bindable의 사용시점? 상황? 분위기나 사용하는 이유?를 못찾겠더라구요...😭😭
이게 대체 왜 어떤 과정에서 나왔을까도 사실 확실한 이유에 대한 정의가 어려웠죠...🤦🏻♂️🤦🏻♂️
그래서 자칭 Bindable무새...답게 폭풍 구글링과 공식문서 10회독을 하면서 공부해보고 주변 러너들과 얘기를 많이 나누었습니다.
그 결과 정말 빙글빙글 돌아 수많은 학습들을 하게 되었죠...ㅎㅎㅎ
그래도 다행인 건 확실하고 완벽한 답을 얻어내진 못했지만 그래도 한 주동안 얻은 주변 지식들을 가지고 근거있는 의견을 낼 수 있어진 것 같습니다ㅎㅎ
부끄러웠던 지난 Bindable 포스팅을 잊고 한번 다시 정리를 해봐야겠어요...!
조금은 길지만 최대한 다양한 방향으로 정리를 해보려고 한 것을 공유해보겠습니다.
그럼 그 과정을 시작해보겠습니다!
Migrating from the Observable Object protocol to the Observable macro
'엥?? 님 뭐임? Bindable 얘기한다면서 왜 Observable 마이그레이션 얘기함??'
맞습니다...! 사실 둘은 다른 영역의 얘기지만 Observable을 알아보는 이유는 Binding의 공식문서 내 정의 때문입니다.
Bindable
A property wrapper type that supports creating bindings to the mutable properties of observable objects.
Observable한 객체의 프로퍼티들에 대한 Binding을 제공하기 위한 Property Wrapper라고 하네요.
즉, Observable한 아이를 지원해주기 위해 있다고 봐도 무방합니다!
그럼 일단 가볍게 이 Observable을 구현하는 방식을 알아보겠습니다.
일단 소제목을 다음 공식문서의 제목에서 따왔습니다.
그럼 한번 이 문서를 기반으로 얘기를 해보겠습니다!
한번 전래동화처럼 제가 생각하고 학습한 내용을 기반으로 얘기를 해보겠습니다...!!
예전에는 ObservableObject라는 프로토콜을 통해 Observation 기능을 구현했습니다.
근데, iOS17에서 아주아주 핫했던 Macro가 등장했고 그걸 사용해서 Apple에선 @Observable이란 것을 만들어주었습니다.
이건 class 앞에만 써줄 수 있는데요~
이걸 앞에 명시해준 친구는 구독이 가능한 class가 되어 내부의 속성들이 변경되면 해당 속성이 포함된 곳의 View를 리렌더링하게 됩니다.
자 그럼 어떤 과정으로 마이그레이션이 되었는지 살펴보겠습니다!
일단 크게 3가지가 달라졌습니다.
Observable로 Migrating하면서 달라진 점
1. ObservableObject 대신 @Observable을 쓰게 되면서 옵셔널 객체, Collection 타입의 객체들 트랙킹이 가능
-> 이전에는 optional로 된 객체나 Collection타입의 속성들을 트래킹할 수 없었는데 이제는 가능해졌습니다.
2. 예전에 쓰던 StateObject와 EnvironmentObject 등을 대신해서 State, Environment로 표기
-> 이렇게 씀으로서 @State, @StateObject, @Environment, @EnvironmentObject를 통합시키게 되었습니다.
(흠...사실 아직 iOS 17 이전 방식에 대한 학습이 아주 잘 되어있지는 않기에 이 부분은 'A-HA! 이렇게 바뀌었구나??' 정도로만 두고 가고자 합니다.')
3. 객체에서 발생하는 속성 변경이 아닌 해당 객체에 대하여 View가 읽고 있는 속성에 대해서만 View를 업데이트할 수 있어짐.
-> 공식문서에 다음과 같이 나와있습니다.
when tracking as Observable(), SwiftUI updates a view only when an observable property changes and the view’s body reads the property directly. The view doesn’t update when observable properties not read by body changes. In contrast, a view updates when any published property of an ObservableObject instance changes, even if the view doesn’t read the property that changes, when tracking as ObservableObject.
이게 무슨 소리냐?
예를 들면 Person이란 클래스가 있고 이 클래스 안에 속성으로 id, name, height가 있다고 합시다.
@Observable
final class Person {
var id: String
var name: String
var height: Int
}
struct SecondView: View {
var person1: Person
var body: some View {
VStack{
Text("\(person1.name))
}
}
}
이런 상황에서 SecondView는 person1의 name만 가져와서 읽고 있죠.
그런데 이 person1의 다른 속성(id나 height)이 다른 View에서 변경된다면...!
ObservableObject로 구현할 때는 현재 SecondView도 화면이 리렌더링이 되었습니다.
하지만, 지금은 name값이 바뀐 것이 아니기에 SecondView는 업데이트가 되지 않습니다.
호오?? 이러면 불필요한 화면 업데이트가 발생하지 않게 되는 것이죠!
https://forums.swift.org/t/lifecycle-of-swiftui-view-observable-vs-observableobject/69842
한번 위 포럼 글을 확인해보시면 그 차이를 몸소 느끼실 수 있으리라 생각이 듭니다!
code를 통해 알아보는 달라진 점
자 그러면 어떤 식으로 코드들이 변경되었는지 확인해봅시다.
1. ObservableObject -> @Observable
// BEFORE
class Library: ObservableObject {
// ...
}
//----------------------------------------
// AFTER
@Observable class Library {
// ...
}
이렇게 ObservableObject를 프로토콜로 붙이던 방식을 앞에 @Observable 프로퍼티 래퍼를 붙여주는 방식으로 바뀌었습니다.
2. Observable한 객체 내부의 속성에 @Published 떼기
// BEFORE
@Observable class Library {
@Published var books: [Book] = [Book(), Book(), Book()]
}
//----------------------------------------
// AFTER
@Observable class Library {
var books: [Book] = [Book(), Book(), Book()]
}
이렇게 속성들에서 Published를 떼주게 되었습니다.
이 부분은 이전 방식으로 만들던 Observable 객체에서 <Combine의 @Published를 붙여서 변화되는 것을 구독하는 방식>이었는데 하다보니 죄다 @Published를 붙이게 되던 것이었죠. 이러면 굳이 @Published를 붙이지 말고 <모두 구독이 되는 형태로 바꾸자>는 생각에서 변화된 것으로 보입니다.
그럼 반대로 만약 내가 이 속성은 변화에 따라 View를 그리게 하고 싶지 않은데...??(즉, 트랙킹하기 싫은데??) 라고 생각이 들 수도 있잖아요??
그럼 @ObservationIgnored를 속성에 달아주시면 됩니다!
3. @StateObject -> @Observable + @State로 변경
// BEFORE
@main
struct BookReaderApp: App {
@StateObject private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
//----------------------------------------
// AFTER
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(library)
}
}
}
막상 @StateObject를 @State로 바꾸니까 '응? 둘이 똑같은 건가?' 싶게 생겼습니다 ㅎㅎ
자, 그럼 일단 @StateObject부터 이해해봅시다.
일단 @StateObject 이 친구로 만들어진 객체는 View에 종속되지 않고 메모리에 올라갑니다.
그래서 예를 들면, 상위 View가 업데이트되어도 하위 View에 있는 @StateObject 객체가 새로 초기화되지 않고 값을 유지합니다.
@ObservedObject는 해당 객체를 포함한 View와 함께 값이 저장이 되기에 라이프 사이클이 해당 View가 업데이트되면 새로 값을 변경합니다. 하지만 @StateObject는 View와 따로 메모리가 저장되기에 View가 아예 사라지지 않는 한 값이 유지가 되는 것입니다.
그럼 언제 @StateObject 객체가 사라지는데?
해당 객체를 참조하는 View가 아예 사라질 때 없어집니다. GC(Garbage Collection)과 비슷한 Swift의 ARC(Automatic Reference Counting)에 의해서요!
그렇기에 적어도 View보다 @StateObject가 수명이 짧지 않기에 안정성이 좋습니다.
자 너무 딴 얘기로 샜네요.
다시 본론으로 돌아와서 @StateObject를 어떻게 무엇으로 대체했느냐...!
일단 Observable한 객체를 @Observable 매크로를 선언해줘서 만들어줬으니,
그거에다가 @State를 이용해서 SOT로 만들어줍니다.
그러면 Observable한데 Source Of Truth도 갖는 형태로 변경이 되는 것이죠.
앙??????? 이건 또 뭔소리임?
Source Of Truth로 만들지 않으면 해당 값을 참조하는 참조 주소들을 만들어서 접근하는 건데 @State처리를 해주면 해당 값에 대한 접근이 모두 그 SOT 그 자체로 접근할 수 있게 해줍니다.
즉, @State로 만들어서 넘겨주면 접근 가능한 다리를 하나 만드는 느낌보단 좀 더 접근하기 쉽게 몸을 갖다대주는 느낌입니다.
사실, Apple 공식문서를 참고해보면
@StateObject
Use a state object as the single source of truth for a reference type that you store in a view hierarchy.
@State
Use state as the single source of truth for a given value type that you store in a view hierarchy.
라고 나와있죠.
그리고...!
@State는 이전 iOS13에 나왔을 때부터 값 타입에 대한 Source Of Truth로서의 state를 생성하는 역할이었습니다.
근데, Observable이 나오면서 또 하나! Observable 매크로로 만들어진 Observable 객체도 앞에 @State를 명시해주면!
이 state에 저장이 됩니다...!!
결론적으로, @StateObject, @State 둘 다 SOT를 적용시키려는 점이 공통점이구 @State를 통해서 @Observable 객체에도 SOT를 적용시켜줄 수 있다는 사실을 알 수 있습니다.
여기까진 좋아요...! 근데!! 여기서 조금 중요한 논의 거리가 있습니다.
이건 다음에도 다룰 예정인데,
'@State로 선언 시 private을 꼭 붙여줘야 하는 것이냐??'입니다.
결론부터 말씀드리면 상황에 따라 다르다.
위 링크에서 그 해답을 얻었는데,
MVVM 형태로 어떤 ViewModel을 주입해줘야 하는 상황이라면 private 처리하면 안됩니다.
어??? 아니 제라스 양반! 이 아티클은 StateObject에 private한 거잖소~~배려부렀네잉...
따로 @State private var (Observable객체) 에 대한 글이 잘 안보여서 이전 방식이던 @StateObject private var을 가져와보았습니다 ㅎㅎㅎ
다만, 다른 경우들에서는 private 처리를 해줘서 해당 인스턴스가 SOT로써 역할을 할 수 있도록 해줘야 합니다.
다 필요없고 특히 Apple 공식 문서에 나온 @Observable 인스턴스에 @State처리 시 항상 private으로 만들어줍니다.
뭔가 다른 경우가 없는 듯 합니다.(Apple 공식 SwiftUI Tutorial에서도 private으로만 선언을 했습니다.)
아직 위에 말씀드린 MVVM 방식에서의 경우를 아직 Apple에서 반영하지 못한 것 같습니다.
조만간 Apple에서 이 부분에 대한 것을 커버해주면 더 이해가 쉬워질 것 같네요 😭😭😭
반면, @ObservedObject의 경우엔 View가 다시 그려지면 값을 유지하지 못하고 초기화됩니다.
즉, 라이프 사이클이 해당 객체를 가진 View가 업데이트되면 다시 초기화됩니다.
4. ObservedObject -> 더 이상 필요가 없음
// BEFORE
struct BookView: View {
@ObservedObject var book: Book
@State private var isEditorPresented = false
var body: some View {
HStack {
Text(book.title)
Spacer()
Button("Edit") {
isEditorPresented = true
}
}
.sheet(isPresented: $isEditorPresented) {
BookEditView(book: book)
}
}
}
//----------------------------------------
// AFTER
struct BookView: View {
var book: Book
@State private var isEditorPresented = false
var body: some View {
HStack {
Text(book.title)
Spacer()
Button("Edit") {
isEditorPresented = true
}
}
.sheet(isPresented: $isEditorPresented) {
BookEditView(book: book)
}
}
}
우리는 해당 객체가 ObservedObject임을 명시하지 않아도 @Observation 처리해둔 객체이기에 알아서 트래킹을 하게 됩니다.
따라서 @ObservedObject를 그대로 지우면 됩니다.
다만 우리는 @ObservedObject 인스턴스의 ProjectedValue가 인스턴스 내 각 속성들에 Binding을 제공하는 반면,
@ObservedObject variable's projected value provides a wrapper from which you can get the Binding<Subject> for all of it's properties
출처: https://stackoverflow.com/questions/59259921/binding-value-from-an-observableobject
@Observable은 그렇지 않습니다.
따라서, 우리는 해당 객체에 Binding을 제공하기 위해 새로운 프로퍼티 래퍼인 @Bindable을 사용하면 됩니다.
(와...드디어 @Bindable 쓰는 이유가 나오는구나...)
Bindable 공식문서를 뜯어보자!
참고: https://developer.apple.com/documentation/swiftui/bindable
일단 @Bindable에 대한 설명부터 보자면...
Use this property wrapper to create bindings to mutable properties of a data model object that conforms to the Observable protocol.
즉, @Observable로 표기된 객체의 각 속성들에 Binding처리를 해주는 친구입니다.
근데 이때 쯤이면 생각이 드는 부분은 '어떻게 쓰는데?', '언제 쓰는데?', '왜 쓰는데?' 이지 않을까 싶어요.
이걸 유추할 수 있는 부분은 공식문서 뿐이란 생각이 들.기.에!
Apple 공식문서에서의 Bindable 사용법
한번 Apple 공식문서에서 @Bindable을 사용하는 예제들을 가지고 정리해보겠습니다.
1. (Binding 처리되어 있지 않은 채) 해당 View로 넘어온 Observable 객체에 대한 Binding처리
보통 Observable 객체는 이미 구독을 거치고 있고 따로 어느 View와 View 간의 연결을 해 줄 필요없이 그저 인스턴스만 넘겨줘도 수정한 내용이 바로 해당 인스턴스에 반영이 됩니다.
import SwiftUI
@Observable
class Book: Identifiable {
var title = "Sample Book Title"
var isAvailable = true
}
struct BookEditView: View {
// var book: Book으로 하면 그저 Observable한 객체이고
// 그 내부의 title값은 Binding되지 않아서 Binding<String>타입이 아닌 String 타입이라
// TextField에 못 넘김
@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()
}
}
}
}
근데 우리가 해당 인스턴스의 속성들을 projectedValue로 넘겨줘야 할 때(ex. TextField 등) 이렇게 @Bindable을 써서 projectedValue로 만들어주면 됩니다!
아니 근데!! Observable 객체의 속성은 Published잖아요? 그냥 바인딩 없이 넘겨도 변화 감지하지 않나요?
이것에 대해서 생각해보면 우린 View에서 이전 View로부터 Observable 객체를 받아와서 그것에 있는 속성들을 바꿉니다.
그렇기에 문제가 없는데,
지금 같은 경우엔 따로 해당 속성만 틱- 넘겨줘야 합니다. 그렇기에 우리는 해당 속성을 projectedValue로 변경해줘야 업데이트가 가능해지죠. 그래서 Observable임에도 우리가 @Bindable을 활용해야 합니다.
즉, 우리가 Observable 인스턴스를 넘기는게 아니라 그것의 속성들을 넘기는 상황이기에 그렇습니다!
2. Environment 객체를 받아와서 Binding 처리해줘야 할 때
struct TitleEditView: View {
@Environment(Book.self) private var book
var body: some View {
@Bindable var book = book
TextField("Title", text: $book.title)
}
}
이때도 우리는 Environment 에서 Book타입의 객체를 가져오고 거기에 있는 속성들 중 하나인 title을 TextField에 넘깁니다. 그렇기에 또 @Bindable 처리를 해줘야 하죠.
저는 개인적으로 이렇게 Environment 객체는 @Bindable과 하나의 뗄 수 없는 쌍이라고 느껴집니다. 거의 항상 같이 쓰이죠.
3. 참조타입으로(Observable 객체) 만들어진 객체가 Binding으로 변화한 것을 View에 알려줘야 할 때
struct LibraryView: View {
@State private var books = [Book(), Book(), Book()]
var body: some View {
List(books) { book in
@Bindable var book = book // 여기서 Binding을 제공하여 각 book들도 구독 중인 것을 바꿀 뿐만 아니라 변화된 것을 감지하여 View를 새로그려 반영할 수 있게 됩니다.
TextField("Title", text: $book.title)
}
}
}
이 예시에선 Bindable의 쓰임 부분을 얘기하기 전에 왜 @State를 썼는가를 생각해볼게요!
@State는 이전 iOS13에 나왔을 때부터 값 타입에 대한 Source Of Truth로서의 state를 생성하는 역할이었습니다.근데, Observable이 나오면서 또 하나! Observable 매크로로 만들어진 Observable 객체도 앞에 @State를 명시해주면!
이 state에 저장이 됩니다...!!
그래서 지금 위와 같은 상황에선 @State를 통해 Book 배열을 State에 저장해둡니다.
즉, books라는 Observable conforming하는 Book 객체의 배열에 대한 Source Of Truth를 만들어주는 거죠!
죄송합니다ㅋㅋㅋㅋㅋㅋㅋ 다시 이 코드 상황에서 @Bindable의 쓰임으로 돌아와서...!
@Bindable의 쓰임 부분을 보면, 이것도 위 두가지 상황과 같은 결로 books 안에 있는 각 Book 인스턴스들의 속성들에 대해 ProjectedValue로 만들어주기 위해서
@Bindable var book = book
이렇게 @Bindable로 새로 쉐도잉 형식으로 book을 만들어준 겁니다.
Bindable의 기타 용도
사실 이 글도 정말 많이 봤었습니다.
HackingWithSwift Paul 아저씨 정말 동경합니다...!!🫶🏻🫶🏻
여기서 Paul 아저씨는 위 얘기들보다는 List나 Form 과 같은 View 안에서 사용할 수 있단 점이 Binding과의 차이점이라고 얘기를 하더라구요.
상위 View로부터 Observable을 바인딩해올 때는 다음과 같이
@Binding var book: Book
이렇게 받아올 수 있고,
만약 이게 어려운 상황.
예를 들면, Environment처럼 이미 상단에서
@Environment(Book.self) var book: Book
이런 식으로 받아오게 되었다면,
@Bindable var book = book
이렇게 View 내부에서 사용을 해주는 방식으로 쓰면 좋을 것 같습니다.
물론 이게 모든 것을 대변하는 얘기는 아닙니다.
다만, 용도적인 면에서는 이렇게 이해를 하는 것도 '오~~일리가 있군!' 하는 생각이 듭니다.
학습적인 면에서는 정보가 부족하겠지만 사용적인 면에서는 틀린 것이 없으니까요~!! 👍🏻🥹👍🏻
마무리
어...음...사실 이게 제가 앞서 적어뒀다시피 Bindable에 대해 깊이 파보려고 했어요......
정말입니다!
근데 이게 점점 Observable을 파게 되고, 그래서 ObservableObject를 파게 되고,
ObservedObject를 파고,
StateObject를 파고,
State를 파고...
사실 얘네가 다~~~엮여있었습니다.
그래서 어떤 개념 하나를 이해하려고 하면 다른 것들도 좀 알아야 하고, 왜 이렇게 쓰는지를 알아야 하고, 그래야 비교가 가능했습니다...!
너무 중요한 개념들이 이번주 내내 머릿속을 맴돌면서 이해의 고리를 흔들었는데
이렇게 우연한 기회로 깊게 상태관리와 객체의 종류들, 그것을 쓰는 이유들을 알아볼 수 있는 너무 중요한 시간들이었습니다.
하지만 지금은 좀 난잡해서 흐름이 잘 안 읽힐 것 같아서 조만간 한번 나눠서 정리해보는 것도 좋겠다는 생각이 드네요 껄ㄲ..껄
결론적으론 사실 Bindable의 쓰임이 그렇게 막 크리티컬하다는 없는 것 같아요.
Apple 공식문서에 나와있듯이
'Observable 프로토콜을 따르는 data model 객체의 변경가능한 속성들에 binding을 생성해주기 위한 프로퍼티 래퍼'
이 정도가 딱 맞는 듯합니다...!!
그저 Observable 인스턴스를 못넘기는 상황에 Binding 처리가 못된 Observable 인스턴스의 속성들을 ProjectedValue로 적용시켜주기 위함이 전부인 것 같습니다.
다시보니 너무 복잡하게 생각할 거리가 아니었던 것도 같아 허무하기도 한데 덕분에 다른 부분들을 공부했으니 행복합니다 ㅋㅋㅋㅋㅋㅋㅋ
그럼 이제 Bindable무새는 졸업해야겠네요 ㅎㅎㅎ
최근에 TCA-Tuist에 대한 관심이 호기심에서 큰 관심으로 변하고 있는데 한번 그쪽 공부도 해봐야겠습니다.
또 최근 WWDC를 밥먹으면서, 자기 전에 종종 보고 있는데 학습을 해본 것들을 정리해둔 포스팅으로 돌아오겠습니다!
긴 포스팅 읽어주셔서 감사합니다!
참고
https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app
https://developer.apple.com/documentation/swiftui/bindable
https://developer.apple.com/documentation/observation/observable()
https://developer.apple.com/documentation/swiftui/state
https://developer.apple.com/documentation/swiftui/stateobject
https://developer.apple.com/documentation/swiftui/binding
https://forums.swift.org/t/lifecycle-of-swiftui-view-observable-vs-observableobject/69842
https://medium.com/@eung7/the-source-of-truth-in-swiftui-state-observableobject-1421b8f80f54
https://inuplace.tistory.com/1381
https://www.donnywals.com/whats-the-difference-between-binding-and-bindable
아직 꼬꼬마 개발자입니다.
더 나은 설명이나 코드가 있다면 언제든 환영입니다.
적극적인 조언과 피드백 부탁드립니다!
그럼 오늘도 개발 가득한 하루되세요!
- Xerath -
🤖🤖🤖🤖🤖🤖🤖
'제라스의 iOS 공부 > Swift 지식' 카테고리의 다른 글
[Swift 지식] UITableView Cell의 생명주기(feat.prefetch를 써보자!) (0) | 2024.05.10 |
---|---|
[Swift 지식] @ObservedObject vs @StateObject 이 둘 언제 쓰는데? (feat.Observation) - (24.06.07 업데이트) (0) | 2024.05.05 |
[Swift 지식] Swift에서 프로토콜이 클래스를 상속한다고? (1) | 2024.03.31 |
[Swift 지식] removeAll(keepingCapacity: Bool)의 성능 비교 (1) | 2024.03.14 |
[Swift 지식] Single Source Of Truth(SSOT)란? (0) | 2024.03.12 |