iOS 앱에서 데이터를 로컬에 저장하는 방법은 다양하다.
오늘은 그중에서 UserDefaults에 대해서 알아보고, 간단한 사용방법과 함께 구조체를 UserDefaults에 저장하는 방법까지 알아보겠다.
UserDefaults란?
앱을 사용할 때 서버를 사용하지 않는 이상 데이터를 로컬 즉, iOS 디바이스에 저장해야 한다. 데이터를 로컬에 저장하는 방식에는 UserDefaults, CoreData, SwiftData 등 다양한 방법이 있다.
그 중 UserDefaults는 런타임에 메모리에 데이터를 저장하기 때문에 사용자의 기본 설정(닉네임) 등의 단일 데이터 값을 저장하기에 적합하다. 반면 CoreData등은 관계형 데이터베이스를 이용해 대량의 데이터를 저장하기에 적합하다.
예를 들어 일기앱의 경우 사용자가 일기를 쓰면 그 데이터는 CoreData에 저장되고, 일기를 아무리 많이 써봤자 사용자의 닉네임은 변하지 않는데, 이는 UserDefaults에 저장된다고 보면 된다.
UserDefaults의 데이터 저장방식
UserDefaults는 iOS Sandbox System이라는 방식으로 데이터를 저장하게 된다.
Sandbox System은 쉽게 말해서 앱마다 별도의 공간을 가지고 있고, 데이터에 대한 접근은 그 공간 안에서만 가능한 개념이다.
그림을 먼저 보자.
왼쪽에 큰 원이 디바이스의 저장공간이고, 각 앱마다 하나의 공간을 가지게 된다. 각 공간의 위치는 변동될 수 있다.
하나의 공간을 쉽게 '사물함'이라고 표현해보면 그 안에는 또 4개의 공간으로 구분되어있다.
그중에 UserDefaults는 Library에 저장되게 된다.
UserDefaults는 Key-Value 형태로 데이터를 저장하는데, Library 안에 "number"라는 이름의 주머니가 있고, 그 안에 실제 데이터가 저장된다고 생각하면 된다. 즉, 주머니 이름이 Key이고, 그 안의 데이터가 Value인 것이다.
UserDefaults 사용법
- 데이터 저장
코드는 UserDefaults의 내 공간의 위치 확인 -> 주머니 만들기 -> 데이터 저장의 순서라고 생각하면 쉽다.
예를 들어서 어떤 버튼을 눌렀을 때, textField에 적힌 text를 저장하는 상황이라면 다음과 같이 작성할 수 있다.
@IBAction func saveButtonTapped(_ sender: UIButton) {
// 텍스트필드에 적었던 내용을 영구적으로 저장.
UserDefaults.standard.set(numberTextField.text, forKey: "number")
print(#function)
}
여기서 `UserDefaults.standard`까지가 내 공간의 위치를 확인하는 부분이고
`.set`은 "데이터를 저장하겠다" 라는 의미이다.
`numberTextField.text`는 저장할 데이터이고, `forKey: "number"`는 "number"라는 이름의 주머니를 만들어서 그 안에 넣어놓겠다는 의미이다.
- 데이터 불러오기
저장한 데이터를 불러올 때는 `UserDefaults.standard`여기까지는 저장하는 동작과 같고, 그 뒤에는 `set`대신에 불러올 데이터의 타입을 명시해주어야 한다.
즉, 저장한 데이터가 String타입이었다면 `.string(forKey: "number")`라고 적어줌으로써, "number"라는 주머니 안에 있는 String타입의 데이터를 불러온다는 의미를 전달한다.
override func viewDidLoad() {
super.viewDidLoad()
// UserDefaults에 저장된 데이터 불러오기
let result = UserDefaults.standard.string(forKey: "number")
numberTextField.text = result
}
이런식으로 불러온 데이터를 별도의 상수에 저장해서 사용하기도 한다.
단, UserDefaults에 저장할 수 있는 타입은 `NSData`, `NSString`, `NSNumber`, `NSDate`, `NSArray`, `NSDictionary`로 한정되며, 구조체와 같은 여기에 해당하지 않는 타입을 저장할 때는 아카이빙이라는 개념을 통해 'NSData' 타입으로 저장해야 한다.
Struct형태를 UserDefaults에 저장하기
Struct 즉, 구조체 형태는 UserDefaults에 저장할 수 있는 타입에 해당하지 않기 때문에 NSData타입으로 저장해야 한다. 이를 위해선 아카이빙이라는 작업이 별도로 필요하다.
사실 아카이빙은 String 등 UserDefaults가 지원하는 타입을 저장할 때도 내부적으로 동작하게 된다. 그러나 지원하지 않는 타입인 Struct를 저장할 때는 직접 아카이빙 작업을 해주어야 하는 것이다.
처음에는 이러한 개념을 모르고 다음과 같이 저장하려고 했다.
struct Checklist {
var title: String?
var bookmark: Bool = false
var complete: Bool = false
}
class ChecklistTableViewController: UITableViewController {
var checklistItems: [Checklist] = [] {
didSet {
tableView.reloadData()
UserDefaults.standard.set(checklistItems, forKey: "checklistItems")
}
}
// ...
}
구조체 타입의 객체들이 담긴 배열이었기 때문에 배열 형태로 저장해서 불러오면 된다고 생각했다.
그런데 저장하는 순간 다음과 같은 오류가 뜨면서 앱이 터졌다.
이는 구조체 형태를 아카이빙 작업 없이 저장하려고 해서 발생한 문제이다.
다음과 같은 형태로 저장해야 올바르게 UserDefaults에 구조체를 저장할 수 있다.
struct Checklist: Codable {
var title: String?
var bookmark: Bool = false
var complete: Bool = false
}
먼저 아카이빙, 언아카이빙 작업을 위해 해당 구조체는 Codable 프로토콜을 준수해야 한다.
즉, encoding과 decoding이 가능한 형태로 만들어 주어야 한다.
다음으로 `JSONEncoder()`를 이용해 구조체 데이터를 NSData로 변환해 UserDefaults에 저장한다.
var checklistItems: [Checklist] = [] {
didSet {
let encoder = JSONEncoder()
if let encodedData = try? encoder.encode(checklistItems) {
UserDefaults.standard.set(encodedData, forKey: "checklistItems")
print("success")
}
}
}
Checklist타입의 배열이 변경될때마다(배열에 데이터가 추가될때마다) UserDefaults에 저장는 과정이다.
저장한 데이터를 불러올 때는 `JSON Decoder()`를 이용해 언아카빙하여 로드할 수 있다.
override func viewDidLoad() {
super.viewDidLoad()
if let savedData = UserDefaults.standard.object(forKey: "checklistItems") as? Data {
let decoder = JSONDecoder()
if let savedObject = try? decoder.decode([Checklist].self, from: savedData) {
checklistItems = savedObject
}
}
//...
}
이렇게 viewDidLoad()에서 저장된 데이터를 언아카이빙해서 위에서 선언해놓은 checklistItems라는 변수에 담아주면, 저장된 데이터를 확인할 수 있다.
※ 전체 소스코드
https://github.com/WooFeather/Assignment8
※ 참고자료 및 블로그
https://developer.apple.com/documentation/foundation/userdefaults
https://ios-development.tistory.com/702
'iOS > UIKit' 카테고리의 다른 글
[iOS/UIKit] 프로젝트 트러블슈팅 (0) | 2025.01.28 |
---|---|
[iOS/UIKit] @objc와 #selector - 커스텀뷰에 addTarget을 하면 안되는 이유 (0) | 2025.01.15 |
[iOS/UIKit] awakeFromNib과 prepareForReuse - 셀의 재사용 원리 (0) | 2025.01.08 |
[iOS / UIKit] Unwind - 맨 처음 뷰로 돌아가기 (0) | 2025.01.03 |
[iOS / UIKit] AppDelegate와 SceneDelegate - 앱의 생명주기 (0) | 2024.12.29 |