본문 바로가기
한눈가계부/데이터 모델

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

by 밤새는 탐험가89 2025. 8. 24.
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