[Swift 입문] 27편 — FileManager와 파일 시스템

🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [26편] UserDefaults와 앱 설정 저장

파일로 데이터를 저장해야 할 때

UserDefaults는 소규모 설정값에 적합하지만, 다음 상황에서는 파일이 필요합니다.

  • 수백 개 이상의 메모·문서·로그 저장
  • 이미지, 오디오, 바이너리 데이터
  • 내보내기/가져오기가 필요한 데이터
  • 사용자가 접근해야 하는 문서

Swift에서 파일 시스템을 다루는 핵심 클래스는 FileManager입니다.


앱이 사용하는 디렉토리

macOS/iOS 앱은 보안 샌드박스 안에서 동작합니다. 아무 경로에나 파일을 쓸 수 없고, 정해진 디렉토리를 사용해야 합니다.

import Foundation

let fm = FileManager.default

// 1. Documents — 사용자 문서. iTunes/Finder로 접근 가능, iCloud 백업 대상
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
print(docs)  // /Users/hoin/Library/Containers/com.myapp/Data/Documents/

// 2. Application Support — 앱 데이터. 사용자에게 노출 안 됨
let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!

// 3. Caches — 캐시. 시스템이 필요 시 삭제 가능
let caches = fm.urls(for: .cachesDirectory, in: .userDomainMask).first!

// 4. 임시 파일
let temp = fm.temporaryDirectory
print(temp)  // /var/folders/.../T/

// macOS — 사용자 홈 디렉토리 (샌드박스 없는 앱)
let home = fm.homeDirectoryForCurrentUser

일반적으로:

  • Documents: 사용자가 직접 관리하는 파일 (노트, 내보내기 파일)
  • Application Support: 앱 내부 데이터베이스, 설정 파일
  • Caches: 다운로드 캐시, 썸네일 (삭제되어도 재생성 가능한 것)

파일 읽기와 쓰기

텍스트 파일

let docsURL = FileManager.default.urls(for: .documentDirectory,
                                        in: .userDomainMask).first!
let fileURL = docsURL.appendingPathComponent("note.txt")

// 쓰기
let content = "안녕하세요!\n이것은 파일에 저장된 텍스트입니다."
try content.write(to: fileURL, atomically: true, encoding: .utf8)

// 읽기
let loaded = try String(contentsOf: fileURL, encoding: .utf8)
print(loaded)

// atomically: true → 임시 파일에 먼저 쓴 뒤 교체 (중간에 앱 충돌 시 데이터 보호)

Data (바이너리) 파일

let imageURL = docsURL.appendingPathComponent("photo.png")

// 쓰기
let imageData: Data = // ... 이미지 데이터
try imageData.write(to: imageURL, options: .atomic)

// 읽기
let loadedData = try Data(contentsOf: imageURL)

Codable 객체를 파일로 저장

struct Note: Codable, Identifiable {
    let id: UUID
    var title: String
    var body: String
    var createdAt: Date
}

class NoteStorage {
    private let fileURL: URL

    init() {
        let docs = FileManager.default.urls(for: .documentDirectory,
                                             in: .userDomainMask).first!
        fileURL = docs.appendingPathComponent("notes.json")
    }

    func save(_ notes: [Note]) throws {
        let data = try JSONEncoder().encode(notes)
        try data.write(to: fileURL, options: .atomic)
    }

    func load() throws -> [Note] {
        guard FileManager.default.fileExists(atPath: fileURL.path) else {
            return []  // 파일이 없으면 빈 배열
        }
        let data = try Data(contentsOf: fileURL)
        return try JSONDecoder().decode([Note].self, from: data)
    }
}

파일과 디렉토리 관리

let fm = FileManager.default
let baseURL = fm.urls(for: .documentDirectory, in: .userDomainMask).first!

// 디렉토리 생성
let notesDir = baseURL.appendingPathComponent("Notes")
try fm.createDirectory(at: notesDir, withIntermediateDirectories: true)
// withIntermediateDirectories: true → 중간 경로도 자동 생성

// 파일 존재 확인
if fm.fileExists(atPath: notesDir.path) {
    print("디렉토리 있음")
}

// 파일 목록 조회
let files = try fm.contentsOfDirectory(at: notesDir,
                                        includingPropertiesForKeys: [.fileSizeKey, .creationDateKey],
                                        options: .skipsHiddenFiles)
for file in files {
    print(file.lastPathComponent)
}

// 복사
let srcURL = notesDir.appendingPathComponent("note1.txt")
let dstURL = notesDir.appendingPathComponent("note1_backup.txt")
try fm.copyItem(at: srcURL, to: dstURL)

// 이동/이름 변경
let newURL = notesDir.appendingPathComponent("renamed_note.txt")
try fm.moveItem(at: srcURL, to: newURL)

// 삭제
try fm.removeItem(at: dstURL)

파일 속성 읽기

let fileURL = baseURL.appendingPathComponent("data.json")

// 리소스 값 (성능 최적화됨)
let resourceValues = try fileURL.resourceValues(forKeys: [
    .fileSizeKey,
    .creationDateKey,
    .contentModificationDateKey,
    .isDirectoryKey
])

let fileSize = resourceValues.fileSize ?? 0
let created = resourceValues.creationDate
let modified = resourceValues.contentModificationDate
let isDir = resourceValues.isDirectory ?? false

print("크기: \(fileSize) bytes")
print("생성일: \(created?.description ?? "알 수 없음")")

// 사람이 읽기 좋은 파일 크기
let bcf = ByteCountFormatter()
bcf.allowedUnits = [.useKB, .useMB]
bcf.countStyle = .file
print("크기: \(bcf.string(fromByteCount: Int64(fileSize)))")  // "4.2 KB"

비동기 파일 처리

큰 파일을 읽고 쓸 때는 메인 스레드를 블록하면 안 됩니다.

// async/await로 파일 작업 비동기 처리
actor FileStore {
    private let fileURL: URL

    init(url: URL) {
        self.fileURL = url
    }

    func save(_ data: Data) async throws {
        // Task.detached로 백그라운드 스레드에서 실행
        try await Task.detached(priority: .background) {
            try data.write(to: self.fileURL, options: .atomic)
        }.value
    }

    func load() async throws -> Data {
        try await Task.detached(priority: .background) {
            try Data(contentsOf: self.fileURL)
        }.value
    }
}

actor를 사용해 동시 접근을 막고, Task.detached로 파일 IO를 백그라운드에서 처리합니다.


앱 그룹 — 앱 간 파일 공유

macOS에서 여러 앱(예: 메인 앱 + 확장 앱)이 같은 파일에 접근하려면 App Group을 사용합니다.

// App Group 컨테이너 URL
let groupURL = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.mycompany.myapp"
)

let sharedDB = groupURL?.appendingPathComponent("shared.db")

App Group은 Xcode에서 Signing & Capabilities에 추가합니다.


핵심 요약

  • FileManager.default: 파일 시스템 작업의 진입점
  • 앱 샌드박스 내 주요 디렉토리: Documents, Application Support, Caches, Temp
  • String.write(to:atomically:encoding:): 텍스트 파일 저장
  • Data.write(to:options:): 바이너리 파일 저장. .atomic으로 안전하게
  • createDirectory, copyItem, moveItem, removeItem으로 파일 관리
  • 대용량 파일은 백그라운드 스레드에서 처리

다음 편에서는 대량의 구조화된 데이터를 효율적으로 저장하는 SQLite를 배웁니다.

🤖 Generated with Claude Code

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다