이번 WWDC 에서는 3가지 토픽에 대해서 다룹니다.
SwiftUI 앱에서의 데이터 흐름을 다루는 State 와 Binding 같은 것들을 소개할 예정입니다.
데이터모델을 앱에 적용시키는 방법을 다룰 예정입니다.
또한 매우 중요한 부분으로 SwiftUI 의 라이프사이클에 대해서도 다룰 예정입니다.
View 를 만들기 전에 생각해야 할 것들
SwiftUI 에서 책을 읽을 수 있는 앱의 메인 UI을 만들었다고 가정합니다. UI 를 만들 때 다음과 같은 것들을 생각하고 만들어야 합니다.
- 뷰의 역할에 따라 어떤 데이터 모델이 필요한지? (예를들어 썸네일, 이름, 저자 등)
- 뷰가 데이터를 변경하는지?
- 데이터가 어디서부터 오는 것인지?, 이것을 "Source of Truth" 라고 합니다.
- 궁극적으로 "Source of Truth"는 데이터 모델을 만들 때 가장 중요한 요소입니다.
아래 간단한 뷰를 만들어내는 BookCard 가 존재합니다. book 과 progress 는 let 상수로 선언되어 있습니다.
let 으로 선언되었다는 것은 데이터가 변동되지 않는다는 것을 의미하며 상위 뷰로부터 전달받아 값이 결정됩니다.
let 으로 초기화된 값들은 뷰가 랜더링할때만 사용되고 랜더링된 이후에는 메모리를 차지하지 않습니다. (메모리 효율성 향상)
struct BookCard : View {
let book: Book
let progress: Double
var body: some View {
HStack {
Cover(book.coverName)
VStack(alignment: .leading) {
TitleText(book.title)
AuthorText(book.author)
}
Spacer()
RingProgressView(value: progress)
}
}
}
위에서 나온 뷰 계층을 뜯어보면 컴파일러가 어떤 순서로 랜더링을 하는지 구성도를 아래 그림으로 확인해볼 수 있습니다.
뷰에서 나타내는 변수들을 모델로 만들어 관리하기
뷰에서는 여러 값들이 존재할 수 있습니다. 하지만 이런 값들을 모델로 만들어서 관리한다면 테스트할 때에도 매우 용이합니다.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
}
struct BookView: View {
var editorConfig = EditorConfig()
var body: some View {
…
ProgressEditor(editorConfig: editorConfig)
…
}
}
@State
모델의 값을 변경하면서 변경되는 값을 추적하기 위해서는 모델 인스턴스를 @State 프로퍼티 래퍼로 선언해줘야 합니다.
- 이때 struct 는 프로퍼티 값을 변경할 수 없다는 특징이 있는데 이를 mutating 으로 선언하게 된다면 가능해집니다.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
mutating func present(initialProgress: Double) {
progress = initialProgress
note = ""
isEditorPresented = true
}
}
struct BookView: View {
@State private var editorConfig = EditorConfig()
func presentEditor() { editorConfig.present(…) }
var body: some View {
…
Button(action: presentEditor) { … }
…
}
}
@Binding
바인딩을 사용하지 않았을 때 발생할 수 있는 문제에 대해서 먼저 설명하겠습니다. 데이터 모델을 다룰 때에 있어 Source of Truth 는 매우 중요합니다. 다음과 같이 BookView 의 @State editorConfig 로부터 ProgressEditor 뷰는 값을 받았지만 이것은 문제가 됩니다.
이때 State 는 새로운 Source of Truth 를 생성하게 되어 ProgressEditor 에서는 새로운 Source of Truth 의 값을 갖게 됩니다.
Source of Truth 즉 데이터의 근원지가 2개가 생기게 된다는 것은 서로 각각 다른 값을 같기에 서로의 값 상태를 공유하지 않게 됩니다.
각각의 다른 값을 유지하고 싶은 상황에서는 괜찮을 수 있지만 메모리에 하나만 존재해도 되는 값이 2개 각각 존재하는 것은 메모리적으로도 효율 적이지 않습니다.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
}
struct BookView: View {
@State private var editorConfig = EditorConfig()
var body: some View {
…
ProgressEditor(editorConfig: editorConfig)
…
}
}
struct ProgressEditor: View {
var editorConfig: EditorConfig
…
TextEditor($editorConfig.note)
…
}
이때 @Binding 프로퍼티 래퍼를 사용하게 된다면 Single Source of Truth 로 존재하게 되어 하나의 값의 상태를 서로 공유할 수 있게 됩니다.
@Binding 은 @State 의 값의 메모리 주소에 존재하는 값을 참조하는 것으로 Binding 의 값을 수정하게 된다면 State 의 값이 존재하는 메모리 주소의 값을 변경하여 State 또한 값이 변경되게 됩니다.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
}
struct BookView: View {
@State private var editorConfig = EditorConfig()
var body: some View {
…
ProgressEditor(editorConfig: $editorConfig)
…
}
}
struct ProgressEditor: View {
@Binding var editorConfig: EditorConfig
…
TextEditor($editorConfig.note)
…
}
ObservableObject
ObservableObject 은 다음과 같은 특징을 가지고 있습니다
- 데이터의 life cycle 관리
- side effect 처리하기
- 여러 UI 로부터 공유하는 데이터 만들기
ObservableObject는 참조 타입에만 적용될 수 있는 클래스 제약 프로토콜입니다. 주요 요구 사항은 objectWillChange라는 프로퍼티를 포함하는 것입니다. 이 프로퍼티는 Publisher로, 데이터 변경이 일어나기 전에 값을 방출해야 합니다. 기본적으로 제공되는 Publisher는 대부분의 경우 충분히 작동합니다. 하지만 필요하다면, 커스텀 Publisher를 사용할 수도 있습니다. 예를 들어, 타이머에 대한 Publisher나 KVO(Key-Value Observing)를 통해 모델을 관찰하는 Publisher를 사용할 수 있습니다.
만약 ObservableObject 를 준수한다면 새로운 Source of Truth 를 만들어 값의 변화에 대응해야 하는지 알려줍니다
즉 ObservableObject는 UI 에서 랜더링할 때 필요한 데이터를 정의 및 원하는 로직을 정의합니다.
SwiftUI는 데이터를 뷰와 의존성 관계로 만들고, 이 관계를 통해 뷰를 자동으로 최신 상태로 유지합니다. ObservableObject는 뷰에 데이터를 제공하는 모델의 일부이지만, 전체 모델일 필요는 없습니다. 모델의 데이터와 저장, 생명 주기를 분리할 수 있으며, value type 으로 데이터를 모델링하고, reference type으로 생명주기를 관리할 수 있습니다.
ObservableObject 로 선언된 클래스는 프로젝트 내에서 단일의 source of truth 가 되어 모든 뷰에 데이터를 공유할 수 있게 됩니다.
struct CurrentlyReading: ObservableObject {
@Published var progress: Int = 0
}
struct BookView: View {
@ObservedObject var currentlyReading: CurrentlyReading
var body: some View {
VStack {
Text("Progress: \(currentlyReading.progress)%")
}
}
}
@Published
@Published 는 ObservableObject 내부의 특정 값을 관찰 가능하게 만들어 Publisher를 통해 해당 @Published 의 값을 노출합니다.
ObservableObject에 @Published 만 추가하면 자동으로 데이터와 뷰가 동기화되며, 기본적으로 값이 변경되기 직전에 자동으로 갱신됩니다. 이는 이후에 @Published의 프로젝트된 값을 사용하여 리액티브 스트림을 구축할 수 있습니다.
더 설명하면 @Published 로 선언된 값은 Publisher 를 통해 @Published 의 값의 변화를 추적할 수 있는 것이고 이후 Subscriber 를 사용하여 @Published 의 값이 변경될 때를 감지하여 원하는 로직를 수행할 수 있도록하는 Combine 프레임워크를 활용한 리액티브 스트림 구축이 있습니다.
@ObservedObject
ObservedObject 는 프로퍼티 래퍼로 ObervableObject 타입을 준수하는 클래스를 사용하여 뷰가 이 데이터 모델의 변화를 감지할 수 있게 만듭니다.
ObservedObejct 는 클래스 내부의 값의 변화에 자동으로 동기화되어 뷰에 적용하여 따로 코드를 작성할 필요가 없습니다.
struct BookView: View {
@ObservedObject var currentlyReading: CurrentlyReading
var body: some View {
VStack {
BookCard(
currentlyReading: currentlyReading)
//…
ProgressDetailsList(
progress: currentlyReading.progress)
}
}
}
ObservedObject 는 사용할때는 SwiftUI 가 특정 ObservableObject 클래스의 objectWillChange를 구독합니다. 이후 ObservableObject 의 값이 바뀔때마다 ObservableObject 로 받고 있는 모든 뷰는 업데이트됩니다.
🔵 왜 objectWillChange를 사용하는가?
데이터 변경 후인 didChange가 아닌 willChange를 사용하는 이유는, SwiftUI가 뷰 갱신 작업을 최적화하기 위해서입니다. 데이터가 변경되기 전에 SwiftUI는 그 데이터를 사용하는 모든 뷰에 대해 단일 업데이트 작업을 수행하려고 합니다. 여러 데이터 변경이 있을 때 각각의 변화를 개별적으로 처리하지 않고, 한 번의 업데이트로 처리함으로써 성능을 향상시키는 것입니다.
@StateObject
특징
- @StateObject는 SwiftUI에서 관리하는 객체의 생명 주기를 책임집니다. 속성에 @StateObject를 지정하면 초기 값을 제공해야 하며, SwiftUI는 이 값을 뷰의 body가 처음 실행되기 직전에 인스턴스화합니다.
- 이 인스턴스는 뷰가 살아 있는 동안 유지되고, 뷰가 더 이상 필요 없을 때 자동으로 해제됩니다.
life cycle 관리
@StateObject로 선언된 객체는 SwiftUI가 뷰의 생명 주기와 함께 관리합니다. 뷰가 처음 생성될 때 인스턴스화되고, 뷰가 화면에서 사라질 때 자동으로 해제됩니다. 이 과정에서 onDisappear와 같은 수명 관리 메서드를 직접 처리할 필요가 없습니다.
class CoverImageLoader: ObservableObject {
@Published public private(set) var image: Image? = nil
func load(_ name: String) {
// …
}
func cancel() {
// …
}
deinit {
cancel()
}
}
struct BookCoverView: View {
@StateObject var loader = CoverImageLoader()
var coverName: String
var size: CGFloat
var body: some View {
CoverImage(loader.image, size: size)
.onAppear { loader.load(coverName) }
}
}
코드를 통해 쉽게 이해하기
코드를 통해 좀 더 알아보겠습니다.
🔴 @ObservedObject 특징
분명 @ObservedObject 의 클래스를 통해 여러 뷰 계층에서 데이터를 공유하며 사용할 수 있습니다.
하지만 @ObservedObject 는 객체 생성 시점이 외부에서 생성되어 뷰에 주입된다는 특징이 있습니다.
struct DataEnssentialTestView: View {
@ObservedObject var testStruct: TestStruct
var body: some View {
TestView1()
Button {
testStruct.number += 1
} label: {
Image(systemName: "plus.app")
}
}
}
그렇기 때문에 상위 뷰에서 다음과 같이 객체를 생성하여 주입해야 합니다.
@main
struct DataEnssentialTestApp: App {
var body: some Scene {
WindowGroup {
DataEnssentialTestView(testStruct: TestStruct())
}
}
}
이렇게 @ObservedObject 를 사용하여 하위 계층에서도 값의 변화를 감지하여 UI 를 업데이트할 수 있게 됩니다.
struct DataEnssentialTestView: View {
@ObservedObject var testStruct: TestStruct
var body: some View {
TestView1(testStruct: testStruct)
Button {
testStruct.number += 1
} label: {
Image(systemName: "plus.app")
}
}
}
struct TestView1: View {
@ObservedObject var testStruct: TestStruct
var body: some View {
Text("TestView1: \(testStruct.number)")
TestView2()
}
}
🔴 @StateObject 특징
@StateObject 는 @ObservedObject 와 달리 뷰에서 객체가 생성되며 다음과 같이 사용됩니다.
하위 뷰에 데이터를 전달하기 위해서는 하위뷰에서 @ObservedObject 를 사용해야 합니다.
class TestStruct: ObservableObject {
@Published var number = 0
}
struct DataEssentialTestView: View {
@StateObject var testStruct = TestStruct() // 생명 주기를 관리
var body: some View {
TestView1(testStruct: testStruct) // 하위 뷰에 전달
Button {
testStruct.number += 1
} label: {
Image(systemName: "plus.app")
}
}
}
struct TestView1: View {
@ObservedObject var testStruct: TestStruct // 부모에서 전달받음
var body: some View {
Text("TestView1: \(testStruct.number)")
.padding()
}
}
@EnvironmentObject 란?
@EnvironmentObject 란 모든 뷰에서 공유하는 하나의 단일 Source of Truth 를 상위 뷰에서 @StateObject를 통해 사용합니다.
최상위 뷰계층에서 Single Source of Truth 인 @StateObject 인스턴스를 만들어 environmentObject 를 통해 담아주게 된다면 DataEnssentialTestView() 포함 하위 계층에 뷰들은 모두 testStruct 가 Source of Truth 인 데이터를 공유해서 사용하게 됩니다.
@main
struct DataEnssentialTestApp: App {
@StateObject private var testStruct = TestStruct()
var body: some Scene {
WindowGroup {
DataEnssentialTestView()
.environmentObject(testStruct)
}
}
}
위에서 environmentObject modifier 를 통해 적용시켰다면 다음과 같이 사용할 수 있게 됩니다.
struct DataEnssentialTestView: View {
@EnvironmentObject var testStruct: TestStruct
var body: some View {
TestView1()
Button {
testStruct.number += 1
} label: {
Image(systemName: "plus.app")
}
}
}
struct TestView1: View {
@EnvironmentObject var testStruct: TestStruct
var body: some View {
Text("TestView1: \(testStruct.number)")
TestView2()
}
}
struct TestView2: View {
@EnvironmentObject var testStruct: TestStruct
var body: some View {
Text("TestView2: \(testStruct.number)")
}
}
StateObject, ObservedObject, EnvironmentObject 차이점
@StateObject
@StateObject는 SwiftUI가 객체의 생명 주기를 관리하는 프로퍼티 래퍼입니다.
- 생명 주기 관리: SwiftUI가 객체를 처음 생성하고 뷰가 소멸되면 객체도 해제됩니다. 이때 onDisappear를 사용하여 객체의 생명 주기를 관리할 필요가 없습니다.
- 뷰 갱신: 객체가 변경될 때마다 해당 뷰가 자동으로 갱신됩니다. 객체를 처음 생성할 때 사용되므로, 업데이트될 때 SwiftUI가 올바르게 뷰를 다시 렌더링합니다.
@ObservedObject
@ObservedObject는 외부에서 주입된 객체를 사용하는 프로퍼티 래퍼입니다.
- 생명 주기 관리: 해당 객체의 생명 주기는 SwiftUI가 아닌 개발자가 관리해야 합니다.
- 뷰 갱신: 객체가 변하면 SwiftUI는 뷰를 갱신하지만, 이 객체는 외부에서 제공되므로 해당 뷰의 생명 주기와 연결되지 않습니다. 따라서 다른 뷰가 같은 객체를 사용할 수 있습니다.
@EnvironmentObject
@EnvironmentObject는 상위 뷰에서 제공된 객체를 사용하는 프로퍼티 래퍼입니다.
- 생명 주기 관리: 객체의 생명 주기를 소유하지 않으며, 상위 뷰에서 관리되는 객체를 사용합니다.
- 데이터 공유: 여러 뷰에서 공통적으로 사용할 수 있는 상태나 설정을 공유할 때 적합합니다.
특성 | @StateObject | @ObservedObject | @EnvironmentObject |
생명 주기 관리 | 뷰가 객체의 생명 주기를 소유하고, SwiftUI가 관리 | 객체의 생명 주기를 소유하지 않음 | 객체의 생명 주기를 소유하지 않음 |
객체 생성 시점 | 뷰가 처음 생성될 때 SwiftUI가 자동으로 생성 | 객체는 외부에서 생성 후 뷰에 주입 | 객체는 상위 뷰에서 생성되어 하위 뷰로 전달 |
뷰의 생명 주기와의 연관성 | 객체의 생명 주기는 뷰와 연결 | 뷰와 객체의 생명 주기는 독립적 | 뷰와 객체의 생명 주기는 독립적 |
사용 목적 | 뷰에서 객체를 직접 소유하고 관리할 때 | 외부 데이터 모델에서 제공된 객체를 관찰할 때 | 여러 뷰에서 공통적으로 사용할 수 있는 객체를 제공할 때 |
데이터 공유 | 뷰 간에 데이터 공유가 어려움 | 여러 뷰가 동일한 객체를 공유 가능 | 모든 뷰 계층에서 사용될 수 있음 |
리소스 관리 | SwiftUI가 자동으로 관리 | 리소스 관리는 외부에서 이루어짐 | 리소스 관리는 외부에서 이루어짐 |
적용 사례 | 네트워크 요청, 데이터 로딩 관리 등 | 외부에서 관리되는 데이터 모델을 참조할 때 | 애플리케이션 전체에서 필요한 상태나 설정을 공유할 때 |
뷰 갱신 방식 | 객체 변경 시 뷰 자동 갱신 | 객체 변경 시 뷰 자동 갱신 | 객체 변경 시 뷰 자동 갱신 |
사용 예시 | @StateObject var loader = BookLoader() | @ObservedObject var loader: BookLoader | @EnvironmentObject var loader: BookLoader |
View 업데이트 생명 주기와 성능 최적화하기
SwiftUI 업데이트 생명 주기
- SwiftUI의 업데이트 주기는 UI가 이벤트에 반응하면서 시작됩니다. 이벤트가 발생하면 특정 액션이나 클로저가 실행되어 데이터(source of truth)를 수정하고, 수정된 데이터를 기반으로 새로운 뷰가 렌더링됩니다. 이 주기는 애플리케이션이 실행되는 동안 계속 반복됩니다.
성능 문제 해결 방법
- 성능 저하를 피하기 위해서는 뷰의 초기화 비용을 최소화해야 합니다. 뷰의 body는 순수 함수여야 하며, 부작용이 없어야 합니다. 뷰를 그리는데 집중하고, 네트워크 요청 등 무거운 작업은 지양해야 합니다.
- body는 언제 호출될지 예측할 수 없으므로, 호출 횟수를 기준으로 로직을 작성하지 않는 것이 중요합니다.
아래는 안좋은 코드 예시입니다.
@ObservedObject 를 사용하여 데이터를 생성하는 것은 뷰가 재생성될 때마다 새로운 객체를 생성하게 되어 매번 뷰가 다시 렌더링될 때마다 새로운 객체가 생성됩니다. 이는 객체가 불필요하게 여러 번 메모리에 할당되는 문제를 일으켜 불필요한 메모리 할당과 성능 저하를 유발합니다.
@ObservedObject 를 사용하면 뷰가 랜더링될 때마다 ReadingListViewer 객체가 새롭게 할당됩니다.
struct ReadingListViewer: View {
var body: some View {
NavigationView {
ReadingList()
Placeholder()
}
}
}
struct ReadingList: View {
@ObservedObject var store = ReadingListStore()
var body: some View {
// ...
}
}
좋은 코드 예시입니다.
위에서 발생한 문제를 해결하기 위해 SwiftUI 는 @StateObject 를 도입하였습니다. StateObject는 뷰의 생명 주기와 맞물려 적절한 시점에 객체를 초기화하고 불필요한 메모리 할당을 방지합니다. 즉 @StateObject 는 뷰의 최초 생성 시점에 한 번만 객체를 초기화하고, 이후 뷰가 다시 렌더링될 때도 같은 객체를 사용합니다.
@StateObject는 뷰가 다시 렌더링될 때 객체가 새로 생성되지 않고 기존 객체를 유지하면서 값을 업데이트합니다
struct ReadingListViewer: View {
var body: some View {
NavigationView {
ReadingList()
Placeholder()
}
}
}
struct ReadingList: View {
@StateObject var store = ReadingListStore()
var body: some View {
// ...
}
}
이벤트 소스
SwiftUI에서 이벤트는 사용자 상호작용이나 타이머, 알림 등 다양한 이벤트 소스에서 발생합니다. 새로운 기능으로 환경 변화나 바인딩, URL 처리 등에 대응할 수 있는 이벤트 핸들링 방법이 추가되었습니다. 이러한 이벤트는 주로 메인 스레드에서 실행되므로 무거운 작업은 백그라운드에서 처리하는 것이 좋습니다.
데이터 생명주기
데이터 수명 주기는 뷰(View), 씬(Scene), 앱(App)에 묶일 수 있습니다. 예를 들어,State와StateObject는 뷰의 수명 주기에 맞게 데이터를 관리합니다. 씬에 대한 데이터는 씬의 루트 뷰에 저장할 수 있으며, 전역 데이터는 앱에서 관리할 수 있습니다.
저장소 종류
SwiftUI는 새로운 저장소 시스템을 도입하여 데이터를 저장하고 복원할 수 있도록 했습니다. 예를 들어, SceneStorage는 씬에 대한 데이터를 관리하고, AppStorage는 UserDefaults를 통해 앱 전역 데이터를 관리합니다.
SceneStorage
SceneStorage는 씬(Scene) 단위로 데이터를 저장하고 복원할 수 있게 도와주는 저장소입니다. 뷰가 종료되거나 앱이 다시 시작되더라도 각 씬의 데이터를 자동으로 복원합니다.
- 예를 들어, 현재 뷰에서 사용자가 선택한 상태나 필터와 같은 일시적인 데이터를 저장하는 데 유용합니다.
- 주의할 점은, 뷰 내부에서만 사용할 수 있는 경량 데이터 저장소로, 씬 범위에서 데이터를 관리합니다.
struct ReadingListViewer: View {
@SceneStorage("selection") var selection: String?
var body: some View {
NavigationView {
ReadingList(selection: $selection)
BookDetailPlaceholder()
}
}
}
AppStorage
AppStorage는 앱 전역 데이터를 저장하고 복원할 때 사용합니다.
- AppStorage는 SwiftUI에서 UserDefaults와 통합된 속성 래퍼입니다
- 이 저장소는 UserDefaults를 통해 데이터가 영구적으로 저장되며, 앱을 종료하거나 다시 시작하더라도 데이터가 유지됩니다.
- 주로 설정 값, 사용자 기본값 등 앱 전체에서 공통으로 사용하는 데이터를 저장하는 데 적합합니다.
UserDefalts란?
Apple의 iOS, macOS, tvOS, watchOS 에서 제공하는 기본 저장소로, 앱의 간단한 데이터를 영구적으로 저장하는 데 사용됩니다. 이 데이터는 앱이 종료되거나 기기가 다시 부팅되더라도 유지되며, 앱 전반에서 쉽게 접근할 수 있습니다.
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 로 UIColor 저장하기
@AppStorage 를 사용하여 색깔을 저장할 때에는 Color 는 저장할 수 없습니다.
그렇기 때문에 따로 데이터로 인코딩을 해야합니다.
@AppStorage("buttonColorData") private var bookmarkColorData: Data = UIColor.blue.encode() ?? UIColor.blue.encode()!
@State private var buttonColor: UIColor = UIColor.decode(from: Data())
extension UIColor {
// UIColor를 Data로 인코딩
func encode() -> Data? {
do {
return try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false)
} catch {
print("UIColor 인코딩 실패: \(error)")
return nil
}
}
// Data를 UIColor로 디코딩
static func decode(from data: Data) -> UIColor {
do {
if let color = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data) {
return color
} else {
return .blue // 기본 색상
}
} catch {
print("UIColor 디코딩 실패: \(error)")
return .blue // 기본 색상
}
}
}
🔴 주의할점
SceneStorage 와 AppStorage 에 저장할 수 있는 데이터는 크지 않기 때문에 유의해야 합니다.
- AppStorage: UserDefaults 기반이므로 1~2MB 이하의 작은 데이터만 저장하는 것이 적합합니다. 큰 데이터는 따로 파일 시스템이나 데이터베이스에 저장하는 것이 권장됩니다.
- SceneStorage: UI 상태와 같은 아주 작은 데이터만 저장하는 용도로 사용해야 하며, 큰 데이터를 저장하려고 하면 성능 문제를 일으킬 수 있습니다.
'SwiftUI' 카테고리의 다른 글
WidgetKit - 위젯 만들기 전에 알아둘 것들 (10) | 2024.10.07 |
---|---|
WWDC24 Translation API (번역 API) (3) | 2024.10.05 |
SwiftUI - MapKit 에 대한 모든 것 (0) | 2024.09.08 |
SwiftUI - iCloud 사용하기 (0) | 2024.09.05 |
Swift - 대소문자 바꾸기 (아스키코드, map) 2가지 방법 (0) | 2024.08.23 |