한눈가계부/데이터 모델

📝 iOS 가계부 앱 개발: FileManager로 로컬 데이터 관리하기

밤새는 탐험가89 2025. 8. 24. 06:44
728x90
SMALL

안녕하세요! 가계부 앱을 만들며 코어 데이터 모델뿐만 아니라, 앱의 로컬 저장소를 효율적으로 관리하는 방법에 대해서도 고민했어요.

오늘은 FileManager를 활용해 이미지 같은 데이터를 앱 내부에 저장하고 관리하는 방법에 대해 공유해 보려고 합니다. 📁


1. TransactionFileManager 클래스: 싱글톤 패턴으로 설계 🔑

 앱에서 파일 관리 로직은 여러 곳에서 필요할 수 있기 때문에, 저는 TransactionFileManager라는 클래스를 싱글톤 패턴으로 만들었습니다.

이 패턴 덕분에 앱의 어떤 곳에서든 단 하나의 인스턴스에 접근해 파일을 관리할 수 있어 코드가 깔끔해집니다.

class TransactionFileManager {
    static let shared = TransactionFileManager()
    private init() { }
    private let fileManager = FileManager.default
    // ... (메서드 생략)
}

2. 핵심 기능: CRUD 메서드 파헤치기 🛠️

이 클래스는 이미지를 저장하고(Create), 불러오고(Read), 삭제(Delete)하는 핵심 기능을 담당합니다.

 

✔︎ 파일 경로 가져오기 (getDocumentsDirectory)

 모든 파일 작업의 시작은 저장할 경로를 찾는 것에서 시작합니다.

앱의 샌드박스 안에 있는 Documents 폴더는 사용자 데이터를 안전하게 저장하기에 가장 적합한 위치입니다.

private func getDocumentsDirectory() -> URL {
    guard let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
        fatalError("❌ Document 디렉토리를 찾을 수 없습니다.")
    }
    return url
}

 

✔︎ 이미지 저장 및 업데이트 (saveImage)

 이 메서드는 영수증 같은 이미지를 저장하고, 만약 기존 이미지가 있다면 덮어쓰는 업데이트 기능도 함께 수행합니다.

각 거래 내역의 UUID를 이용해 폴더를 생성하는 방식으로 파일을 깔끔하게 정리하죠.

func saveImage(_ image: UIImage, _ transactionID: String) -> String? {
    // transactionID로 폴더 경로 생성
    let transactionFolder = getDocumentsDirectory().appendingPathComponent(transactionID)
    
    // 기존 폴더 삭제 후 새 폴더 생성
    if fileManager.fileExists(atPath: transactionFolder.path) {
        try? fileManager.removeItem(at: transactionFolder)
    }
    // ... (이미지 저장 로직)
    
    // 저장 성공 시 상대 경로 반환
    return "\(transactionID)/image.jpg"
}

 

 이 메서드가 반환하는 상대 경로(transactionID/image.jpg)를 ExpenseModel에 저장해두면 나중에 이미지를 쉽게 찾아 불러올 수 있습니다.

 

✔︎ 이미지 불러오기 (loadImage)

 saveImage 메서드가 반환한 상대 경로를 이용해 이미지를 다시 불러오는 기능입니다.

파일이 존재하지 않을 경우 nil을 반환하도록 하여 오류를 방지합니다.

func loadImage(from relativePath: String) -> UIImage? {
    let fullPath = getDocumentsDirectory().appendingPathComponent(relativePath)
    if !fileManager.fileExists(atPath: fullPath.path) {
        return nil
    }
    return UIImage(contentsOfFile: fullPath.path)
}

 

✔︎ 이미지 삭제 (deleteFolder)

 가계부 내역을 삭제할 때, 그 내역과 관련된 이미지 파일도 함께 삭제되어야 불필요한 용량 낭비를 막을 수 있습니다.

이 메서드는 transactionID에 해당하는 폴더 전체를 제거합니다.

func deleteFolder(for transactionID: String) -> Bool {
    let folder = getDocumentsDirectory().appendingPathComponent(transactionID)
    if fileManager.fileExists(atPath: folder.path) {
        do {
            try fileManager.removeItem(at: folder)
            return true
        } catch {
            print("❌ 폴더 삭제 실패: \(error.localizedDescription)")
            return false
        }
    }
    return false
}

3. 마무리하며: 로컬 저장소의 중요성 💡

 데이터베이스는 텍스트 위주의 모델을 관리하고, FileManager는 이미지 같은 바이너리 파일을 관리하도록 역할을 분담하면 앱의 성능과 안정성을 모두 잡을 수 있습니다.

이렇게 체계적인 파일 관리 클래스를 미리 설계해두니, 앞으로의 개발이 훨씬 수월할 것 같네요!


✅ 전체코드

import Foundation
import UIKit


class TransactionFileManager {
    
    
    
    // MARK: - Variable
    static let shared = TransactionFileManager()
    private init() { }
    private let fileManager = FileManager.default
    
    
    
    // MARK: - Function
    /// Documents 폴더 경로 가져오기
    private func getDocumentsDirectory() -> URL {
        guard let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("❌ Document 디렉토리를 찾을 수 없습니다.")
        }
        return url
    }
    
    
    /// 이미지를 지정된 트랜잭션 ID 폴더에 저장하고, 저장된 이미지의 상대 경로를 반환합니다.
    /// - Parameters:
    ///   - image: UIImage 타입의 저장할 이미지.
    ///   - transactionID: 이미지를 저장할 폴더 이름으로 사용될 트랜잭션의 고유 ID.
    /// - Returns: 이미지가 성공적으로 저장되었을 경우 상대 경로(String)를 반환하고, 실패했을 경우 nil을 반환합니다.
    func saveImage(_ image: UIImage, _ transactionID: String) -> String? {
        let transactionFolder = getDocumentsDirectory().appendingPathComponent(transactionID)
        
        // 기존 폴더 삭제
        if fileManager.fileExists(atPath: transactionFolder.path) {
            try? fileManager.removeItem(at: transactionFolder)
        }
        
        do {
            try fileManager.createDirectory(at: transactionFolder,
                                            withIntermediateDirectories: true,
                                            attributes: nil)
        } catch {
            print("❌ 디렉토리 생성 실패: \(error.localizedDescription)")
        }
        
        let fileName = "image.jpg"
        let fileURL = transactionFolder.appendingPathComponent(fileName)
        
        guard let imageData = image.jpegData(compressionQuality: 1.0) else {
            print("❌ 이미지 데이터를 JPEG로 변환 실패")
            return nil
        }
        
        do {
            try imageData.write(to: fileURL)
            return "\(transactionID)/\(fileName)"
        } catch {
            print("❌ 이미지 저장 실패: \(error.localizedDescription)")
            return nil
        }
    }
    
    
    /// 저장된 이미지를 상대 경로를 이용해 불러옵니다.
    /// - Parameter
    ///   - relativePath: 이미지 파일이 저장된 상대 경로 (예: "UUID/image.jpg").
    /// - Returns: 이미지가 존재할 경우 UIImage 객체를 반환하고, 파일이 없을 경우 nil을 반환합니다.
    func loadImage(from relativePath: String) -> UIImage? {
        let fullPath = getDocumentsDirectory().appendingPathComponent(relativePath)
        if !fileManager.fileExists(atPath: fullPath.path) {
            print("❌ 이미지 경로 없음: \(fullPath.path)")
            return nil
        }
        return UIImage(contentsOfFile: fullPath.path)
    }
    
    
    
    /// 지정된 트랜잭션 ID에 해당하는 폴더를 Documents 디렉토리에서 삭제합니다.
    /// - Parameter
    ///   - transactionID: 삭제할 폴더 이름으로 사용되는 트랜잭션의 고유 ID.
    /// - Returns: 폴더 삭제에 성공했을 경우 true를, 실패했거나 폴더가 존재하지 않을 경우 false를 반환합니다.
    func deleteFolder(for transactionID: String) -> Bool {
        let folder = getDocumentsDirectory().appendingPathComponent(transactionID)
        if fileManager.fileExists(atPath: folder.path) {
            do {
                try fileManager.removeItem(at: folder)
                return true
            } catch {
                print("❌ 폴더 삭제 실패: \(error.localizedDescription)")
                return false
            }
        }
        return false
    }
}
728x90
LIST