UserDefaultsManager 개선하기

Heedon Ham·2023년 8월 25일
0

iOS 이것 저것

목록 보기
15/17
post-thumbnail

Previously On...

Sesac Recap Improvements: UserDefaults

지난 글에서는 DataManager에서 코드를 분리해 UserDefaults 연관 코드만 담당하는 Manager를 만들기로 했다. DataManagerUserDefaults에서 가져오는 data만 갖다 쓰거나,UserDefault에게 저장하도록 data를 넘겨주는 작업만 한다.


강조되고 반복되는 코드는 개발자를 불안하게 해요

UserDefaultsManage가 하는 일을 잠깐 생각해본다면 결국 dataManager에게서 받아온 data를 local storage에 저장하거나 local storage에 저장된 data를 불어와서 dataManager로 전달하는 작업임을 알 수 있다.
이 말은 결국 save & load할 data의 타입은 다르지만 작업은 동일한, 즉 구조가 비슷한 코드가 반복됨을 의미한다.

저번 글에서는 단순 Strin type인 data를 활용했지만, 이번에는 Struct 예시를 생각해보자.

mlbGameImage
(출처: Google Play Store)

모바일 스포츠 게임의 경우, 회원 계정이 있다면 삭제 후 재설치 혹은 오랜 기간 접속하지 않다가 접속한 경우, 구매/획득한 선수 카드가 마지막 접속 시의 상태로 남아있는 경우가 대부분이다.

enum PlayerType {
	case pitcher
    case hitter
}

enum CardRarity {
	case diamond
	case platinum
    case gold
    case silver
    case bronze
    case normal
}

enum SeasonLevel {
	case basic
    case singleA
    case doubleA
    case tripleA
    case major
    case allStar
    case worldClass
}

struct User: Codable {
	let uid: String
	let id: String
    var nickname: String
    var seasonLevel: SeasonLevel
    var gamePoint: Int
    var cardList: [PlayerCard]
    //...추가 유저 데이터...
}

struct PlayerCard: Codable {
	let name: String
    let type: PlayerType
    let rarity: CardRarity
    var overall: Int
	//...추가 카드 데이터...
}

따라서 우리는 회원 계정이 없는 경우는 UserDefaults를 비롯해 local storage로 해당 device의 정보를 관리하며, 앱을 삭제하는 경우 UserDefaults에 저장된 모든 정보도 같이 삭제됨을 알 수 있다.

이 경우, UserDefaultsManager에서는 다음과 같이 코드가 반복됨을 확인할 수 있다.

class UserDefaultsManager {
	
    private let userDefault = UserDefaults.standard
    
    //UserDefaults Key
    enum UserDefaultsKey: String, CaseIterable {
    	case isLoggedIn
        case userData
        case currentGetPlayerCard
    }
    
    var user: User? {
    	get {
        	if let retrievedData = userDefault.object(forKey: UserDefaultsKey.userData.rawValue) as? Data {
           		let decoder = JSONDecoder()
            	if let data = try? decoder.decode(User.self, from: retrievedData) {
                	return data
            	}
        	}
        	return nil
        } set {
        	let encoder = JSONEncoder()
        	if let encoded = try? encoder.encode(newValue) {
            	userDefault.setValue(encoded, forKey: UserDefaultsKey.userData.rawValue)
        	}
        }
    }
    
    var currentGetPlayer: PlayerCard? {
    	get {
        	if let retrievedData = userDefault.object(forKey: UserDefaultsKey.currentGetPlayerCard.rawValue) as? Data {
           		let decoder = JSONDecoder()
            	if let data = try? decoder.decode(PlayerCard.self, from: retrievedData) {
                	return data
            	}
        	}
        	return nil
        } set {
        	let encoder = JSONEncoder()
        	if let encoded = try? encoder.encode(newValue) {
            	userDefault.setValue(encoded, forKey: UserDefaultsKey.currentGetPlayerCard.rawValue)
        	}
        }
    }
}

save & load의 동작이 구조적으로 똑같아서 반복됨을 확인할 수 있다.
그나마 간단하게 표현해서 user와 가장 최근 획득한 카드 데이터만 가지고 활용한 것이지만 실제 서비스에서는 이 외에도 다양한 데이터를 저장하고 불러오는 작업을 수 없이 많이 수행할 것이다.
다루는 data가 유저 당 수십개만 넘어가도 수십 번 get과 set 코드 구조를 CTRL+C, CTRL+V를 해야한다.


타입에 유연하게 대응하기

이를 해결하기 위해 Generic을 활용해보자.
우리가 다룰 data들은 UserDefaults에 활용할 Struct 데이터이므로 Codable protocol을 채택한다.
따라서 타입의 일반화를 나타내기 위한 type parameter인 T도 Codable을 채택하고 있음을 나타내야 한다.

class UserDefaultsManager {
	private let userDefault = UserDefaults.standard
    
    //UserDefaults Key
    enum UserDefaultsKey: String, CaseIterable {
    	case isLoggedIn
        case userData
        case currentGetPlayerCard
    }
    
    //Generic Method
   	
    //GET
    func retrieveDataFromUserDefaults<T: Codable>(forKey: String) -> T? {
        if let retrievedData = userDefault.object(forKey: forKey) as? Data {
            let decoder = JSONDecoder()
            if let data = try? decoder.decode(T.self, from: retrievedData) {
                return data
            }
        }
        return nil
    }
    
    //LOAD
    func saveDataToUserDefaults<T: Codable>(newValue: T, forKey: String) {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(newValue) {
            userDefault.setValue(encoded, forKey: forKey)
        }
    }
    
    var user: User? {
    	get { retrieveDataFromUserDefault(forKey: UserDefaultKey.userData.rawValue) } 
        set { saveDataToUserDefault(newValue: newValue, forKey: UserDefaultKey.userData.rawValue) }
    }
    
    var currentGetPlayer: PlayerCard? {
    	get { retrieveDataFromUserDefault(forKey: UserDefaultKey.currentGetPlayerCard.rawValue) } 
        set { saveDataToUserDefault(newValue: newValue, forKey: UserDefaultKey.currentGetPlayerCard.rawValue) }
    }
    
}

연산 property의 get, set 부분이 정말 깔끔하게 정리됨을 알 수 있다.


UI component들의 반복되는 코드는 Custom Class를 생성해서 init 호출 시 같이 attribute가 설정되도록 하는 Design System을 구성하고, 화면 전환과 같은 메서드 반복 작업은 Generic으로 구성하면 동일 작동 함수 수십 개를 Generic으로 만든 blueprint하나로 다 적용할 수 있을 것이다.

profile
dev( iOS, React)

0개의 댓글