iOS/iOS 지식

[Swift] UserDefaults + PropertyWrapper

홍복치 2024. 5. 26. 21:44

앱의 간단한 데이터들을 저장하기 위해 UserDefaults에 대해 알아보고자 함. 

정의를 살펴보면 앱이 실행전반에 걸쳐 영구적으로 key-value를 저장하는 데이터베이스라고 함. 

개요를 보면 사용자가 앱을 사용할 때의 기본적인 설정(미디어 재생속도) 등을 저장해서 작동방식을 결정함

기본적인 객체 저장을 지원하는데  float, double, Int, Bool, URL 같은 것들을 key-value로 저장할 수 있음

UserDefaults.standard.set("홍복치", forKey: "name")

이런식으로 기본적인 값들을 저장할 수 있다는 것임

 

property list 형태인 NSData, NSString, NSNumber, NSDate, NSArray, NSDictionary 같은 것들도 저장할 수 있음.

만약에 그 외의 객체를 저장하려면 NSData의 인스턴스로 만들어서 보관해야 함 

여기서 초보자의 시선으로 Dictionary도 저장할 수 있네?라고 생각했는데 타입이 정해져 있음

NSDictionary 저장할 수 있는데, ["age": 28] 이런식으로 키가 string이어야 저장이 가능하다고 함

실제로 Dictionary를 [28: "age"] 이런식으로 넣으면 앱이 터지는 것을 경험할 수 있음

NSDictionary and CFDictionary objects allow their keys to be objects of any type, 
if the keys are not string objects, the collections are not property-list objects.
property lists should be used for data that consists primarily of strings and numbers.

 

이제 propertyWrapper랑 같이 사용해보도록 하겠음. propertyWrapper는 swift5.1에 나옴

아래 사진은 WWDC에서 소개한 코드인데 통상적으로 class에 저런 코드들을 넣어놓고 쓰게 됨

코드를 보면 계속 get, set코드가 반복되면서 타입이랑 키만 변경되고 있음

UserDefaults에 저장할 데이터가 많아진다면 저 get, set을 계속해서 써주어야 함

그러면 propertyWrapper로 그 부분을 따로 정의를 해줄수가 있다는건데, 어떻게 하는건지 공식문서 예시를 봐보겠음

일단 propertyWrapper의 정의를 보면 프로퍼티가 저장되는 방식과 프로퍼티의 정의를 분리해준다고 함

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

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

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"

rectangle.height = 10
print(rectangle.height)
// Prints "10"

rectangle.height = 24
print(rectangle.height)
// Prints "12"

예시 코드를 보면 height건 width건 12넘는 값은 set이 안됨

왜냐면 TwelveOrLess의 wrappedValue에서 그렇게 저장되도록 지정해놨기 때문임

공식문서에서는 wrapper를 property에 사용하면, 컴파일러가 레퍼를 위한 저장소와 속성에 대한 접근을 제공하는 코드를 합성한다고 함. 그니까 저 height는 래퍼를 위한 저장소고, wrappedValue의 로직이 합쳐진다는 거구나라고 이해할 수 있음

 

그렇다면 UserDefaults에 저장하고, 가져오는 로직은 propertyWrapper의 wrappedValue에 정의해놓고 래퍼를 적용해서 걍 꺼내서 쓰면 되겠다 라는 걸 알게됨. 아래처럼 제네릭을 사용하면 다양한 타입에 대응해줄 수 있음

@propertyWrapper
struct UserDefaultsManager<T> {
    let defaultValue: T
    let key: String
    let storage: UserDefaults
    
    var wrappedValue: T {
        get{
            return self.storage.object(forKey: key) as? T ?? defaultValue
        }
        set{
            self.storage.set(newValue, forKey: key)
        }
    }
}

final class UserManager {
    private init(){}
    
    @UserDefaultsManager(
        defaultValue: false,
        key: "isUser",
        storage: .standard
    )
    static var isUser: Bool
  }
  
  //사용
UserManager.isUser = true

 

참고자료

https://developer.apple.com/documentation/foundation/userdefaults

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/properties

https://developer.apple.com/videos/play/wwdc2019/402/