SwiftUI에는 다양한 툴을 이용하여 데이터 모델을 정의할 수 있습니다. State, ObservableObject와 관련된 툴들이 하나의 예시입니다. 이 툴들과 SwiftUI의 뷰들간의 최적의 성능을 만들어내기 위해서는 선행적으로 뷰들이 내부적으로 어떻게 동작하는지에 대한 이해가 필요합니다.
일반적으로 뷰는 UI 컴포넌트를 직접적으로 가지고 있습니다. 그렇기 때문에 뷰는 비용이 적게 들어야 함과 동시에 가벼워야 합니다. 여기서 유의할 점이 있습니다. 화면의 나타나는 View의 수명과 Struct인 View의 수명은 따로 움직입니다. 개발자가 직접 코드를 입력하는 View 프로토콜을 채택하는 구조체는 매우 짧은 수명을 가집니다. 내부적으로 SwiftUI는 이 구조체를 이용하여 화면을 렌더링하고 사라집니다.
위 그림을 참조하면 SwiftUI 뷰의 라이프 사이클을 직관적으로 이해할 수 있습니다. 시작점은 가장 위에서 부터 시작합니다.
- 사용자의 어떤 이벤트가 들어오면 클로저나 액션이 실행됩니다.
- Source Of Truth가 업데이트 됩니다.
- Source Of Truth와 종속성을 가지고 있는 뷰의 새로운 복사본을 얻게 됩니다.
- 이 구조체를 사용하여 화면을 새로 렌더링합니다.
앱이 사용되는 동안 이 라이프사이클은 주기적으로 반복됩니다. 여기서 뷰의 성능이 좋다라는 의미는 이 라이프사이클 주기가 막힘없이 돌아간다는 뜻과 같습니다. 만약 한 지점에서 무거운 작업이 있다면 앱의 성능(프레임 저하 및 앱 크러시)이 현저히 줄어들 것입니다. 그렇다면 어떻게 이 문제를 해결할 수 있을까요?
많은 방법이 있겠지만, 가장 중요한 것은 구조체의 View의 생성(초기화)을 가볍게 만드는 것입니다. 뷰를 가볍게 만드는 것은 다음과 같은 작업이 필요합니다. 사이드 이펙트(Dispatching 등)가 없어야 하고 body가 언제 어떻게 호출되는지에 대한 예측을 하지말아야합니다.
struct ReadingListViewer: View {
var body: some View {
NavigationView {
ReadingList()
Placeholder()
}
}
}
struct ReadingList: View {
@ObservedObject var store = ReadingListStore() ❌
@StateObject var store = ReadingListStore() 🟢
var body: some View {
// ...
}
}
다음은 뷰를 가볍게 만드는 한 가지 예시 코드입니다. ReadingListViewer는 ObservableObject의 Source Of Truth를 생성하고 있는 ReadingList라는 하위 뷰를 가지고 있습니다. 겉으로 보기에는 별 문제가 없어보이지만, 여기에는 꽤나 심각한 이슈가 숨겨져 있습니다. 바로 StateObject를 사용하지 않았다는 점입니다.
그렇다면 이것이 왜 문제가 되는걸까요? 앞서 말했듯이 Struct의 View는 정해진 수명을 가지고 있지 않습니다. 그래서 뷰가 끊임없이 렌더링이 되면서 뷰의 새로운 복사본이 계속해서 생성될 것입니다. 여기서 ObservedObject를 사용한다면 계속해서 Source Of Truth를 만들어내는 꼴이나 다름 없습니다. 즉 반복된 힙 할당(Heap Allocation)이 일어나 앱의 성능 저하가 발생합니다.
여기서 StateObject가 그 해결 방법이 될 수 있습니다. StateObject는 뷰가 렌더링되는 그 시점을 추적하면서 인스턴스를 할당하고 해제할 수 있는 좋은 도구입니다. 🟢 단 한 줄의 코드 변경이 앱의 성능을 엄청나게 향상시켰습니다. 이외에도 유저의 액션에 반응하는 뷰의 렌더링은 메인 스레드에서만 동작합니다. 그래서 무거운 작업들은 백 그라운드 큐에 디스패치 하는 것으로 성능을 향상시킬 수 있습니다.
즉 View의 ObservableObject를 소유할 때에는 StateObject를 선호하는 것이 나은 선택이라는 결론이 나옵니다.
위 그림과 같이 뷰의 수명과 연관되어 있는 속성은 State, StateObject, Constant 세 가지 입니다. 하지만 만약 앱이 종료 및 다시 시작되면서 State를 유지하고 싶은 경우가 있을 수 있습니다. 뷰의 수명과 연관되어 있는 위 세 가지 속성은 이런 경우에서 다시 되돌릴 수 없습니다.
이 문제를 해결 할 수 있는 방법은 바로 AppStorage를 사용하는 것입니다. SceneStorage는 Scene을 기준으로 Storage를 관리하는 것이고 AppStorage는 그것보다 한 차원 작은 App 단위로 움직입니다. 이것은 자체 모델로서의 기능은 하지 않고 오직 가벼운 저장소 느낌이라고 생각하시면 됩니다.
AppStorage의 뿌리는 UserDefaults를 사용합니다. 그래서 KVO방식을 그대로 따릅니다. 이 툴은 앱 내부나 뷰 내부나 어디서든 엑세스가 가능하여 매우 유용하게 사용하실 수 있습니다.
struct BookClubSettings: View {
@AppStorage("updateArtwork") private var updateArtwork = true
@AppStorage("syncProgress") private var syncProgress = true
var body: some View {
Form {
Toggle(isOn: $updateArtwork) {
//...
}
Toggle(isOn: $syncProgress) {
//...
}
}
}
}
사용법은 간단합니다. 이렇게 @AppStorage라는 어노테이션과 Key값 그리고 Value값을 주입해주면 완성입니다. 결국 AppStorage 마저도 Source Of Truth인 것은 변함이 없습니다. 따라서 해당 값을 변경하기 위해서는 $을 통해 바인딩 해줘야 합니다. 만약 AppStorage값이 바뀐다면, 업데이트를 감지하여 자동으로 저장이 됩니다.
많은 툴이 있지만, 결국 이것들은 모두 데이터의 특성과 많은 상황 속에서 다르게 이용됩니다. 한 가지 명심할 것은 앱이 복잡해지지 않도록 Source Of Truth의 수를 최대한 제한하려고 해야합니다. 이 방법으로 대표적으로는 Binding을 이용하여 원본 데이터의 재사용을 이용하면 됩니다. Binding은 추상화를 구현하는 데 강력한 도구라고 Apple은 설명하고 있습니다.