⎮ UserDefaults 매니저가 필요하게 된 이유
날씨 앱에서 유저가 입력한 검색 기록을 저장하고 있으면 UX적으로 좋을 것 같다는 내부 논의가 있었다.
내부 논의를 토대로 필요한 데이터를 정리해보니 입력한 주소, 위도, 경도 데이터만 저장하면 구현이 가능할 것 같았다.
비교적 단순한 데이터이므로, UserDefaults를 사용하여 구현하기로 합의했다.
struct UserLocationData: Codable {
let address: String
let lat: String
let lon: String
}
위 데이터 타입은 struct로 JSON Decoding, Encoding의 데이터 변환 과정이 필요하다
Swift | 유저 디폴트(UserDefaults) 이해하기
유저 디폴트(UserDefaults) 전화번호부 앱을 만든다고 생각해보자 let이나 var로 선언한 배열 타입 안에 데이터를 append해서 저장할 수도 있겠지만, 이런 형태라면 앱을 껐다키면 앱을 실행하면서 저
uddt.tistory.com
만약 UserDefaults Manager가 없다면, UserDefaults에 저장된 데이터를 사용하는 곳마다
load, save하는 함수를 계속 써줘야 한다.
// 1번 VC에서 작성
func loadKickBoardsFromUserDefaults() -> [KickBoard]? {
guard let data = UserDefaults.standard.data(forKey: "kickBoardHistory") else { return nil }
print(data)
return try? JSONDecoder().decode([KickBoard].self, from: data)
}
// 2번 VC에서 작성
func loadCurrentUser() -> User? {
guard let currentEmail = UserDefaults.standard.string(forKey: "currentUserEmail"),
let data = UserDefaults.standard.data(forKey: "savedUsers"),
let users = try? JSONDecoder().decode([User].self, from: data) else {
return nil
}
return users.first(where: { $0.email == currentEmail })
}
// 3번 VC에서도 작성
if let savedData = defaults.data(forKey: "kickBoardHistory"),
let decoded = try? JSONDecoder().decode([KickBoard].self, from: savedData) {
savedKickboardData = decoded
// 중복 이름 체크
if savedKickboardData.contains(where: { $0.name == name }) {
showAlert(title: "중복 경고", message: "이미 등록된 킥보드 입니다")
return true
}
}
이렇게 하면 decode하는 플로우는 똑같은데 계속 반복적으로 코드를 작성하게 된다
이를 방지하기 위해 UserDefaults 매니저를 구현하여 재사용성을 높여보고자 한다.
⎮ UserDefaults Manager 구현하기
먼저 UserDefaultsManager를 여러 곳에서 사용할 수 있도록 싱글톤 패턴을 적용해서 작성하고,
메모리 누수를 방지하기 위해 init을 막아주었다
final class UserDefaultsManager {
static let shared = UserDefaultsManager()
private let defaults = UserDefaults.standard
private init() {}
}
필요한 데이터는 2가지 데이터다.
1. 현재 위치가 저장된 유저의 데이터
2. 기존 검색 기록이 저장되어 있는 유저의 데이터
UserDefaults는 Key-Value Type의 쌍으로 데이터를 다루는데,
Key를 하드코딩하게 되면 휴먼 에러의 위험이 있으므로 enum으로 userDefaultsKey를 분기해줬다
extension UserDefaultsManager {
enum UserDefaultsKey: String {
case currentLocation
case locationHistory
}
}
이제 데이터의 CRUD를 작성하면 된다
재사용이 가능하도록 Generic Type으로 작성해주면 된다.
- Create (데이터 저장)
/**
key를 입력하고 value를 저장할 수 있는 메서드입니다
- key: currentLocation(현재 위치), locationHistory(위치 전체 기록)
- value: 제네릭 Type이므로 저장하는 타입에 따라 LocationHistory(history: [value]) 등으로 타입을 맞춰줘야 합니다.
*/
func saveData<T: Encodable>(key: userDefaultsKey, value: T) {
if let encodedData = try?JSONEncoder().encode(value) {
defaults.set(encodedData, forKey: key.rawValue)
}
}
데이터를 저장할 때는 Key를 가지고 Value를 저장해야하기 때문에,
파라미터로 key와 value를 주입할 수 있게 작성했다.
- Read
/**
key를 입력하고 value에 접근할 수 있는 메서드입니다
- key: currentLocation(현재 위치), locationHistory(위치 전체 기록)
*/
func getData<T: Decodable>(with key: userDefaultsKey) -> T? {
if let data = defaults.data(forKey: key.rawValue) {
if let decodedData = try?JSONDecoder().decode(T.self, from: data) {
return decodedData
}
}
return nil
}
데이터를 불러올 때는 Key를 가지고 Value에 접근할 수 있기 때문에
파라미터에 Key만 입력해주고 Key에 해당하는 Value를 return하도록 작성했다
- Update
/**
위치 데이터를 추가할 수 있는 메서드입니다.
- 데이터는 배열형태로 저장되며, locationHistory key에 접근하여 확인할 수 있습니다.
*/
func updateLocationHistory(with locationData: UserLocationData) {
var userHistory = getData(with: .locationHistory) ?? LocationHistory(history: [])
userHistory.history.insert(locationData, at: 0)
saveData(key: .locationHistory, value: userHistory)
}
Update도 Create와 마찬가지로 저장할 때는 Key를 가지고 Value를 저장해야하기 때문에,
파라미터로 key와 value를 주입할 수 있게 작성했다.
입력된 데이터를 최신순으로 정렬하기 위해 append가 아닌 insert(_, at: 0)를 사용했다
우리 앱에서 현재 위치를 불러와서 업데이트하는 기능은 없기 때문에,
Update는 Key를 고정해서 사용하도록 구현했다.
- Delete
/**
인덱스에 해당하는 데이터를 삭제할 수 있는 메서드입니다.
- 추후 테이블뷰에 적용할 때 선택한 indexPath.row의 값을 주입하여 삭제할 수 있습니다.
*/
func removeData(index: Int) {
var userHistory = getData(with: .locationHistory) ?? LocationHistory(history: [])
userHistory.history.remove(at: index)
saveData(key: .locationHistory, value: userHistory)
}
/**
데이터를 삭제할 수 있는 메서드입니다.
- 데이터는 배열형태로 저장되며, locationHistory key에 접근하여 확인할 수 있습니다.
- 전체 삭제 구현 시에 사용할 수 있습니다.
*/
func removeData(with key: userDefaultsKey) {
defaults.removeObject(forKey: key.rawValue)
}
삭제는 2가지 방법으로 구현했다.
Key의 Index 접근해서 데이터를 삭제하거나, Key에 접근해서 모든 데이터를 삭제하는 방법으로 구현했다.
'스파르타코딩 클럽 > 팀프로젝트' 카테고리의 다른 글
팀프로젝트4 [식당 큐레이팅 앱(2) - Data Flow 작성, Google API] (0) | 2025.06.10 |
---|---|
팀프로젝트4 [식당 큐레이팅 앱(1) - 앱 기획] (0) | 2025.06.08 |
팀프로젝트3 [날씨 앱 만들기(2) - Moya 라이브러리 사용기] (0) | 2025.05.25 |
팀프로젝트3 [날씨 앱 만들기(1) - 앱 기획] (0) | 2025.05.24 |
팀프로젝트2 [공유 킥보드 앱 만들기(4) - 네이버 커스텀 마커, 버튼 이벤트] (0) | 2025.05.01 |