[iOS] 데이터를 저장하는 방법들 / 간단 예제

2024. 12. 31. 07:42🍏/Swift

간단한 예제들과 함께 알아보는 iOS에서 데이터를 저장하는 방법


1. UserDefaults

NSUserDefaults에 저장되어 키-값 쌍으로 앱이 삭제되기 전까지 영구적으로 저장됩니다.
간단하고 사용이 쉽습니다만 대규모 데이터나 민감한 데이터에 적합하지 않습니다.

import Foundation

class UserDefaultsHelper {
    static let shared = UserDefaultsHelper()
    private let launchCountKey = "launchCount"
    
    private init() {}

    // 실행 횟수 읽기
    func getLaunchCount() -> Int {
        return UserDefaults.standard.integer(forKey: launchCountKey)
    }

    // 실행 횟수 업데이트
    func updateLaunchCount() {
        let currentCount = getLaunchCount()
        UserDefaults.standard.set(currentCount + 1, forKey: launchCountKey)
        print("App launched \(currentCount + 1) times")
    }
}

// 사용 예제
let userDefaultsHelper = UserDefaultsHelper.shared
userDefaultsHelper.updateLaunchCount()

 

2. Keychain

iOS보안 프레임워크 기반으로 높은 보안으로 저장됩니다. 데이터는 앱 삭제 후에도 유지가 가능합니다.
민감한 데이터 저장에 최적되어있지만 사용이 복잡하며, 키-값 쌍 저장밖에 하지 못합니다.

import Security

class KeychainHelper {
    static let shared = KeychainHelper()

    private init() {}

    // 데이터 저장
    func savePassword(_ password: String, for account: String) {
        let passwordData = password.data(using: .utf8)!
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecValueData as String: passwordData
        ]

        SecItemDelete(query as CFDictionary) // 기존 데이터 삭제
        let status = SecItemAdd(query as CFDictionary, nil)
        if status == errSecSuccess {
            print("Password saved successfully")
        } else {
            print("Failed to save password")
        }
    }

    // 데이터 읽기
    func getPassword(for account: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]

        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        if status == errSecSuccess, let data = item as? Data {
            return String(data: data, encoding: .utf8)
        }
        print("Failed to retrieve password")
        return nil
    }
}

// 사용 예제
let keychainHelper = KeychainHelper.shared
keychainHelper.savePassword("securePassword123", for: "userAccount")
if let password = keychainHelper.getPassword(for: "userAccount") {
    print("Retrieved password: \(password)")
}

 

3. Core Data

메모리와 디스크를 효율적으로 사용하며, 데이터 캐싱이 가능합니다.
데이터 모델링 및 관계형 데이터 관리에 적합하지만, 초기 설정 및 학습이 다소 복잡합니다.

import Foundation
import CoreData

class CoreDataHelper {
    static let shared = CoreDataHelper()
    private let persistentContainer: NSPersistentContainer
    
    private init() {
        persistentContainer = NSPersistentContainer(name: "AppModel")
        persistentContainer.loadPersistentStores { _, error in
            if let error = error {
                fatalError("Failed to load Core Data stack: \(error)")
            }
        }
    }

    var context: NSManagedObjectContext {
        return persistentContainer.viewContext
    }

    // 데이터 저장
    func saveTask(name: String) {
        let task = Task(context: context)
        task.name = name
        task.date = Date()
        saveContext()
    }

    // 데이터 읽기
    func fetchTasks() -> [Task] {
        let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
        do {
            return try context.fetch(fetchRequest)
        } catch {
            print("Failed to fetch tasks: \(error)")
            return []
        }
    }

    private func saveContext() {
        if context.hasChanges {
            do {
                try context.save()
                print("Data saved successfully")
            } catch {
                print("Failed to save data: \(error)")
            }
        }
    }
}

// 사용 예제
let coreDataHelper = CoreDataHelper.shared
coreDataHelper.saveTask(name: "Buy groceries")
let tasks = coreDataHelper.fetchTasks()
tasks.forEach { print("Task: \($0.name ?? "Unnamed")") }

 

4. SQLite

경량 SQL DB입니다. 구조화된 데이터를 효율적으로 저장하고 조회할 수 있습니다.
플랫폼 독립적이여서 꺼내서 다른곳에서 읽을 수도 있습니다. SQL쿼리를 알아야 사용할 수 있다는 러닝커브가 존재합니다.

import Foundation
import SQLite3

class SQLiteHelper {
    static let shared = SQLiteHelper()
    
    private var db: OpaquePointer?
    private let dbName = "AppData.sqlite"
    private let writeQueue = DispatchQueue(label: "com.usicroom.sqlite.write",
                                           attributes: .concurrent)
    
    private init() {
        openDatabase()
        createTable()
    }
    
    private func openDatabase() {
        let fileURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
            .first!
            .appendingPathComponent(dbName)
        
        if sqlite3_open(fileURL.path, &db) != SQLITE_OK {
            print("Error opening database")
        } else {
            print("Database opened successfully")
        }
    }
    
    private func createTable() {
        let createTableQuery = """
        CREATE TABLE IF NOT EXISTS AppUsage (
            id INTEGER PRIMARY KEY,
            launchCount INTEGER
        );
        """
        
        var statement: OpaquePointer?
        if sqlite3_prepare_v2(db, createTableQuery, -1, &statement, nil) == SQLITE_OK {
            if sqlite3_step(statement) == SQLITE_DONE {
                print("Table created or already exists")
            } else {
                print("Failed to create table")
            }
        } else {
            print("Error preparing create table statement")
        }
        sqlite3_finalize(statement)
    }
    
    // 실행 횟수 읽기 (동기)
    func getLaunchCount() -> Int {
        let query = "SELECT launchCount FROM AppUsage WHERE id = 1 LIMIT 1;"
        var statement: OpaquePointer?
        var count = 0
        
        if sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK {
            if sqlite3_step(statement) == SQLITE_ROW {
                count = Int(sqlite3_column_int(statement, 0))
            }
        } else {
            print("Error preparing select statement")
        }
        sqlite3_finalize(statement)
        return count
    }
    
    // 실행 횟수 읽기 (비동기)
    func getLaunchCountAsync(completion: @escaping (Int) -> Void) {
        DispatchQueue.global().async { [weak self] in
            guard let self = self else { return }
            let count = self.getLaunchCount()
            DispatchQueue.main.async {
                completion(count)
            }
        }
    }
    
    // 실행 횟수 업데이트 (비동기, 멀티스레드 지원)
    func updateLaunchCount(_ newCount: Int, completion: (() -> Void)? = nil) {
        writeQueue.async(flags: .barrier) { [weak self] in
            guard let self = self else { return }
            
            let updateQuery = """
            INSERT INTO AppUsage (id, launchCount)
            VALUES (1, ?1)
            ON CONFLICT(id)
            DO UPDATE SET launchCount = ?1;
            """
            
            var statement: OpaquePointer?
            if sqlite3_prepare_v2(self.db, updateQuery, -1, &statement, nil) == SQLITE_OK {
                sqlite3_bind_int(statement, 1, Int32(newCount))
                
                if sqlite3_step(statement) == SQLITE_DONE {
                    print("Launch count updated to \(newCount)")
                } else {
                    print("Failed to update launch count")
                }
            } else {
                print("Error preparing update statement")
            }
            sqlite3_finalize(statement)
            
            DispatchQueue.main.async {
                completion?()
            }
        }
    }
    
    deinit {
        sqlite3_close(db)
    }
}

 

5. File System

파일 형태로 데이터를 직접 저장하는 방식입니다. 원하는 형식의 데이터를 자유롭게 저장 가능하나 데이터 검색 및 관리가 복잡해 질 수 있습니다.

import Foundation

class FileSystemHelper {
    static let shared = FileSystemHelper()
    private let fileName = "userSettings.json"

    private init() {}

    private var fileURL: URL {
        let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        return documentDirectory.appendingPathComponent(fileName)
    }

    // 데이터 저장
    func saveSettings(_ settings: [String: Any]) {
        do {
            let data = try JSONSerialization.data(withJSONObject: settings, options: .prettyPrinted)
            try data.write(to: fileURL)
            print("Settings saved to \(fileURL)")
        } catch {
            print("Failed to save settings: \(error)")
        }
    }

    // 데이터 읽기
    func loadSettings() -> [String: Any]? {
        do {
            let data = try Data(contentsOf: fileURL)
            return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
        } catch {
            print("Failed to load settings: \(error)")
            return nil
        }
    }
}

// 사용 예제
let fileSystemHelper = FileSystemHelper.shared
fileSystemHelper.saveSettings(["theme": "dark", "fontSize": 14])
if let settings = fileSystemHelper.loadSettings() {
    print("Loaded settings: \(settings)")
}

 

6. Cloud Kit

iCloud와 연동하여 데이터를 클라우드에 저장합니다. 애플 생태계에 최적화된 클라우드 솔루션이며 동기화 및 공유 데이터에 적합합니다.

import CloudKit

class CloudKitHelper {
    static let shared = CloudKitHelper()
    private let container = CKContainer.default()
    private let publicDB: CKDatabase
    
    private init() {
        publicDB = container.publicCloudDatabase
    }

    // 데이터 저장
    func saveRecord(title: String, content: String) {
        let record = CKRecord(recordType: "Memo")
        record["title"] = title
        record["content"] = content
        
        publicDB.save(record) { savedRecord, error in
            if let error = error {
                print("Failed to save record: \(error)")
            } else {
                print("Record saved: \(savedRecord!)")
            }
        }
    }

    // 데이터 읽기
    func fetchRecords(completion: @escaping ([CKRecord]) -> Void) {
        let query = CKQuery(recordType: "Memo", predicate: NSPredicate(value: true))
        publicDB.perform(query, inZoneWith: nil) { records, error in
            if let error = error {
                print("Failed to fetch records: \(error)")
                completion([])
            } else {
                completion(records ?? [])
            }
        }
    }
}

// 사용 예제
let cloudKitHelper = CloudKitHelper.shared
cloudKitHelper.saveRecord(title: "First Note", content: "This is a test note.")
cloudKitHelper.fetchRecords { records in
    records.forEach { print("Title: \($0["title"] ?? ""), Content: \($0["content"] ?? "")") }
}

 

7. Realm

경량 객체 기반 DB입니다. Core Data보다 간단하고 직관적입니다. 관계형 데이터 관리와 쿼리 성능이 우수합니다.
외부라이브러리를 관리해야하는 번거로움이 있습니다.

import RealmSwift

class Task: Object {
    @objc dynamic var id = UUID().uuidString
    @objc dynamic var name = ""
    @objc dynamic var date = Date()

    override static func primaryKey() -> String? {
        return "id"
    }
}

class RealmHelper {
    static let shared = RealmHelper()
    private let realm = try! Realm()

    private init() {}

    // 데이터 저장
    func saveTask(name: String) {
        let task = Task()
        task.name = name

        try! realm.write {
            realm.add(task)
            print("Task saved: \(name)")
        }
    }

    // 데이터 읽기
    func fetchTasks() -> Results<Task> {
        return realm.objects(Task.self)
    }
}

// 사용 예제
let realmHelper = RealmHelper.shared
realmHelper.saveTask(name: "Read a book")
let tasks = realmHelper.fetchTasks()
tasks.forEach { print("Task: \($0.name), Date: \($0.date)") }