CloudKit을 Firebase로 마이그레이션 하기

마이그레이션 배경

migrate-cloukit-firebase

서비스가 애플 기기만 지원한다면 CloudKit과 CoreData를 사용해 DB 동기화를 진행하면 편합니다. 하지만 웹과 안드로이드를 지원해야 한다면 상황이 달라집니다.

CloudKit JS를 통해 웹과 안드로이드에서도 iCloud DB에 접근할 수 있기는 합니다. 하지만 다른 솔루션보다 비교적 구현이 까다롭고, 사용자 입장에서 애플 계정이 반드시 필요하다는 단점이 있습니다.

미래에라도 웹과 안드로이드를 지원할 계획이 있다면, CloudKit은 조금 아쉬운 솔루션이 될 수 있습니다. 제가 운영하고 있는 서비스 또한 미래에 웹과 안드로이드를 지원할 계획이 생기면서 기존에 사용하던 CloudKit을 걷어내고 Firebase를 사용하기 시작했습니다.

Firebase 장점

migrate-cloukit-firebase

Firebase에는 여러 장점이 있지만, 그 중에서도 가장 마음에 들었던 장점에 대해 소개해보고자 합니다.

우선, 사용자가 다양한 방식으로 로그인해서 DB에 접근 할 수 있도록 지원합니다. 이메일과 전화번호 로그인은 당연하고, 구글 로그인이나 애플 로그인과 같은 소셜 로그인을 비롯해 SAML 로그인 방식까지 제공합니다.

서비스 초기에는 Firebase로 충분하고도 넘치겠지만, 서비스가 커지면 조금 더 유연한 기능과 가격 정책의 솔루션이 필요할 수 있습니다. 그럴 때 개발 인력이 충분하기만 하다면, 언제든 GCP(구글 클라우드 플랫폼)로 확장할 수 있어 이 또한 큰 장점입니다.

Firebase에는 이미 수많은 기능이 구현되어 있고, 이를 사용하기만 하면 서비스에 바로 적용할수 있기 때문에 비교적 손쉬운 구현 방법과 인터넷상에 존재하는 방대한 양의 리소스 역시 장점이라고 생각합니다.

마지막으로, Firestore는 Firebase의 DB 서비스인데요. Firestore가 오프라인 모드를 지원하기 때문에 Core Data와 같이 별도의 로컬 데이터베이스가 필요하지 않았고 이 또한 모바일에 개발 할 때 매우 편리했습니다.

마이그레이션 방법

마이그레이션을 위해서 가장 좋은 시나리오는 CloudKit의 iCloud 데이터베이스에서 데이터를 추출해서 Firebase의 Firestore로 옮기는 것입니다. 하지만, iCloud는 데이터 추출을 지원하지 않고 있고 그렇기에 이런 방식으로는 마이그레이션 할 수 없었습니다.

그래서 약간의 우회적인 방법을 사용했습니다. CloudKit에서 사용자의 로컬 DB인 Core Data에 데이터가 들어오면, 그 데이터를 갖고 Firebase로 마이그레이션을 진행하는 것입니다. 즉, CloudKit에서 Core Data로, 그리고 Core Data에서 Firebase로 데이터를 옮기는 것입니다.

마이그레이션 진행

앱이 Firebase 마이그레이션 버전을 최초로 실행 할 때 실행할 마이그레이션 함수를 구현해보겠습니다. 우선 제 서비스의 데이터는 사진과 같이 크게 Facials와 Tasks로 나뉘어 있습니다.

migrate-cloukit-firebase

그렇기 때문에 Firestore에 Facials와 Tasks 컬렉션을 만들어줍니다.

let database = Firestore.firestore().collection("Users").document("\(user.id)")

let facialsRef = database.collection("Facials")
let tasksRef = database.collection("Tasks")

이제 migrateCloudKit이라는 마이그레이션 함수를 구현해보겠습니다. 우선, parameter로 CoreData 데이터를 각각 받아와 migrateFacialsmigrateTasks에 각각 넣어줍니다.

func migrateCloudKit(coreFacials: FetchedResults<Facials>, coreTasks: FetchedResults<Tasks>) {
  migrateFacials(coreFacials: coreFacials)
  migrateTasks(coreTasks: coreTasks)
}

migrateFacials는 이렇게 생겼습니다. CoreData의 모든 Facials 데이터를 돌면서 Firestore에 자료형에 맞게 setData 합니다.

func migrateFacials(coreFacials: FetchedResults<Facials>) {
  coreFacials.forEach { coreFacial in
    let facial = facialsRef.document(UUID().uuidString)
    
    facial.setData([
      "value": coreFacial.value // Example
    ])
  }
}

migrateTasks 역시 비슷한 방식으로 migrate 합니다. 다만, 여기서는 setData가 완료되고 마이그레이션이 끝나면 migrated 키 값을 가진 UserDefaults를 true로 설정합니다. 이렇게 하면, 앱이 다음 번에 실행 될 때 마이그레이션이 일어나지 않습니다.

func migrateTasks(coreTasks: FetchedResults<Tasks>) {
  coreTasks.forEach { coreTask in
    if let uuid = coreTask.id?.uuidString {
      let task = tasksRef.document(uuid)

      task.setData([
        "due": coreTask.due ?? Date(), // Example
        "text": coreTask.text ?? "" // Example
      ]) { _ in
        // Set migrated to true
        UserDefaults.standard.set(true, forKey: "migrated")
      }
    }
  }
}

SwiftUI 라이프사이클 onAppear에서 마이그레이션 여부에 따라, migrateCloudKit를 실행합니다.

.onAppear {
  if !UserDefaults.standard.bool(forKey: "migrated")  {
    migrateCloudKit(coreFacials: facials, coreTasks: tasks)
  }
}

이렇게 마이그레이션 과정이 끝나면, 기존의 데이터는 모두 Firebase의 Firestore로 이동됩니다. 이제 기존의 CoreData 코드를 Firebase에 맞게 수정해주면 Firebase로의 마이그레이션이 모두 끝납니다.