Sesac Recap Improvements: UserDefaults
지난 글에서는 DataManager
에서 코드를 분리해 UserDefaults
연관 코드만 담당하는 Manager를 만들기로 했다. DataManager
는 UserDefaults
에서 가져오는 data만 갖다 쓰거나,UserDefault
에게 저장하도록 data를 넘겨주는 작업만 한다.
UserDefaultsManage
가 하는 일을 잠깐 생각해본다면 결국 dataManager에게서 받아온 data를 local storage에 저장하거나 local storage에 저장된 data를 불어와서 dataManager로 전달하는 작업임을 알 수 있다.
이 말은 결국 save & load할 data의 타입은 다르지만 작업은 동일한, 즉 구조가 비슷한 코드가 반복됨을 의미한다.
저번 글에서는 단순 Strin
type인 data를 활용했지만, 이번에는 Struct
예시를 생각해보자.
(출처: 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하나로 다 적용할 수 있을 것이다.