ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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()
    }

     

    1. RootView 안에서 @StateObject 인 model 을 만듭니다.
    2. RootView 의 Model 을 이용하는 뷰는 아래의 3가지 입니다. 
      1. ObservedObjectView (뷰 모델을 관찰하는 용도)
      2. EnvironmentObjectView (앱 전역에서 공유되는 객체)
      3. 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 로 새로운 페이지를 로드하도록 설계했습니다.

     

    이렇게 되면 다음과 같은 상황이 발생됩니다.

    1. RootView 에서 제공하는 모델을 공유하는 뷰 3가지
      • ObservedObjectView, EnvironmentObject, BindingView
    2. StateObject 로 직접 뷰모델을 생성하는 뷰인 StateObjectView
    3. 뷰 내부에서 값만 들고 있는 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. 공통 뷰)

     

     

     

    1. 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

     

Designed by Tistory.