개인 프로젝트를 진행하며 쉽고 편리하게 데이터를 저장할 수 있어서 많이 써왔었는데요

문득 궁금해졌습니다

사용하면서 이게 어디에 저장되는 것이고, 어떤 형식으로 저장되었는지

혹시 내가 코딩하면서 키가 잘못쓰여서 사용되지 않는 데이터가 있는지 많은 생각이 들었는데요

궁금증을 해결하고자 검색하고 앱 디렉토리를 탐색하며 얻은 정보를 바탕으로 작성하였습니다

 

UserDefaults


UserDefaults는 런타임동안 Key-Value 값 쌍을 영구적으로 저장하는 기본 데이터베이스에 대한 인터페이스입니다.

공식문서에 따르면 기본값을 설정하면 동기적으로 변경되고, 저장소에서는 비동기적으로 변경된다고 합니다.

Key만 정해준다면 다양한 타입의 데이터를 저장할 수 있습니다.

영구적인 저장이기 때문에 앱을 종료해도 데이터가 남아있습니다.

 

 

 

저장장소


UserDefaults를 이용하여 저장한 데이터는 xml형식의 Property List기반인 plist 파일로 저장되어 있습니다.

저장위치는 Sandbox내부의 Data Container에 저장됩니다.

갑자기 튀어나온 Sandbox는 나중에 자세하게 글로 남기겠습니다.

쉽게 앱이 외부의 데이터와 접근 및 허용을 하지 않게 가둔 틀이라고 생각하시면 됩니다.

 

저는 두 눈으로 plist로 저장된 데이터를 직접 보고 싶어서 디렉토리를 찾아보았습니다.

 

1. 장치에서 실행한 앱의 데이터를 보기

Xcode에서 아이폰에서 직접 앱을 돌려보기 위해 저장했던 장치입니다.

중간에 INSTALLED APPS에 설치된 앱의 목록이 뜨는데요.

Download Container버튼을 통해 다운받아주시면 됩니다!

다운받은 파일을 패키지 내용 보기를 누르시면 접근하실 수 있습니다.

AppData\Library\Preferences\앱번들ID.plist

다크모드 기능 여부에 대한 key-value값이 저장되어 있네요

 

2. 시뮬레이터에서 실행한 앱의 데이터를 보기

시뮬레이터에서 실행한 앱의 데이터를 보기 위해서는 직접 디렉터리를 찾아가도 좋지만 명령어를 통해 더 빠르게 접근할 수 있습니다.

저는 시뮬레이터로 많은 앱을 돌려보니 정말 많은 앱 폴더가 생겼는데요.. 이 명령어로 쉽게 찾을 수 있었습니다.

 

※ 명령어 사용 전 시뮬레이터를 켜주세요

※ app.identifier 대신에 앱 번들 Identifier을 넣어주세요

$ xcrun simctl get_app_container booted app.identifier data

위 명령어를 쓰시면 해당 앱의 위치가 출력됩니다

$ cd(디렉터리 폴더)
$ open .

open . 을 쓰시면 파인더가 바로 열리고 Library\Preferences\앱번들ID.plist를 확인하실 수 있습니다.

 

 

데이터 저장


데이터를 저장할 땐 set메서드를 사용하면 됩니다.

Array의 append와 같이 기존의 값에서 추가하는 방식이 아닌 덮어 쓰이는 방식으로 만약 같은 키값에서 밸류의 값이 추가가 된다면 

set메서드를 사용하여 덮어쓰면 됩니다!

// 문자열
UserDefaults.standard.setValue("String", forKey: "String")
// Bool
UserDefaults.standard.setValue(true, forKey: "Bool")
// 배열
UserDefaults.standard.setValue([1, 2, 3, 4, 5], forKey: "IntegerArray")

 

구조체

// 구조체는 인코딩하여 저장하고 디코딩하여 가져오므로 Codable을 채택한다.
struct Person: Codable {
    let name: String
    let age: Int
    let job: String
}

let person = Person(name: "cheolsu", age: 20, job: "student")    
let encoder = JSONEncoder()
do {
    let encoded = try encoder.encode(person)
    UserDefaults.standard.setValue(encoded, forKey: "struct")
} catch {
    print("Error")
}

 

데이터 저장된 모습

 

데이터 불러오기


데이터를 불러오는 방법은 해당 타입에 맞게 캐스팅해 주거나, 해당타입의 메서드로 불러오면 됩니다.

// 문자열
UserDefaults.standard.string(forKey: "String")
// Bool
UserDefaults.standard.bool(forKey: "Bool")
// 배열
UserDefaults.standard.array(forKey: "IntegerArray")


// 구조체
let decoder = JSONDecoder()
guard let data = UserDefaults.standard.object(forKey: "struct") as? Data else { return }
do {
    let decoded = try decoder.decode(Person.self, from: data)
    print(decoded)
} catch {
    print("Error")
}

 

 

 

 

데이터 제거하기


// key로 저장된 데이터 제거
UserDefaults.standard.removeObject(forKey: "key")

 

 

 

 

 

저장된 데이터 확인하기


직접 디렉터리를 접근하여 찾는 방법도 있지만 모든 저장된 UserDefaults 데이터를 불러오는 메서드가 있습니다.

// UserDefaults.standard.dictionaryRepresentation()

for (key, value) in UserDefaults.standard.dictionaryRepresentation() {
    print("key: \(key), value: \(value)")
}

/* 출력
 key: struct, value: {length = 43, bytes = 0x7b226a6f 62223a22 73747564 656e7422 ... 61676522 3a32307d }
 key: String, value: String
 key: IntegerArray, value: (
     1,
     2,
     3,
     4,
     5
 )
 key: Bool, value: 1
 */

(위 메서드 사용 하면 UserDefaults에 저장된 시스템 기본설정값도 출력됩니다.)

 

 

 

열거형을 사용하여 키 분류하기


UserDefaults는 plist파일로 저장되기 때문에 보안이 강력하다고 하기는 어렵고, 데이터를 쉽게 저장하고 불러올 수 있지만 저장공간을 많이 차지하기 때문에 대용량 데이터를 저장하기에는 적합하지 않다고 생각합니다.

그래서 비교적 가벼운 데이터를 저장하는데요 (예로 자동로그인 여부, 다크모드 설정여부 등등)

그래도 많은 Key-Value타입의 데이터를 저장하면서 느꼈던 경험이 있습니다.

바로 키를 잘못입력해서 생성되는 불필요한 데이터가 생성되는 점과 호출할 때 키값이 뭐였더라.. 하는 점입니다.

코드를 짜고 여러 군데에서 호출하여 데이터를 수정하다 보면 쉽게 발생할 수 있을 거라 생각합니다.

한두 개야 뭐 앱 성능에 영향 주겠어? 싶지만 좋은 습관은 아닌 거 같아서 열거형으로 키값을 분류하는 방법을 적어놓고 사용하고자 합니다!

// 키 열거형
enum UserDefaultsKey: String {
    case string
    case bool
    case array
    case object
}

// 데이터 입력
// 문자열
UserDefaults.standard.setValue("String", forKey: UserDefaultsKey.string.rawValue)
// Bool
UserDefaults.standard.setValue(true, forKey: UserDefaultsKey.bool.rawValue)
// 배열
UserDefaults.standard.setValue([1, 2, 3, 4, 5], forKey: UserDefaultsKey.array.rawValue)

// 구조체
let person = Person(name: "cheolsu", age: 20, job: "student")
let encoder = JSONEncoder()

do {
    let encoded = try encoder.encode(person)
    UserDefaults.standard.set(encoded, forKey: UserDefaultsKey.object.rawValue)
} catch {
    print("Error")
}

// 데이터 가져오기
// 문자열
UserDefaults.standard.string(forKey: UserDefaultsKey.string.rawValue)
// Bool
UserDefaults.standard.bool(forKey: UserDefaultsKey.bool.rawValue)
// 배열
UserDefaults.standard.array(forKey: UserDefaultsKey.array.rawValue)

// 구조체
let decoder = JSONDecoder()
guard let data = UserDefaults.standard.object(forKey: UserDefaultsKey.object.rawValue) as? Data else { return }
do {
    let decoded = try decoder.decode(Person.self, from: data)
    print(decoded)
} catch {
    print("Error")
}

 

 

+ Recent posts