☁️ 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