@propertyWrapper와 UserDefaults를 활용한 데이터 저장 방법

Doyeong Kim·2023년 8월 14일
0

Swift

목록 보기
8/9

기존의 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에 값 저장
UserDefaults.standard.set(true, forKey: "isDarkModeEnabled")

// UserDefaults에서 값 꺼내기
let isDarkModeEnabled = UserDefaults.standard.bool(forKey: "isDarkModeEnabled")

@propertyWrapper 소개:

@propertyWrapper는 저장에 대한 사용자 정의 동작을 정의할 수 있게 해줍니다. 이 속성은 코드의 가독성과 유지 관리성을 크게 향상시킬 수 있습니다.

@propertyWrapper를 사용하면 데이터를 UserDefaults에 저장하고 검색하는 과정을 간소화(캡슐화)하여 보일러플레이트 코드와 오류 발생 가능성을 줄일 수 있습니다.


😃 정의

  1. 먼저 'UserDefault'라는 구조체에 @propertyWrapper를 정의.
    저는 Codable 유형과 함께 사용해야했기 때문에 타입 인자는 제네릭인 <T: Codable>를 사용했습니다.

  2. 초기화를 위한 init 값 지정 (초기값은 각자 케이스에 맞게 설정하면 됨.)

    • 'key': 값을 유저디폴트에 저장할때 사용할 키
    • 'defaultValue': 키에 해당하는 값이 유저디폴트에 없을 경우 사용할 기본 값
    • 'needEncrypt': 암호화가 필요한지 flag 값
    • 'isCustomObject': 코더블 커스텀 object를 사용할 것인지 flag 값
  3. 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 유형을 저장해야 할 때도 훨씬 더 편리하게 사용할 수 있습니다.

profile
신비로운 iOS 세계로 당신을 초대합니다.

0개의 댓글