@PropertyWrapper (with WWDC)

Jason·2023년 11월 27일
0

최근 WWDC를 2019년도 버전부터 찾아보는 습관을 들이고있는 과정에서
SwiftUI함수형 프로그래밍에 대해 관심을 갖게되었네요!
SwiftUI를 학습하기 위해서는 가장 기본적으로 맞이하게되는 @PropertyWrapper에 대해
알고 넘어가야할 필요성을 느껴 학습하며 정리한 내용입니다.

1️⃣ 왜 사용해야 할까❓

PropertyWrapper는 값 저장을 관리하는 코드를 재사용하기 위한 매커니즘을 제공하기 위해서
Swift 5.1에서 공개되었습니다.
PropertyWrapper를 사용하면 특정 동작 또는 로직을 캡슐화(Encapsulation)❗️
하여 여러 프로퍼티에 쉽게 재사용할 수 있다는 장점이 있습니다.

2️⃣ 예제 코드를 통한 이해

기본적인 코드를 살펴보며 매커니즘을 이해해보죠!

// 12 이하로 제한되도록 하는 프로퍼티를 생성하는 propertyWrapper 정의
import Foundation

@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    
    init() { self.number = 0 }
    
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

struct SmallRectangle {
    @TwelveOrLess var width: Int
    @TwelveOrLess var height: Int
}

/* 명시적으로 사용하는 경우
struct SmallRectangle {
    private var _width = TwelveOrLess()
    private var _height = TwelveOrLess()
    
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
    
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
}
*/

var smallRect = SmallRectangle()
print(smallRect.width, smallRect.height)        // 0, 0

smallRect.width = 10
smallRect.height = 15

print(smallRect.width, smallRect.height)        // 10, 12

1. 정의

PropertyWrapper를 사용하기 위해서는 @propertyWrapper라는 키워드를 사용하여 프로퍼티가
저장되는 방식을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리 계층을
추가합니다. (즉, 값의 저장과 접근을 관리하는 로직 2가지로 존재)

2. 사용:

SmallRectangle에서 width, height에서 볼 수 있듯이 PropertyWrapper를 적용하려면
프로퍼티 선언 앞에 @wrapper의 이름을 붙입니다.
이렇게 하면 해당 프로퍼티의 저장과 접근 방식이 Wrapper에 정의된 대로 처리되죠!
(결과값이 10, 12가 나온 이유를 알 수 있습니다.)

3. 구성요소

  • wrappedValue : 필수 작성 프로퍼티로 실제 값을 저장하고 접근하는 데 사용되는
    프로퍼티 (struct, class, enum)
  • init : PropertyWrapper를 초기화하는데 사용됩니다.

3️⃣ 장점 및 단점

장점 👍🏻

  1. 코드 재사용성: 같은 로직을 여러 프로퍼티에 적용할 때 유용합니다.
  2. 캡슐화 및 추상화: 복잡한 로직을 wrapper 내부에 숨기고, 사용자에게는
    간단한 인터페이스를 제공합니다.
  3. 명확성과 읽기 쉬운 코드: 로직이 분리되어 있어 프로퍼티의 목적이 더 명확해집니다.

단점 👎🏻

  1. 유연성을 제공하지만, 오버 엔지니어링을 피하고 실제 필요한 경우에만 사용해야 합니다.
  2. 복잡한 로직은 디버깅을 어렵게 만들 수 있으므로 주의가 필요합니다.
    즉, 그 적용 범위와 목적을 명확히 이해하고 적절하게 사용하는 것이 중요합니다.

4️⃣ 좀 더 나아가서..

Apple의 공식문서에서는 아래와 같은 케이스에서 적용하면 좋다고 합니다.

  1. Thread의 안전 검사를 제공하기 위한 경우
  2. 데이터베이스에 저장하는 프로퍼티가 있는 경우 (Entity??)

WWDC19에서 사용한 UserDefaults 예제가 있으며,
개발하는 정대리님의 강의에서는 API를 받아올 때 Int 타입으로 명시는 되어있지만
String 타입으로 받아오는 경우에 사용하는 방법을 보여주셔서 다양한 경우에 적용되는 것 같습니다.

//MARK: - UserDefault 만들기
@propertyWrapper
struct UserDefault<T> {
    
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get {
            UserDefaults.standard.object(forKey: key) as? T ?? self.defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: self.key)
        }
    }
}

class UserManager {
    @UserDefault(key: "userID", defaultValue: false)
    static var userID: Bool
    
    @UserDefault(key: "myEmail", defaultValue: nil)
    static var myEmail: String?
    
    @UserDefault(key: "isLoggedIn", defaultValue: false)
    static var isLoggedIn: Bool
}
//MARK: - TodoResponse
struct TodoResponse: Codable {
    let createAt, title: String
    let id: String
    
    @AceeptStringToInt
    var viewCount: Int
}

@propertyWrapper
struct AceeptStringToInt: Codable {
    let wrappedValue: Int
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        
        if let stringValue = try? container.decode(String.self) {
            self.wrappedValue = Int(stringValue) ?? 0
        } else {
            self.wrappedValue = try container.decode(Int.self)
        }
    }
}   

5️⃣ lazy와 NSCopying과의 관련성

Swift에서는 다양한 프로퍼티 구현 패턴을 간편하게 만들기 위해
이전에는 특정한 언어 차원에서의 기능을 지원하였습니다.
하여 공식 문서에서 이야기하는 차원이 lazy 키워드와 NSCopying이며
lazy는 객체가 실제로 필요할 때까지 초기화를 지연시키고 이는 Swift 언어 초창기 시절부터 내장되어 사용되어왔죠.
NSCopying은 Objective-C에서 가져온 것으로, 프로터티의 값이 복사되어 할당되도록 클래스에서 깊은 복사를 사용할 때 사용하는 방식 중 하나였죠.

이러한 특정 패턴들은 언어 차원에서 직접 지원하는 것으로 한계가 있었다고 합니다.

  1. 제한된 범위와 유용성
  2. 유연성 부족

lazy와 NSCopying 같은 특성은 특정 사용 사례에만 유용하였고...
다른 종류의 패턴을 구현하려면 개발자가 많은 양의 보일러플레이터(반복적인) 코드를 작성해야만 했습니다.
즉, 재사용성에서 부족한 점이 보였다는 점이였죠..

새로운 프로퍼티 패턴이 필요할 때마다 언어 자체를 확장하거나 변경해야하는 문제가 있었고 이는 개발자들이 자신만의 커스텀 프로퍼티 패턴을 작성하는데 어려움이 발생했다는 점입니다.

즉, PropertyWrapper는 lazy와 NSCopying의 언어 차원에서 지원되는 특정 패턴에 국한되지 않고 더 넓은 범위의 패턴에서 유연하게 지원하기 위해 도입된 매커니즘으로서 편리함을 제공하기 위함이였네요❗️

위 내용을 바탕으로 Example로 구현되어있는 내용들이 있어 함께 살펴보면 좋을 것 같습니다 👍🏻
(WWDC19| Modern Swift API Design 에서도 NSCopying과
Delayed Initialization에 대해 보여준 내용이 꽤 어려웠지만 흥미로웠습니다!)

6️⃣ projectedWrapper

PropertyWrapper 내에서 projectedValue라는 이름의 프로퍼티를 정의함으로써,
개발자는 특정 프로퍼티에 대한 추가적인 정보나 기능을 제공할 수 있습니다.
아래의 예시 코드를 보면 훨씬 이해하기가 쉬웠습니다.

@propertyWrapper
struct ExampleWrapper {
    var wrappedValue: Int
    var projectedValue: String {
        return "Current Value: \(wrappedValue)"
    }
}

struct ExampleStruct {
    @ExampleWrapper var number: Int
}

var example = ExampleStruct(number: 5)
print(example.$number)  // "Current Value: 5" 출력

projectedValue$키워드로 해당 값에 접근할 수 있는 기능으로
wrappedValue로 wrapping된 값에 직접 접근하는 방법이 아닌
부가적으로 프로퍼티에 접근하는 방법입니다.
아직 Combine에 대해 학습하지 못하였지만 해당 내용을 많이 접할 수 있다고 하네요..😋

🌐 Reference Site

Properties | Documentation - Swift.org
Github | swift-evolution/proposals/propertyWrapper
Modern Swift API Design - WWDC19 - Videos

Property Wrapper - ZeddiOS - 티스토리
취준생을 위한 iOS 앱만들기 - API 파싱 propertyWrapper - iOS Dev Tutorial (2022)
[iOS - swift] @propertyWrapper의 projectedValue 개념 ($ 접두사, 달러 접두사)

profile
🧑🏼‍💻 iOS developer

0개의 댓글

관련 채용 정보