☁️ CloudKit 완전정복

iCloud를 무료 백엔드로 사용하세요. 서버 없이 데이터 동기화, 인증, 푸시 알림까지!

✨ CloudKit이란?

CloudKit은 Apple이 제공하는 무료 클라우드 데이터베이스입니다. iCloud 계정만 있으면 별도 서버 없이 데이터를 저장하고 동기화할 수 있습니다.

📊 Database 종류

DatabaseTypes.swift
import CloudKit

// Public Database: 모든 사용자가 읽기 가능
let publicDB = CKContainer.default().publicCloudDatabase

// Private Database: 사용자 개인 데이터 (iCloud 로그인 필요)
let privateDB = CKContainer.default().privateCloudDatabase

// Shared Database: 여러 사용자 간 공유 (iOS 10+)
let sharedDB = CKContainer.default().sharedCloudDatabase

💾 Record 저장하기

SaveRecord.swift
import CloudKit

func saveNote(title: String, content: String) async throws {
    let record = CKRecord(recordType: "Note")
    record["title"] = title as CKRecordValue
    record["content"] = content as CKRecordValue
    record["createdAt"] = Date() as CKRecordValue

    let database = CKContainer.default().privateCloudDatabase
    try await database.save(record)
}

// 사용 예시
Task {
    try await saveNote(title: "첫 번째 메모", content: "CloudKit 테스트")
}

🔍 Record 검색하기

QueryRecords.swift
import CloudKit

func fetchNotes() async throws -> [CKRecord] {
    let database = CKContainer.default().privateCloudDatabase

    // 모든 Note 가져오기
    let query = CKQuery(
        recordType: "Note",
        predicate: NSPredicate(value: true)
    )

    // 최신순 정렬
    query.sortDescriptors = [NSSortDescriptor(key: "createdAt", ascending: false)]

    let (results, _) = try await database.records(matching: query)

    // 결과 변환
    let records = results.compactMap { _, result in
        try? result.get()
    }

    return records
}

// 특정 조건으로 검색
func searchNotes(keyword: String) async throws -> [CKRecord] {
    let database = CKContainer.default().privateCloudDatabase

    // title에 keyword 포함
    let predicate = NSPredicate(format: "title CONTAINS %@", keyword)
    let query = CKQuery(recordType: "Note", predicate: predicate)

    let (results, _) = try await database.records(matching: query)
    return results.compactMap { try? $0.value.get() }
}

✏️ Record 수정 & 삭제

UpdateDelete.swift
// 수정
func updateNote(_ record: CKRecord, newTitle: String) async throws {
    record["title"] = newTitle as CKRecordValue
    record["updatedAt"] = Date() as CKRecordValue

    let database = CKContainer.default().privateCloudDatabase
    try await database.save(record)
}

// 삭제
func deleteNote(_ record: CKRecord) async throws {
    let database = CKContainer.default().privateCloudDatabase
    try await database.deleteRecord(withID: record.recordID)
}

// 여러 Record 한 번에 처리
func batchSave(_ records: [CKRecord]) async throws {
    let database = CKContainer.default().privateCloudDatabase
    try await database.modifyRecords(saving: records, deleting: [])
}

🔗 Reference (관계)

References.swift
// 1:N 관계 구현
func createComment(noteRecord: CKRecord, text: String) async throws {
    let comment = CKRecord(recordType: "Comment")
    comment["text"] = text as CKRecordValue

    // Note와 연결
    let reference = CKRecord.Reference(record: noteRecord, action: .deleteSelf)
    comment["note"] = reference

    let database = CKContainer.default().privateCloudDatabase
    try await database.save(comment)
}

// 특정 Note의 모든 Comment 가져오기
func fetchComments(for noteRecord: CKRecord) async throws -> [CKRecord] {
    let database = CKContainer.default().privateCloudDatabase

    let reference = CKRecord.Reference(record: noteRecord, action: .none)
    let predicate = NSPredicate(format: "note == %@", reference)
    let query = CKQuery(recordType: "Comment", predicate: predicate)

    let (results, _) = try await database.records(matching: query)
    return results.compactMap { try? $0.value.get() }
}

📎 파일 업로드 (CKAsset)

AssetUpload.swift
import CloudKit
import UIKit

func saveNoteWithImage(title: String, image: UIImage) async throws {
    // 이미지를 임시 파일로 저장
    let tempDir = FileManager.default.temporaryDirectory
    let imageURL = tempDir.appendingPathComponent("temp_image.jpg")

    if let imageData = image.jpegData(compressionQuality: 0.8) {
        try imageData.write(to: imageURL)
    }

    // CKAsset 생성
    let asset = CKAsset(fileURL: imageURL)

    // Record에 추가
    let record = CKRecord(recordType: "Note")
    record["title"] = title as CKRecordValue
    record["image"] = asset

    let database = CKContainer.default().privateCloudDatabase
    try await database.save(record)

    // 임시 파일 삭제
    try? FileManager.default.removeItem(at: imageURL)
}

// 이미지 다운로드
func downloadImage(from record: CKRecord) async -> UIImage? {
    guard let asset = record["image"] as? CKAsset,
          let fileURL = asset.fileURL,
          let data = try? Data(contentsOf: fileURL) else {
        return nil
    }

    return UIImage(data: data)
}

🔔 실시간 동기화 (Subscription)

Subscription.swift
import CloudKit

func subscribeToNotes() async throws {
    let database = CKContainer.default().privateCloudDatabase

    // 모든 Note 변경 감지
    let predicate = NSPredicate(value: true)
    let subscription = CKQuerySubscription(
        recordType: "Note",
        predicate: predicate,
        options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion]
    )

    // 알림 설정
    let notification = CKSubscription.NotificationInfo()
    notification.shouldSendContentAvailable = true  // Silent push
    subscription.notificationInfo = notification

    try await database.save(subscription)
}

// AppDelegate에서 푸시 처리
func application(
    _ application: UIApplication,
    didReceiveRemoteNotification userInfo: [AnyHashable: Any],
    fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
    let notification = CKNotification(fromRemoteNotificationDictionary: userInfo)

    if notification.notificationType == .query {
        // 데이터 다시 로드
        Task {
            let notes = try await fetchNotes()
            // UI 업데이트
        }
    }

    completionHandler(.newData)
}

👥 공유 (CKShare)

Sharing.swift
import CloudKit
import UIKit

func shareNote(_ record: CKRecord, from viewController: UIViewController) async throws {
    let share = CKShare(rootRecord: record)

    // 권한 설정
    share[CKShare.SystemFieldKey.title] = "메모 공유"
    share.publicPermission = .readOnly

    let database = CKContainer.default().privateCloudDatabase
    try await database.modifyRecords(saving: [record, share], deleting: [])

    // 공유 시트 표시
    let controller = UICloudSharingController(share: share, container: .default())
    viewController.present(controller, animated: true)
}

🧪 계정 상태 확인

AccountStatus.swift
import CloudKit

func checkiCloudStatus() async {
    let container = CKContainer.default()

    do {
        let status = try await container.accountStatus()

        switch status {
        case .available:
            print("iCloud 사용 가능")

        case .noAccount:
            print("iCloud 로그인 필요")
            // "설정 > iCloud"로 안내

        case .restricted:
            print("iCloud 제한됨 (자녀 보호 등)")

        case .couldNotDetermine:
            print("iCloud 상태 확인 불가")

        @unknown default:
            print("알 수 없는 상태")
        }
    } catch {
        print("에러: \(error)")
    }
}

📱 SwiftUI 통합 예제

NotesApp.swift
import SwiftUI
import CloudKit

struct Note: Identifiable {
    let id: String
    var title: String
    var content: String
    let record: CKRecord
}

@Observable
class NotesViewModel {
    var notes: [Note] = []
    var isLoading = false

    func loadNotes() async {
        isLoading = true
        defer { isLoading = false }

        do {
            let records = try await fetchNotes()
            notes = records.map {
                Note(
                    id: $0.recordID.recordName,
                    title: $0["title"] as? String ?? "",
                    content: $0["content"] as? String ?? "",
                    record: $0
                )
            }
        } catch {
            print("에러: \(error)")
        }
    }
}

struct NotesView: View {
    var viewModel = NotesViewModel()

    var body: some View {
        List(viewModel.notes) { note in
            VStack(alignment: .leading) {
                Text(note.title)
                    .font(.headline)
                Text(note.content)
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
        }
        .overlay {
            if viewModel.isLoading {
                ProgressView()
            }
        }
        .task {
            await viewModel.loadNotes()
        }
    }
}

💡 CloudKit 장점
✅ 무료 (1PB 저장소, 200GB 전송량)
✅ 서버 불필요 (백엔드 인프라 무료)
✅ iCloud 인증 자동 처리
✅ 실시간 동기화 지원
✅ Public/Private/Shared Database

📦 학습 자료

💻
GitHub 프로젝트
🍎
Apple 공식 문서
🎥
WWDC21 세션