Property Wrapper

김민준·2022년 6월 9일
0

Swift

목록 보기
2/2

Swift 5.1에서 추가된 기능인 Property Wrapper에 대해서 살펴보도록 하겠습니다.

필요성

연산 프로퍼티의 중복되는 로직을 프로퍼티 래퍼에 한 번만 구현하고,
이 로직을 필요로 하는 프로퍼티들 자체에 연결하여 사용할 수 있게 하여
코드의 재사용성을 높일 수 있습니다.

정의하는 방법

struct, class, enum을 만들고 @propertyWrapper 를 맨 앞에 명시해주면 프로퍼티 래퍼를 정의할 수 있습니다.

그리고 반드시 wrappedValue라는 연산 프로퍼티를 구현하여야 합니다.

UserDefaults 예제를 통해 한 번 알아보겠습니다. 먼저 Property Wrapper 전의 코드 상태입니다.

class UserManager {
    static var usesTouchID: Bool {
        get { return UserDefaults.standard.bool(forKey: "usesTouchID") }
        set { UserDefaults.standard.set(newValue, forKey: "usesTouchID") }
    }
    
    static var myEmail: String {
        get { return UserDefaults.standard.string(forKey: "myEmail") }
        set { UserDefaults.standard.set(newValue, forKey: "myEmail") }
    }
    
    static var numberOfAccounts: Bool {
        get { return UserDefaults.standard.bool(forKey: "numberOfAccounts") }
        set { UserDefaults.standard.set(newValue, forKey: "numberOfAccounts") }
    }
}

첫째로, UserDefaults를 활용하여 값을 읽거나 쓸 때, 그 key 값으로 항상 String을 사용하고 있는 것을 확인할 수 있습니다.

둘째로, 값을 읽거나 쓸 때, 값의 타입과 관계없이 UserDefaults.standard까지는 동일하게 사용하고 있는 것 역시 확인할 수 있습니다.

마지막으로 가장 중요한건, 위 클래스의 세 연산 프로퍼티가 결국 공통적인 로직을 공유하고 있습니다.

위 세 가지 특징을 파악했으니, 이를 활용하여 Property Wrapper를 이용해서 개선해보도록 하겠습니다.

@propertyWrapper
struct UserDefault<T> {
    let key: String // key는 항상 String 타입let defaultValue: T 
    let defaultValue: T
    private let storage: UserDefaults
    
    var wrappedValue: T { // property wrapper를 사용하려면 반드시 이 이름으로 연산 프로퍼티를 구현하여야 함
        get {
            storage.object(forKey: key) as? T ?? defaultValue
        }
        set {
            storage.set(newValue, forKey: key)
        }
    }
    
    init(key: String, defaultValue: T, storage: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.storage = storage
    }
}

class UserManager {
    @UserDefault(key: "usesTouchID", defaultValue: false)
    static var usesTouchID: Bool
    
    @UserDefault(key: "myEmail", defaultValue: "")
    static var myEmail: String
    
    @UserDefault(key: "numberOfAccounts", defaultValue: 0)
    static var numberOfAccounts: Int
}

UserManager.usesTouchID = true // set
print(UserManager.usesTouchID) // get

UserManager.myEmail = "myEmail" // set
print(UserManager.myEmail) // get

UserManager.numberOfAccounts = 3 // set
print(UserManager.numberOfAccounts) // get

위 코드의 결과는 다음과 같습니다.

true
myEmail
3

원하는대로 동작했음을 확인할 수 있습니다.

projectedValue

추가로 제공하고 싶은 정보가 있다면 어떤 타입이라도 projectedValue를 정의하여 사용할 수 있습니다.
이 값은 프로퍼티 래퍼로 감싼 프로퍼티 앞에 $를 붙여 접근할 수 있습니다.

예를 들어, 위의 예시에서 defaultValue 값을 알고는 있지만 UserManager 클래스를 통해 현재 설정되어 있는 defaultValue를 갖고 오고 싶다고 가정을 해보겠습니다. 현재 상태에서는 바로 defaultValue에 접근할 수 없습니다. 이 때, projectValue를 활용하면 쉽게 defaultValue를 얻어낼 수 있습니다.

@propertyWrapper
struct UserDefault<T> {
    let key: String // key는 항상 String 타입let defaultValue: T 
    let defaultValue: T
    private let storage: UserDefaults
    
    var wrappedValue: T { // property wrapper를 사용하려면 반드시 이 이름으로 연산 프로퍼티를 구현하여야 함
        get {
            storage.object(forKey: key) as? T ?? defaultValue
        }
        set {
            storage.set(newValue, forKey: key)
        }
    }
    
    var projectedValue: T { // 반드시 이 이름을 사용하여야 동작함
        defaultValue
    }
    
    init(key: String, defaultValue: T, storage: UserDefaults = .standard) {
        self.key = key
        self.defaultValue = defaultValue
        self.storage = storage
    }
}

class UserManager {
    @UserDefault(key: "usesTouchID", defaultValue: false)
    static var usesTouchID: Bool
    
    @UserDefault(key: "myEmail", defaultValue: "")
    static var myEmail: String
    
    @UserDefault(key: "numberOfAccounts", defaultValue: 0)
    static var numberOfAccounts: Int
}

print(UserManager.$usesTouchID) // projectedValue
print(UserManager.$myEmail) // projectedValue
print(UserManager.$numberOfAccounts) // projectedValue

위 코드의 결과는 다음과 같습니다.

false

0

정상적으로 동작했음을 확인할 수 있습니다.

profile
trial and error

1개의 댓글

comment-user-thumbnail
2022년 10월 26일

채고에요!

답글 달기