
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하나로 다 적용할 수 있을 것이다.