🤖 이 글은 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