안녕하세요! 가계부 앱을 만들며 코어 데이터 모델뿐만 아니라, 앱의 로컬 저장소를 효율적으로 관리하는 방법에 대해서도 고민했어요.
오늘은 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
}
}'한눈가계부 > 데이터 모델' 카테고리의 다른 글
| 안녕! '신(God) 객체' 👋 더 똑똑한 ViewModel 아키텍처로 가자 (3) | 2025.08.27 |
|---|---|
| 💡 ViewModel 설계: 통합형 vs. 분리형, 당신의 선택은? 🤔 (2) | 2025.08.25 |
| 💡 iOS 개발 필수 개념: Future와 AnyPublisher 완벽 정리 (0) | 2025.08.24 |
| 💡 iOS 가계부 앱 개발: Core Data와 FileManager 연동하기 (0) | 2025.08.24 |
| 📱 iOS 가계부 앱 개발기: 데이터 모델 설계 파헤치기 ✨ (1) | 2025.08.24 |