iOS/SwiftUI

[SwiftUI] 상태 관리 - PropertyWrapper (2)

kimsangjunzzang 2025. 4. 17. 05:14
728x90
반응형

안녕하세요, iOS 개발하는 루피입니다!


이전 글에서 SwiftUI의 상태 관리 기본 개념과 특징에 대해 알아보았습니다. 이번 글에서는 상태를 하위 뷰와 공유하는 방법, Observable 객체와의 활용, 그리고 성능 최적화와 일반적인 실수에 대해 알아보겠습니다.


바로 시작합니다.


하위 뷰와 상태 공유하기 (Share state with subviews)

상태 프로퍼티를 하위 뷰로 전달하면, 상위 뷰에서 해당 값이 변경될 때마다 SwiftUI는 하위 뷰를 자동으로 업데이트합니다. 하지만 하위 뷰는 이 값을 수정할 수 없습니다. 하위 뷰가 상태 값을 수정할 수 있게 하려면, 상태 대신 바인딩을 전달해야 합니다.

 

예를 들어, PlayButton에서 isPlaying 상태를 제거하고, 대신 바인딩을 받도록 변경할 수 있습니다.

struct PlayButton: View {
    @Binding var isPlaying: Bool // PlayButton이 이제 바인딩을 받음

    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            isPlaying.toggle()
        }
    }
}

그런 다음, 상태를 선언하고 해당 상태에 대한 바인딩을 생성하는 PlayerView를 정의할 수 있습니다. 상태의 projectedValue에 접근하여 바인딩을 얻을 수 있으며, 이는 프로퍼티 이름 앞에 달러 기호($)를 붙여 사용합니다.

struct PlayerView: View {
    @State private var isPlaying: Bool = false // 이제 여기서 상태를 생성

    var body: some View {
        VStack {
            PlayButton(isPlaying: $isPlaying) // 바인딩을 전달
            // ...
        }
    }
}

StateObject와 마찬가지로, SwiftUI의 저장소 관리와 충돌을 피하기 위해 State는 private으로 선언해야 합니다. 하지만 상태 객체와 달리, State는 항상 선언 시 기본값을 제공하여 초기화해야 합니다. State는 뷰와 그 하위 뷰에 국한된 로컬 저장소로만 사용해야 합니다.


Observable 객체와 @State (Store observable objects)

@Observable 매크로로 표시된 클래스 인스턴스를 @State 프로퍼티에 저장할 수 있습니다

@Observable
class Library {
    var name = "My library of books"
    // ...
}

struct ContentView: View {
    @State private var library = Library()

    var body: some View {
        LibraryView(library: library)
    }
}

@State 프로퍼티는 SwiftUI가 뷰를 초기화할 때 항상 기본값을 인스턴스화합니다. 따라서 기본값을 초기화할 때 부작용(side effect)이나 성능 집약적인 작업을 피해야 합니다. 예를 들어, 뷰가 자주 업데이트되는 경우, 뷰가 초기화될 때마다 새 객체를 할당하는 것은 비용이 많이 들 수 있습니다.

 

이를 해결하기 위해, 객체 생성을 task(priority:_:) 수정자를 사용하여 지연시킬 수 있습니다. 이 수정자는 뷰가 처음 나타날 때 한 번만 호출됩니다.

struct ContentView: View {
    @State private var library: Library?

    var body: some View {
        LibraryView(library: library)
            .task {
                library = Library()
            }
    }
}

관찰 가능한 상태 객체의 생성을 지연시키면, SwiftUI가 뷰를 초기화할 때마다 불필요한 객체 할당이 발생하지 않습니다. 또한, task(priority:_:) 수정자는 네트워크 호출이나 파일 접근 등 뷰의 초기 상태를 생성하는 데 필요한 다른 작업을 지연시키는 효과적인 방법입니다.


성능 최적화 관련 내용

@State 값이 변경될 때 SwiftUI는 다음과 같은 과정을 거칩니다:

  1. 뷰의 body를 재평가합니다.
  2. 이전 뷰 계층과 새로운 뷰 계층을 비교하는 디핑(diffing) 과정을 수행합니다.
  3. 실제로 변경된 부분만 업데이트하여 성능을 최적화합니다.

따라서 @State 값이 자주 변경되는 경우, 해당 값에 의존하는 뷰를 최소화하고 필요한 경우 뷰를 더 작은 컴포넌트로 분리하는 것이 좋습니다.


Note

ObservableObject 프로토콜을 준수하는 객체를 @State 프로퍼티에 저장할 수 있습니다. 하지만 이 경우, 뷰는 객체 참조가 변경될 때(예: 프로퍼티에 다른 객체 참조를 설정할 때)만 업데이트됩니다. 객체의 @Published 프로퍼티가 변경되어도 뷰는 업데이트되지 않습니다. 객체 참조와 객체의 @Published 프로퍼티 변경 사항을 모두 추적하려면, 객체를 저장할 때 @State 대신 @StateObject를 사용하세요.


@State와 @Published 비교

@State와 @Published는 모두 상태 관리에 사용되지만 다음과 같은 차이점이 있습니다:

  • @State: 단일 뷰에서 소유하고 관리하는 간단한 값 타입 데이터에 적합합니다. SwiftUI가 저장소를 관리하며, 뷰가 재생성되어도 상태는 유지됩니다.
  • @Published: ObservableObject 프로토콜과 함께 사용되며, 여러 뷰 간에 공유되는 복잡한 데이터에 적합합니다. 프로퍼티가 변경될 때 관찰자에게 알림을 보냅니다.

Observable 객체와 하위 뷰 공유하기 (Share observable state objects with subviews)

@State에 저장된 @Observable 객체를 하위 뷰와 공유하려면, 해당 객체의 참조를 하위 뷰로 전달하세요. SwiftUI는 객체의 관찰 가능한 프로퍼티가 변경될 때마다 하위 뷰를 업데이트하지만, 이는 하위 뷰의 body에서 해당 프로퍼티를 읽을 때에만 적용됩니다. 예를 들어, 아래 코드에서 BookView는 title이 변경될 때 업데이트되지만, isAvailable이 변경되어도 업데이트되지 않습니다:

@Observable
class Book {
    var title = "A sample book"
    var isAvailable = true
}

struct ContentView: View {
    @State private var book = Book()

    var body: some View {
        BookView(book: book)
    }
}

struct BookView: View {
    var book: Book

    var body: some View {
        Text(book.title)
    }
}

@State 프로퍼티는 값에 대한 바인딩을 제공합니다. 객체를 저장할 때는 해당 객체 참조에 대한 Binding을 얻을 수 있습니다. 이는 다른 하위 뷰에서 상태에 저장된 참조를 변경해야 할 때 유용합니다(예: 참조를 nil로 설정). 예시:

struct ContentView: View {
    @State private var book: Book?

    var body: some View {
        DeleteBookView(book: $book)
            .task {
                book = Book()
            }
    }
}

struct DeleteBookView: View {
    @Binding var book: Book?

    var body: some View {
        Button("Delete book") {
            book = nil
        }
    }
}

하지만 객체의 프로퍼티를 변경해야 할 때는 @State에 저장된 객체의 바인딩을 전달할 필요가 없습니다. 대신, 객체 참조를 전달하여 하위 뷰에서 객체의 프로퍼티를 새로운 값으로 설정할 수 있습니다:

struct ContentView: View {
    @State private var book = Book()

    var body: some View {
        BookCheckoutView(book: book)
    }
}

struct BookCheckoutView: View {
    var book: Book

    var body: some View {
        Button(book.isAvailable ? "Check out book" : "Return book") {
            book.isAvailable.toggle()
        }
    }
}


@Bindable을 활용한 객체 프로퍼티 바인딩

객체의 특정 프로퍼티에 대한 바인딩이 필요한 경우, 두 가지 방법이 있습니다:

  1. 객체에 대한 바인딩을 전달하고 필요한 곳에서 특정 프로퍼티에 대한 바인딩을 추출하거나,
  2. 객체 참조를 전달하고 @Bindable 프로퍼티 래퍼를 사용하여 특정 프로퍼티에 대한 바인딩을 생성합니다.

예를 들어, 아래 코드에서 BookEditorView는 @Bindable로 book을 래핑한 뒤, $ 구문을 사용하여 title에 대한 바인딩을 TextField에 전달합니다.

struct ContentView: View {
    @State private var book = Book()

    var body: some View {
        BookView(book: book)
    }
}

struct BookView: View {
    let book: Book

    var body: some View {
        BookEditorView(book: book)
    }
}

struct BookEditorView: View {
    @Bindable var book: Book

    var body: some View {
        TextField("Title", text: $book.title)
    }
}


일반적인 실수와 해결책

@State 사용 시 흔히 발생하는 문제와 해결책을 알아보겠습니다:

  1. Release 빌드에서 @State/@Binding이 업데이트되지 않는 문제:
    • 원인: Xcode의 Build Settings에서 Reflection Metadata Level이 "Off"로 설정된 경우 발생할 수 있습니다.
    • 해결책: Build Settings에서 Reflection Metadata Level을 "All"로 변경합니다.
  2. @State 값이 초기화되지 않는 문제:
    • 원인: SwiftUI 뷰는 여러 번 초기화될 수 있으며, 이때 @State 값도 다시 초기화됩니다.
    • 해결책: 비용이 많이 드는 초기화는 .task 또는 .onAppear 수정자를 사용하여 지연시킵니다.
  3. 하위 뷰에서 @State 값이 업데이트되지 않는 문제:
    • 원인: 하위 뷰에 값을 전달할 때 바인딩이 아닌 값 자체를 전달한 경우입니다.
    • 해결책: 하위 뷰가 값을 수정해야 한다면 @Binding을 사용하고, $ 접두사로 바인딩을 전달합니다.

https://developer.apple.com/documentation/swiftui/state

 

State | Apple Developer Documentation

A property wrapper type that can read and write a value managed by SwiftUI.

developer.apple.com

오늘도 화이팅입니다.

728x90
반응형