-
SwiftUI - Property Wrapper 언제 써야할까 ?카테고리 없음 2025. 4. 22. 15:17
프로퍼티 래퍼란 ?
Swift에서는 변수(property)에 부가적인 동작을 자동으로 추가할 수 있는 특수한 문법이 존재합니다.
이것이 바로 프로퍼티 래퍼(Property Wrapper)입니다.
보통 @로 시작하는 문법으로 작성되며, 값 저장, 읽기, 변경 감지 등의 기능을 캡슐화하여 간단하게 사용할 수 있도록 도와줍니다.
SwiftUI에서는 이 기능을 활용해 @State, @Published, @AppStorage 등 다양한 상태 관리를 구현할 수 있습니다.
SwiftUI 의 대표적인 Property Wrapper
프로퍼티 래퍼 기능 요약 @State 값 상태 관리, 변경되면 뷰 업데이트 @StateObject 클래스 상태 생성 및 소유 @ObservedObject 외부에서 주입된 클래스 상태를 관찰 @Binding 부모의 상태와 자식 뷰의 양방향 연결 @EnvironmentObject 앱 전역 공유상태를 뷰에서 사용 @AppStorage UserDefaults에 자동연결
@State
개념
@State
는 뷰 내부에서 사용하는 간단한 값 타입의 상태를 관리할 때 사용합니다.
용도
- Bool, Int, String 등 간단한 값 타입 상태를 뷰 내부에서만 사용할 때 유용합니다.
특징
- 값이 변경되면 해당 뷰를 자동으로 다시 렌더링 합니다.
- 이 값은 뷰 외부에서 직접 접근하거나 변경할 수 없습니다.
예시
struct CounterView: View { @State private var count = 0 var body: some View { Button("증가") { count += 1 } Text("카운트: \(count)") } }
@StateObject
개념
- 뷰 내부에서 ObservableObject를 생성하고 소유하며, 해당 객체의 변경 사항에 따라 뷰도 업데이트 됩니다.
용도
- 뷰 내부에서 객체를 생성하고 해당 객체를 뷰의 상태로 관리할 때 사용합니다.
특징
- 객체의 생명 주기를 뷰와 함께 관리합니다.
- 뷰가 다시 생성되더라도 객체는 유지됩니다.
예시
class CounterModel: ObservableObject { @Published var count = 0 } struct CounterView: View { @StateObject private var model = CounterModel() var body: some View { Button("증가") { model.count += 1 } Text("카운트: \(model.count)") } }
@ObservedObject
개념
@ObservedObject
는 외부에서 생성된 ObservableObject를 관찰하여
그 객체의 변경 사항에 따라 뷰를 자동으로 업데이트합니다.
용도
- 뷰 외부에서 생성된 객체를 뷰에 주입하여 관찰할 때 사용합니다.
특징
- 객체의 소유권은 외부에 있으며, 뷰는 해당 객체를 단순히 관찰합니다.
- 객체가 변경되면 뷰가 자동으로 렌더링 됩니다.
예시
class TimerModel: ObservableObject { @Published var time = 0 } struct TimerView: View { @ObservedObject var model: TimerModel var body: some View { Text("시간: \(model.time)") } }
@EnvironmentObject
개념
- 앱 전체에서 공유되는
ObservableObject
를 환경에 주입하여 여러 뷰에서 사용할 수 있도록 만들어줍니다.
용도
- 앱 전체에서 공유되는 데이터를 여러 뷰에서 사용해야 할 때 적합합니다.
특징
- 상위 뷰에서 객체를 환경에 주입하면,하위 뷰에서는 @EnvironmentObject로 접근할 수 있습니다.
- 객체가 변경되면 해당 객체를 사용하는 모든 뷰가 자동으로 업데이트됩니다
예시
class UserSettings: ObservableObject { @Published var username: String = "사용자" } struct ContentView: View { var body: some View { ProfileView() .environmentObject(UserSettings()) } } struct ProfileView: View { @EnvironmentObject var settings: UserSettings var body: some View { Text("안녕하세요, \(settings.username)님!") } }
@Binding
개념
@Binding은 SwiftUI에서 값의 소유권 없이 외부 상태의 값을 읽고 수정할 수 있도록 해주는 연결고리입니다.
보통 부모 뷰의 상태를 자식 뷰에서 조작하고 싶을 때 사용됩니다
용도
- 자식 뷰에서 부모 상태를 변경해야 할 때
- 입력값/토글 등 자식에서 직접 값 조작이 필요한 경우
- 전체 ViewModel이 아니라 속성 단위의 상태 공유가 필요한 경우
특징
- 클래스보단 Bool, String, Int 같은 값 타입이 일반적
- 뷰가 직접 값을 소유하지 않음 (@State와 다름)
- get/set 클로저가 자동으로 설정됨
- 상위 뷰에서 $value 형태로 넘겨줘야 함
- 바인딩된 값이 바뀌면 SwiftUI가 다시 그려줌
- 자기 뷰에서 직접 쓸 수 없음
@Binding var name = "" ❌ // 에러
예시
struct ParentView: View { @State private var isOn: Bool = false var body: some View { ToggleView(isOn: $isOn) // ← $ 붙여서 바인딩 전달 } } struct ToggleView: View { @Binding var isOn: Bool var body: some View { Toggle("알림 설정", isOn: $isOn) } }
요약
프로퍼티 래퍼 생성 위치 소유권 사용 예시 @State 뷰 내부 뷰 간단한 값 타입 상태 관리 @ObservedObject 뷰 외부 외부 외부에서 주입된 객체 관찰 @StateObject 뷰 내부 뷰 뷰 내부에서 생성한 객체 관리 @EnvironmentObject 앱 전역 환경 앱 전역에서 공유되는 객체 사용 @Binding 뷰 외부 외부 부모 상태를 자식이 변경
예시 프로젝트
import SwiftUI struct RootView: View { @StateObject private var model = CounterModel() var body: some View { VStack { ObservedObjectView(model: model) Divider() EnvironmentObjectView() .environmentObject(model) Divider() StateObjectView() Divider() BindingView(count: $model.count) Divider() StateView() } } } } #Preview { RootView() }
- RootView 안에서 @StateObject 인 model 을 만듭니다.
- RootView 의 Model 을 이용하는 뷰는 아래의 3가지 입니다.
- ObservedObjectView (뷰 모델을 관찰하는 용도)
- EnvironmentObjectView (앱 전역에서 공유되는 객체)
- BindingView (특정 count 만 상태를 변경하기 위함)
// struct ObservedObjectView: View { @ObservedObject var model: CounterModel ... } struct BindingView: View { @Binding var count: Int ... } struct EnvironmentObjectView: View { @EnvironmentObject var model: CounterModel ... }
위 3가지는 같은 상태를 공유하고 있기 때문에
다음과 같이 서로 연결된 것을 확인할 수 있습니다.
이제 @State , @StateObject 를 통해서 차이를 알아보겠습니다.
@State 는 뷰 내부에서 관리되는 간단한 값 상태, @StateObject 는 뷰 내부에서 ObservableObject를 생성하고 소유합니다.
따라서 이 두가지의 상태를 가진다면 외부에서 값을 주입하는 것이 불가합니다.
struct StateObjectView: View { @StateObject private var model = CounterModel() // 뷰모델을 새로 생성함 ... } struct StateView: View { @State private var count = 0 // 값 타입의 상태 ... }
뷰 렌더링 방식
지금까지 모델의 공유 상태에 따라서 각 뷰에 미치는 영향을 알아봤습니다.
이젠 뷰를 다시 렌더링 할때에 SwiftUI 는 어떤 방식으로 뷰를 렌더링 하는지 Print 로그를 통해 알아보겠습니다.
앞서 예제는 한개의 뷰에서 모든 뷰를 보여줬지만 이제 각 페이지에서 관리 할 수 있도록 List 를 생성해 NavigationLink 로 새로운 페이지를 로드하도록 설계했습니다.
이렇게 되면 다음과 같은 상황이 발생됩니다.
- RootView 에서 제공하는 모델을 공유하는 뷰 3가지
- ObservedObjectView, EnvironmentObject, BindingView
- StateObject 로 직접 뷰모델을 생성하는 뷰인 StateObjectView
- 뷰 내부에서 값만 들고 있는 StateView
🔍 상태 변경 시 뷰의 리렌더링 흐름 관찰 (List View)
뷰가 다시 그려지는 것을 확인하기 위해 다음과 같이 print 로그를 추가합니다.
struct ObservedObjectView: View { @ObservedObject var model: CounterModel var body: some View { print("🔁 ObservedObjectView") return VStack { ... } } struct EnvironmentObjectView: View { @EnvironmentObject var model: CounterModel var body: some View { print("🔁 EnvView body") return VStack { Text("💡@EnvironmentObject 사용 뷰.") ... } } } struct BindingView: View { @Binding var count: Int var body: some View { print("🔁 BindingView body") return VStack { Text("💡@Binding 사용 뷰.") ... } } }
1.RootView가 초기화됩니다.
화면 진입 시, RootView의 init() 로그가 출력되며 루트 뷰가 초기화됩니다.
이 시점에는 아직 하위 뷰들은 생성되지 않았습니다.ObservedObjectView 를 클릭해보겠습니다.
2. ObservedObjectView 이동
버튼을 눌러 ObservedObjectView로 이동하면,뷰가 새로 생성되고 해당 뷰의 init()과 body()가 호출됩니다.
3. ObservedObjectView의 버튼을 클릭해 모델의 상태를 변경합니다.
- ViewModel 내부 로그 (count 변경 로그)
- ObservedObjectView의 body() 재호출
- 🔁 RootView의 body()도 함께 다시 호출됨
- 👉 이는 공유된 모델이 변경됨에 따라 상위 뷰도 영향을 받았기 때문입니다
4. 이번에는 StateObjectView를 선택합니다.외부와 상태를 공유하지 않습니다.
해당 뷰는 자체적으로 뷰모델을 @StateObject로 생성하고 있으므로 외부와 상태를 공유하지 않습니다 증가 버튼을 누르면
- StateObjectView 내부의 model.count만 변경되고 해당 뷰의 body()만 다시 호출됩니다
- ✅ RootView는 전혀 영향을 받지 않고, 다시 그려지지 않습니다.
🔍 상태 변경 시 뷰의 리렌더링 흐름 관찰 (2. 공통 뷰)
- ObservedObjectView, EnvironmentObjectView ,BindingView ( 같은 모델 공유)
이 두 뷰는 RootView의 @StateObject 모델을 공유하고 있으며,
내부에서 model.count 값을 변경하면 다음이 발생합니다:
- 🔁 ObservedObjectView 자체가 다시 렌더링
- 🔁 EnvironmentObjectView 도 함께 렌더링
- 🔁 BindingView, RootView도 공유된 모델 상태가 바뀌었기 때문에 같이 리렌더링됨
2. StateObjectView, StateView (뷰에서 상태 관리)
이제 상태를 공유하지 않는 뷰인 StateObjectView, StateView를 확인해봅니다.
이 뷰들은 각각 내부에서 @StateObject, @State로 상태를 갖고 있기 때문에,
다른 뷰에서 모델이 변경되더라도 전혀 영향을 받지 않습니다.
- 🔁 오직 해당 뷰 자신만 body()가 다시 호출됨
- ✅ 외부에서 재렌더링되지 않음
✅ 요약: 상태 공유와 렌더링 관계
뷰 이름 모델 공유 여부 상태 변경 시 영향받는 뷰 ObservedObjectView ✅ 공유됨 본인 + RootView + 다른 공유 뷰들 EnvironmentObjectView ✅ 공유됨 본인 + RootView + 다른 공유 뷰들 BindingView ✅ 공유됨 (일부 값만) 본인 + RootView + 다른 공유 뷰들 StateObjectView ❌ 자체 생성 본인만 StateView ❌ 자체 상태 본인만 📌 요약 정리
- 뷰가 다시 렌더링된다고 해서 모든 구조가 다시 그려지는 것은 아님
- SwiftUI는 뷰의 변경을 감지해, 실제로 바뀐 뷰만 다시 그리도록 최적화
- @State, @StateObject는 자체 뷰에만 국한되며, 외부에는 영향을 주지 않음
- 반대로 모델을 공유하는 경우, 공유된 모든 뷰가 영향을 받음
https://github.com/gadisom/iOS_DeepDive