[iOS] 앱에서 사용자 데이터를 저장하기 (feat. FileManager)

이정훈·2023년 2월 3일
0

iOS

목록 보기
1/3
post-thumbnail

해당 포스트에 사용된 전체 코드는 깃허브를 참고 하세요.

iOS 어플리케이션에서 내부 데이터 저장을 구현하지 않으면 새로운 데이터가 추가 되더라도 어플리케이션을 종료한다면 추가된 데이터는 모두 소멸 된다.

그렇게 해서 여러 자료를 찾아본 결과 어플리케이션의 데이터를 저장하는 방법은 여러가지가 있었는데

  1. UserDefaults
  2. FileManager
  3. CoreData
  4. Keychain

이 중 UserDefaults와 Keychain은 어플리케이션의 설정 값이나 사용자 비밀 번호 등 간단한 데이터를 저장하는데 쓰이는 것이라 배제하였고, File Manager와 Core Data 중 필자는 사용자 데이터를 독립된 인스턴스로 하는 간단한 데이터만 저장하면 되었기에 File Manager를 사용해 보기로 하였다.

Core Data는 더 복잡한 데이터들과 데이터 베이스의 고급 기능까지 다룰 수 있고 오히려 Core Data가 동적인 데이터를 저장하기에 적합할 수도 있겠다는 생각이 들어 다음에 다른 포스트를 통해 다시 한번 더 다루어 보려고 한다. 다만, 일장일단이 있을수 있겠다 생각이 드는게.. Core Data framework로 저장한 데이터는 iCloud를 통한 공유만 허용되는 부분도 있다.

일단 이번 포스트에서는 FileManager 사용법을 숙지하고 넘어가려고 한다.

🏖️ SandBox


먼저 FileManager를 알아보기 전에 어플리케이션이 iOS 기기에 설치될때 생성되는 디렉토리 구조를 살펴 보고자 한다.

iOS 기기에 앱이 설치되면 다음과 같은 SandBox 디렉토리 구조가 만들어진다.

각 디렉토리들의 역할은 다음과 같다.

  • Bundle Container: Xcode에서 작성한 앱의 실행 코드들이 저장
  • Data Container: 앱과 사용자의 데이터 저장
    • Documents: 사용자가 생성한 콘텐츠 데이터가 존재하는 디렉토리, 사용자가 직접 파일 생성, 삭제, 공유 가능. 또한 해당 디렉토리는 iCloud 서버에 백업 된다.
    • Library: 사용자 데이터 파일이 아닌 모든 파일의 최상위 디렉토리, 사용자에게 노출 되면 안되는 데이터들을 포함(캐시 데이터 등)
    • tmp: 어플리케이션 구동 중 생성되는 임시 파일들 존재, 어플리케이션이 종료되면 데이터가 계속 쌓이지 않도록 삭제 시켜줘야함(os에서도 앱이 종료 되어 있는동안 주기적으로 삭제)

iOS app은 이러한 SandBox 디렉토리 구조를 통해 SandBox 디렉토리 내부에서만 access 가능하며, 명시적으로 허가 되지 않은 resourse에 대한 access는 os에서 원천 봉쇄하여 차단한다.

📂 FileManager


위의 SandBox 구조를 보았을때 어플리케이션에서 생성된 사용자 데이터를 저장할 공간은 Documents 디렉토리이다.

이러한 디렉토리에 접근하여 데이터를 저장하는 방식을 가능하게 하는것이 FileManager 클래스이다.

먼저 필자는 데이터를 JSON 형식으로 저장하기 위해 다음과 같이 JSON 인코더, 디코더를 함수로 선언하였다.(해당 코드에 대한 설명은 여기를 참고)

func JSONEncoder<T: Codable>(data: T) -> String? {
    let encoder = JSONEncoder()
    
    do {
        let jsonData = try encoder.encode(data)
        return String(data: jsonData, encoding: .utf8)    //URL 타입의 데이터를 utf8 형식의 String 타입으로 인코딩
    } catch {
        print("JSON encode error")
    }
    
    return nil
}

func JSONDecoder(data: String) -> [Todo]? {
    let decoder = JSONDecoder()
    guard let jsonData = data.data(using: .utf8) else {
        return nil
    }
    
    do {
        let decoded: [Todo] = try decoder.decode([Todo].self, from: jsonData)
        return decoded
    } catch {
        print("JSON decode error")
    }
    
    return nil
}

Filemanager로 경로 가져오기

다음과 같이 Filemanager 클래스를 통해 데이터를 저장할 디렉토리 경로를 반환하는 함수를 정의한다.

func getDocumentPath() -> URL {    //데이터 저장을 위한 디렉토리 경로를 반환하는 함수
    return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}

FileManager 클래스 내부에 정의된 default 프로퍼티는 타입 프로퍼티로 FileManager의 싱글톤 인스턴스를 반환한다.

open class FileManager : NSObject {

    
    /* Returns the default singleton instance.
    */
    open class var `default`: FileManager { get }
    ...

아래는 Apple document에 정의 되어 있는 urls mothod의 정의이다.

urls(for:in:)

Returns an array of URLs for the specified common directory in the requested domains.

for parameter로 전달된 디렉토리 경로와 in parameter로 전달된 도메인 마스크를 통해 URL 타입의 디렉토리 path 배열을 반환한다.

SandBox 구조에서 보면 알 수 있듯, Documents 디렉토리는 단 하나 존재하므로 urls로 반환된 배열의 0번째 요소를 path로서 가져온다.

데이터 저장

아래는 사용자 데이터를 저장하기 위한 save 함수를 선언하였다.

func save<T: Codable>(data: T) {
    let path = getDocumentPath().appendingPathComponent("usr_data.json")    //저장 경로로 부터 파일 지정
    guard let jsonString = JSONEncoder(data: data) else {
        return
    }
    
    do {
        try jsonString.write(to: path, atomically: true, encoding: .utf8)
        //path 경로에 데이터를 저장하는데 atomically의 값이 true이면 보조 파일을 생성하고 에러가 발생하지 않으면 기존 파일 이름으로 대체
    } catch {
        print("save error")
    }
}

상수 path에는 getDocumentPath 함수로 반환된 디렉토리 경로에 usr_data.json이라는 파일을 추가한 경로를 가진다.

그리고 위에서 정의한 JSONEncoder 함수로 데이터를 인코딩하고 String 타입의 write method를 사용하여 해당 경로에 데이터를 저장한다. 이때 atomically parameter가 true라면 임시 파일을 생성하고 에러가 발생하지 않으면 해당 임시 파일을 기존 임시 파일 이름으로 대체하여 저장한다.

데이터 불러오기

이번에는 Documents에 저장된 데이터를 불러오는 방법이다.

func loadData() -> [Todo]? {
    let path = getDocumentPath().appendingPathComponent("usr_data.json")
    
    do {
        let jsonString = try String(contentsOf: path)    //URL 타입을 전달받아 String 타입 반환
        guard let jsonData = JSONDecoder(data: jsonString) else {
            print("(loaded Data)JSON decode error")
            return nil
        }
        return jsonData
    } catch {
        print("Data load error")
    }
    
    return nil
}

데이터를 불러오기 위한 loadData 함수를 정의 하였다.

마찬가지로 데이터가 존재하는 경로를 path 상수로 정의하였고, String(contentsOf:) initializer는 URL 타입의 전달인자를 받아 해당 경로의 데이터를 String 타입으로 반환한다.

나머지 디코딩 방식은 여기를 참고.

❕핵심

사용자 데이터를 저장하기 위해서는 그것이 image 파일이 되었든, 텍스트 파일이 되었든, JSON 파일이 되었든 간에 Filmanager 인스턴스를 통한 Documents 디렉토리에 존재하는 데이터 파일 경로를 잘 가져올 수 있어야 한다는게 핵심인거 같다.

데이터 경로만 잘 가져온다면 후처리는 각 파일 형식에 맞게 알아서 잘 처리하면 될듯..

참고

Apple Document
https://developer.apple.com/documentation/
Apple File System Programming Guide
https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#//apple_ref/doc/uid/TP40010672-CH2-SW2
Apple App Sandbox Design Guide
https://developer.apple.com/library/archive/documentation/Security/Conceptual/AppSandboxDesignGuide/AboutAppSandbox/AboutAppSandbox.html#//apple_ref/doc/uid/TP40011183-CH1-SW1

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글