기존의 UserDefaults 사용법을 보면 key와 type을 제외하고 get{}, set{} 부분이 아래와 같이 중복되어서 사용되고 있었습니다.
UserDefaults.standard.object(forKey: key.rawValue)
UserDefaults.standard.set(object, forKey: key.rawValue)
Swift 5.1에서 property wrapper가 새로 도입되면서 이렇게 반복되는 로직들을 프로퍼티 자체에 연결할 수 있게 되었습니다. 이번 포스팅에서는 @propertyWrapper 속성을 사용하여 UserDefaults 작업을 간소화하는 방법을 살펴보겠습니다. 이를 통해 코드를 더 깔끔하고 효율적으로 유지 관리하기 쉽게 만들 수 있습니다.
유저 디폴트는 키-값 저장소로 가벼운 정보와 같은 작은 양의 데이터를 저장하는 데 사용됩니다. 사용이 간편하고 빠른 설정 때문에 종종 간단한 데이터 저장에 사용됩니다.
// UserDefaults에 값 저장
UserDefaults.standard.set(true, forKey: "isDarkModeEnabled")
// UserDefaults에서 값 꺼내기
let isDarkModeEnabled = UserDefaults.standard.bool(forKey: "isDarkModeEnabled")
@propertyWrapper는 저장에 대한 사용자 정의 동작을 정의할 수 있게 해줍니다. 이 속성은 코드의 가독성과 유지 관리성을 크게 향상시킬 수 있습니다.
@propertyWrapper를 사용하면 데이터를 UserDefaults에 저장하고 검색하는 과정을 간소화(캡슐화)하여 보일러플레이트 코드와 오류 발생 가능성을 줄일 수 있습니다.
먼저 'UserDefault'라는 구조체에 @propertyWrapper를 정의.
저는 Codable 유형과 함께 사용해야했기 때문에 타입 인자는 제네릭인 <T: Codable>를 사용했습니다.
초기화를 위한 init 값 지정 (초기값은 각자 케이스에 맞게 설정하면 됨.)
- 'key': 값을 유저디폴트에 저장할때 사용할 키
- 'defaultValue': 키에 해당하는 값이 유저디폴트에 없을 경우 사용할 기본 값
- 'needEncrypt': 암호화가 필요한지 flag 값
- 'isCustomObject': 코더블 커스텀 object를 사용할 것인지 flag 값
wrappedValue 정의
게터 (getter)
유저디폴트에서 값을 검색하는 역할을 담당합니다. UserDefaults.standard.object(forKey:)를 사용하여 주어진 키에 저장된 오브젝트를 검색합니다. 검색된 오브젝트를 T 유형으로 변환하는데 실패하면, defaultValue를 반환합니다.
세터 (setter)
유저디폴트에 값을 저장하는 역할을 담당합니다.
UserDefaults.standard.set(newValue, forKey:)를 사용하여 지정된 키 아래에 제공된 newValue를 저장합니다.
@propertyWrapper
struct UserDefault<T: Codable> {
let key: UserDefaultKey
let defaultValue: T?
let needEncrypt: Bool
let isCustomObject: Bool
let storage: UserDefaults = UserDefaults.standard
init(key: UserDefaultKey,
defaultValue: T? = nil,
needEncrypt: Bool = false,
isCustomObject: Bool = false) {
self.key = key
self.defaultValue = defaultValue
self.needEncrypt = needEncrypt
self.isCustomObject = isCustomObject
}
var wrappedValue: T? {
// Read value from UserDefaults
get {
if needEncrypt {
// 암호화 필요할때
return self.storage.secretObject(forKey: self.key.rawValue) as? T ?? self.defaultValue
} else if isCustomObject {
// 커스텀 오브젝트일때
guard let data = self.storage.object(forKey: key.rawValue) as? Data else { return defaultValue }
let session = try? JSONDecoder().decode(T.self, from: data)
return session ?? defaultValue
} else {
// 기본 hashable 오브젝트일때 (String, Bool, etc)
return self.storage.object(forKey: self.key.rawValue) as? T ?? self.defaultValue
}
}
// Set value to UserDefaults
set {
if needEncrypt {
self.storage.setSecretObject(newValue, forKey: self.key.rawValue)
} else if isCustomObject {
let data = try? JSONEncoder().encode(newValue)
self.storage.set(data, forKey: self.key.rawValue)
} else {
self.storage.set(newValue, forKey: self.key.rawValue)
}
self.storage.synchronize()
}
}
}
위처럼 선언을 해준 후에는 다른 클래스나 구조체를 만든 후 속성을 정의하고 @UserDefault property wrapper로 주석을 달아줍니다.
class UserDefaultManager {
@UserDefault(key: .loginId, needEncrypt: true)
static var loginId: String?
@UserDefault(key: .showMainOnboarding, defaultValue: true)
static var showMainOnboarding: Bool!
@UserDefault(key: .counselWriteTemporary, isCustomObject: true)
static var counselWriteTemporary: WriteHolder?
}
if UserDefaultManager.showMainOnboarding {
UserDefaultManager.showMainOnboarding = false
let onboardingVC = OnboardingViewController(type: .main)
onboardingVC.modalPresentationStyle = .fullScreen
present(onboardingVC, animated: false)
}
기존과는 다르게 프로퍼티에 값을 대입하기만 하면 값을 저장할 수 있게 되었습니다. 값을 가져오는 것도 훨씬 간편해졌죠!
로직을 캡슐화해서 보일러 플레이트 코드를 줄이고 가독성을 향상시키며 앱 전체에서 일관성을 가질 수 있습니다. 기본값 뿐만이 아니라 Codable 유형을 저장해야 할 때도 훨씬 더 편리하게 사용할 수 있습니다.