# [SwiftUI]  스유에서 뷰를 그리고(draw) 업데이트하는 데이터 흐름 관리 - @propertyWrappers 1

### @propertyWrapper..?

propertyWrapper는 Swift 5.1+ 에 소개된, 이름 그대로 property를 감싸는 역할을 하는 어트리뷰트다. 어떤 상태를 가지는 property를 정의하고 값을 반영하는 과정을 추상화 할 수 있게 해준다. 제네릭을 이용해서** 다양한 타입을 가질 수 있는 멤버 변수도 동일한 로직**을 타게 할 수 있다.

말이 너무 어려운데, propertyWrapper로 많이 관리되는 UserDefaults가 역시 나에게도 이해가 쉬웠다. UserDefaults는 String으로 된 키와 저장할 값을 기반으로 값(상태)을 저장하거나 불러올 수 있다. 저장은 새로운 값으로 업데이트가 될 수 있다는 뜻으로도 쓰일 수 있다. (덮어 쓸 수 있다는 말) 

UserDefaults에 값을 저장, 업데이트,불러오는 등의 로직은 아주 단순하지만 사용할 때마다 그 구문을 반복해야 한다. 그래서 보통 UserDefaults를 관리하는 객체를 만들어서 활용하는데, 이때 propertyWrapper 개념을 접목하면 조금 더 활용도가 높아진다.

```
@propertyWrapper
struct UserDefaultsWrapper<Value> {
   private let standard = UserDefaults.standard
   private let key: someKeyEnums
   
   let defaultValue: Value
   
   init(key: someKeyEnums, defaultValue: Value) {
      self.key = key
      self.defaultValue = defaultValue
   }
   
   var wrappedValue: Value {
      get {
         standard.value(forKey: key.rawValue) as? Value ?? defaultValue
      }
      
      set {
         standard.setValue(newValue, forKey: key.rawValue)
      }
   }
}
```

propertyWrapper 어트리뷰트가 붙은 객체는 내부적으로 wrappedValue라고 하는 연산 프로퍼티를 구현해주어야 한다. 이 wrappedValue 로직을 통해 멤버에 대해 동일한 로직을 반영해줄 수 있다. 생성자를 통해서 기본값도 넣어줄 수 있다.

정의한 propertyWrapper 객체는 어트리뷰트처럼 활용할 수 있다. @로 호출한 propertyWrapper의 생성자에 필요한 초기 값을 넣어준 형태로 멤버에 반영해주면, **따로 해당 멤버의 값(상태)을 불러오거나 업데이트 하는 로직을 신경쓰지 않아도 된다.**

```
struct UserSettings {
  @UserDefaultsWrapper(key: .name, defaultValue: "이름")
  var name: String

  @UserDefaultsWrapper(key: .age, defaultValue: 12)
  var age: Int
}

let userInfo = UserSettings()
userInfo.name = "새로운 이름"
userInfo.age // 12
```

SwiftUI에서도 다양한 propertyWrapper들이 활용되고 있는데, 특히 뷰를 그리고, 뷰를 업데이트하는 데이터(상태 값)를 효율적으로 관리할 수 있게 도와주는 로직이 구현되어 있다.

![Image](https://upload.cafenono.com/image/slashpageHome/20240909/172840_ZX2Zbl9rWTcpJH6TFA?q=80&s=1280x180&t=outside&f=webp)

### @State

- 이름에서 알 수 있듯이, View 객체의 데이터 상태를 관리하는 멤버 앞에 붙는다.

- View struct 안에서 활용되기에 당연히 View 객체 안에서 초기화 되어야 한다. 

- 특정 View 객체 안에서 사용되고, 초기화되기 때문에 외부에서 주입을 받아 상태가 관리되지 않아야 한다. (사실 주입은 받을 수 있겠지만, 내부에서 그 주입받은 상태가 관리되지 않기 때문에 private 사용이 권장된다.)

    - 뷰 객체 내부에서 활용되는 자식 뷰 객체에게 그 상태값을 바인딩 프로퍼티와 함께 전달해줄 수 있다.

- 가장 좋은 점은, @State로 관리되는 상태값에 따라 뷰가 fresh-render되고 뷰가 살아있는 동안 그 상태값이 유지된다는 것이다.

    - class의 인스턴스와 같은 참조형을 상태값으로 활용할 수는 있지만, 해당 인스턴스의 값을 변경한다고 하더라도 상태가 변경되었다고 인지되지는 않는다. (@Published, @ObservableObject와 같은 wrapper가 붙으면 말이 달라지긴 하지만)

- State 의 구현부를 타고 가보면 DynamicProperty 라는 프로토콜을 채택하고 있는 것을 볼 수 있다. 

    - DynamicProperty 프로토콜은 내부적으로 update라고 하는 메서드를 가지고 있고, 이 메서드를 통해서 변경된 상태를 인지하고 뷰를 새롭게 업데이트 해주게 된다.

```
private struct SomeCountView: View {
    @State private var count: Int = 0
    
    var body: some View {
        VStack {
            Text("\(count)")
            
            Button(
                action: {
                    count += 1
                },
                label: {
                    Text("값 1 올리기")
                }
            )
            
            Button(
                action: {
                    count -= 1
                },
                label: {
                    Text("값 1 내리기")
                }
            )
        }
    }
}
```

### @Binding

- 이름 그대로 어떤 상태를 View 객체에 묶어두는 역할을 한다. 

- 보통 부모 객체에서 자식 객체에 전달하는 상태값을 활용할 때 사용된다. 

    - 다른 View 객체가 가지고 있는 상태값을 다른 객체에 '연결'한다는 표현이 더 적절할 수 있다.

    - 그렇기 때문에, 자식 객체는 상태에 대한 저장 공간을 확보하거나 소유권을 가질 수 없다.

- 부모 객체에서 자식 객체로 상태값을 전달할 때는(바인딩할 때는) $ 기호를 활용한다. 

    - 이는 propertyWrapper의 projectedValue라고 하는 연산 프로퍼티를 활용하는 것이다.

    - Binding된 상태가 자식에 의해서 projectedValue 연산 프로퍼티의 내부 로직을 타게 되고, 그 값이 부모의 상태 변경까지 이어진다. (Derived Value)

```
struct ContentView: View {
    @State private var count: Int = 0
    
    var body: some View {
        VStack {
            Text("\(count)")
                .padding(.bottom)
            
            SomeButton(count: $count) // $ 기호로 상태값 바인딩
        }
    }
}

private struct SomeButton: View {
    @Binding var count: Int
    
    var body: some View {
        Button(
            action: {
                count += 1
            },
            label: {
                Text("값 1 올리기")
            }
        )
    }
}
```

### ObservableObject 프로토콜로 구조화된 상태 관리

- ObservableObject 프로토콜은 Combine 프레임워크에 정의되어 class-type에 한정하여 채택할 수 있다. 

    - ObservableObject를 채택한 객체는 값이나 메서드를 참조시키는 인스턴스라는 의미이다.

    - 내부 값을 참조하고 있다가 변경될 때에 필요한 형태로 뷰를 업데이트 시키고 싶을 때 활용될 수 있다.

**@Published**

- 역시 마찬가지로 Combine 프레임워크에 기반한 propertyWrapper다.

- ObservableObject 프로토콜을 채택한 클래스 내부의 멤버들에 붙어서 값이 업데이트 될 수 있음을 표현하고, 값이 변화된 것을 객체에 알려준다. 

- 멤버 변수들에 붙기 때문에, 내부 메서드에서 값이 업데이트 되거나 활용될 수 있다.

**@StateObject**

- ObservableObject 프로토콜을 채택한 객체의 인스턴스를 @State 처럼 뷰의 '상태'로 활용한다.

    - @State와의 가장 큰 차이는 상태를 가진 객체를 항상 참조하고 있다는 것이다. 

- **@Published** 로 선언된 상태 객체의 멤버를 바라보고 있다가, 상태가 변경되면 뷰를 업데이트 한다. 

- 뷰가 초기화 되면서 단 한 번 인스턴스 초기화를 진행한다. 이후에는 초기화된 인스턴스 내부의 멤버를(Published) 바라보면서 뷰를 업데이트 한다. (객체를 재사용한다)

    - 뷰가 fresh render 되더라도 인스턴스를 유지한다는 소리다.

**@ObservedObject**

- @StateObject 의 @Binding과 같은 존재라고 생각하면 좋다.

    - @Binding과 다른 점은, $ 표기를 해주지 않아도 상태 객체가 뷰에 바인딩 된다는 것이다. (객체 인스턴스를 주입해주는 개념이다.)

- 마찬가지로 ObservableObject를 채택한 상태 객체를 뷰에 넣어줄 때 활용한다.

    - 부모 뷰에서 참조하고 있는 상태 객체를 자식 객체에서도 활용할 수 있게 데이터를 아래 방향으로 내려주는 역할을 한다고 보면 된다.

- @StateObject와 사용성이 거의 같지만 가장 큰 차이는, 부모에 의해서 또는 자식이 나에 의해서 뷰가 fresh render 될 경우, **자식 뷰에 초기화되어 있는 @ObservedObject 객체는 다시 새롭게 초기화**가 된다.

    - 따라서, ObservedObject는 부모가 상태를 관리할 객체를 참조하고 있을 경우에만 주입하는 형태로 사용하는 것이 좋다.

```
class CountObservable: ObservableObject {
  @Published var count: Int = 0

  func countUp() { count += 1 }
  func countDown() { count -= 1 }
}

struct CounterView: View {
  @StateObject private var countObservable = CountObservable()
  
  var body: some View {
    // ObservableObject를 자식뷰에 바인딩 시켜줄 수 있다.
    CounterTextView(countObservable: countObservable)
    
    Button {
      countObservable.countUp() // @published 멤버를 업데이트 할 수 있다.
    } label: { Text("Count Up") }
    
    Button {
      countObservable.countDown()
    } label: { Text("Count Down") }
  }
}

struct CounterTextView: View {
  @ObservedObject var countObservable: CountObservable

  var body: some View {
    Text("\(countObservable.count)")
  }
}
```

> iOS17+ 부터 [@Observable](https://developer.apple.com/documentation/observation) 이라고 하는 프로토콜이 새롭게 추가되었다.
상태로 관리할 클래스에 해당 macro를 붙여주면 @StateObject나 ObservableObject, @Published 선언 없이, 뷰에서 @State propertyWrapper로 참조 형태의 상태를 관리할 수 있다.
- 값타입, 참조타입의 상태를 모두 @State 로 관리할 수 있다.

---

2편에서는 EnvironMent, EnvironmentObject, AppStorage, SceneStorage 등을 정리해보겠습니다.

For the site tree, see the [root Markdown](https://slashpage.com/hankyeol.md).
