Parse iOS 앱을 Firebase로 마이그레이션

서비스 솔루션으로 대체 백엔드를 찾고 있는 Parse 사용자에게 Firebase는 iOS 앱을 위한 이상적인 선택이 될 것입니다.

이 가이드에서는 특정 서비스를 앱에 통합하는 방법을 설명합니다. Firebase 기본 설정 안내는 iOS+ 설정 가이드를 참조하세요.

Google 애널리틱스

Google 애널리틱스는 앱 사용 및 사용자 참여에 대한 통계를 제공하는 무료 앱 측정 솔루션입니다. 애널리틱스는 Firebase 기능 전체에 통합되며 Firebase SDK를 사용하여 정의할 수 있는 최대 500개의 고유한 이벤트에 대한 무제한 보고를 제공합니다.

자세한 내용은 Google 애널리틱스 문서를 참조하세요.

추천 마이그레이션 전략

여러 분석 제공업체를 함께 이용하는 경우에도 Google 애널리틱스를 손쉽게 도입할 수 있습니다. 앱에 추가하기만 하면 최초 사용, 앱 업데이트, 기기 모델, 연령 등 애널리틱스에서 자동으로 수집하는 이벤트 및 사용자 속성을 활용할 수 있습니다.

커스텀 이벤트 및 사용자 속성의 경우 이벤트 및 속성을 로깅하는 데 Parse Analytics 및 Google 애널리틱스를 둘 다 사용하는 이중 작성 전략을 사용하여 새 솔루션을 점진적으로 도입할 수 있습니다.

코드 비교

Parse Analytics

// Start collecting data
[PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];

NSDictionary *dimensions = @{
  // Define ranges to bucket data points into meaningful segments
  @"priceRange": @"1000-1500",
  // Did the user filter the query?
  @"source": @"craigslist",
  // Do searches happen more often on weekdays or weekends?
  @"dayType": @"weekday"
};
// Send the dimensions to Parse along with the 'search' event
[PFAnalytics trackEvent:@"search" dimensions:dimensions];

Google 애널리틱스

// Obtain the AppMeasurement instance and start collecting data
[FIRApp configure];

// Send the event with your params
[FIRAnalytics logEventWithName:@"search" parameters:@{
  // Define ranges to bucket data points into meaningful segments
  @"priceRange": @"1000-1500",
  // Did the user filter the query?
  @"source": @"craigslist",
  // Do searches happen more often on weekdays or weekends?
  @"dayType": @"weekday"
}];

Firebase 실시간 데이터베이스

Firebase 실시간 데이터베이스는 NoSQL 클라우드 호스팅 데이터베이스입니다. 데이터는 JSON으로 저장되며 연결된 모든 클라이언트에 실시간으로 동기화됩니다.

자세한 내용은 Firebase 실시간 데이터베이스 문서를 참조하세요.

Parse 데이터와의 차이

객체

Parse에서는 JSON 호환 데이터의 키-값 쌍이 있는 PFObject 또는 그 서브클래스를 저장합니다. 이 데이터는 스키마가 없습니다. 즉, 각 PFObject에 있는 키를 지정할 필요가 없습니다.

모든 Firebase 실시간 데이터베이스 데이터는 JSON 객체로 저장되고 PFObject에 해당하는 것은 없습니다. 사용 가능한 JSON 유형에 해당하는 JSON 트리 값 유형에 작성하면 됩니다.

다음 예시는 게임의 최고 점수를 저장할 수 있는 방법을 보여줍니다.

Parse
PFObject *gameScore = [PFObject objectWithClassName:@"GameScore"];
gameScore[@"score"] = @1337;
gameScore[@"playerName"] = @"Sean Plott";
gameScore[@"cheatMode"] = @NO;
[gameScore saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (succeeded) {
    // The object has been saved.
  } else {
    // There was a problem, check error.description
  }
}];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
NSString *key = [[ref child:@"scores"] childByAutoId].key;
NSDictionary *score = @{@"score": @1337,
                        @"playerName": @"Sean Plott",
                        @"cheatMode": @NO};
[key setValue:score withCompletionBlock:^(NSError *error,  FIRDatabaseReference *ref) {
  if (error) {
    // The object has been saved.
  } else {
    // There was a problem, check error.description
  }
}];
자세한 내용은 Apple 플랫폼에서 데이터 읽기 및 쓰기 가이드를 참조하세요.

데이터 사이의 관계

PFObject는 다른 PFObject와 연관되어 있을 수 있습니다. 모든 객체는 다른 객체를 값으로 사용할 수 있습니다.

Firebase 실시간 데이터베이스에서는 플랫 데이터 구조를 사용하여 관계를 더 잘 표현합니다. 이 구조에서는 별도의 경로로 데이터를 분할하므로 별도의 호출로 효율적으로 다운로드할 수 있습니다.

다음 예는 블로깅 앱의 게시물과 작성자 사이의 관계를 구조화하는 방법을 보여줍니다.

Parse
// Create the author
PFObject *myAuthor = [PFObject objectWithClassName:@"Author"];
myAuthor[@"name"] = @"Grace Hopper";
myAuthor[@"birthDate"] = @"December 9, 1906";
myAuthor[@"nickname"] = @"Amazing Grace";

// Create the post
PFObject *myPost = [PFObject objectWithClassName:@"Post"];
myPost[@"title"] = @"Announcing COBOL, a New Programming Language";

// Add a relation between the Post and the Author
myPost[@"parent"] = myAuthor;

// This will save both myAuthor and myPost
[myPost saveInBackground];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];

// Create the author
NSString *myAuthorKey = @"ghopper";
NSDictionary *author = @{@"name": @"Grace Hopper",
                         @"birthDate": @"December 9, 1906",
                         @"nickname": @"Amazing Grace"};
// Save the author
[[ref child:myAuthorKey] setValue:author]

// Create and save the post
NSString *key = [[ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"author": myAuthorKey,
                       @"title": @"Announcing COBOL, a New Programming Language"};
[key setValue:post]

다음 데이터 레이아웃은 결과입니다.

{
  // Info about the authors
  "authors": {
    "ghopper": {
      "name": "Grace Hopper",
      "date_of_birth": "December 9, 1906",
      "nickname": "Amazing Grace"
    },
    ...
  },
  // Info about the posts: the "author" fields contains the key for the author
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "ghopper",
      "title": "Announcing COBOL, a New Programming Language"
    }
    ...
  }
}
자세한 내용은 데이터베이스 구조화 가이드를 확인하세요.

데이터 읽기

Parse에서는 특정 Parse 객체의 ID를 사용하거나 PFQuery로 쿼리를 실행하여 데이터를 읽습니다.

Firebase에서는 데이터베이스 참조에 비동기 리스너를 연결하여 데이터를 검색합니다. 리스너는 데이터의 초기 상태에 한 번 트리거되고 데이터가 변경되면 다시 트리거되므로 데이터가 변경되었는지 확인하기 위해 코드를 추가할 필요가 없습니다.

다음 예는 '객체' 섹션에 제시된 예를 기준으로 특정 플레이어의 점수를 검색할 수 있는 방법을 보여줍니다.

Parse
PFQuery *query = [PFQuery queryWithClassName:@"GameScore"];
[query whereKey:@"playerName" equalTo:@"Dan Stemkoski"];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
  if (!error) {
    for (PFObject *score in objects) {
      NSString *gameScore = score[@"score"];
      NSLog(@"Retrieved: %@", gameScore);
    }
  } else {
    // Log details of the failure
    NSLog(@"Error: %@ %@", error, [error userInfo]);
  }
}];
Firebase
// Create a reference to the database
FIRDatabaseReference *ref = [[FIRDatabase database] reference];

// This type of listener is not one time, and you need to cancel it to stop
// receiving updates.
[[[[ref child:@"scores"] queryOrderedByChild:@"playerName"] queryEqualToValue:@"Dan Stemkoski"]
    observeEventType:FIRDataEventTypeChildAdded withBlock:^(FIRDataSnapshot *snapshot) {
  // This will fire for each matching child node.
  NSDictionary *score = snapshot.value;
  NSString gameScore = score[@"score"];
  NSLog(@"Retrieved: %@", gameScore);
}];
사용할 수 있는 이벤트 리스너 유형과 데이터의 순서 지정 및 필터링 방법의 자세한 내용은 Apple 플랫폼에서 데이터 읽기 및 쓰기 가이드를 확인하세요.

추천 마이그레이션 전략

데이터 다시 생각하기

Firebase 실시간 데이터베이스는 연결된 전체 클라이언트에서 순식간에 데이터를 동기화하도록 최적화되며 그 결과 데이터 구조는 Parse 코어 데이터와 다릅니다. 따라서 이전의 첫 번째 단계는 다음을 포함하여 데이터에서 필요한 변경사항을 고려하는 것입니다.

  • Parse 객체를 Firebase 데이터에 매핑하는 방식
  • 상위-하위 관계가 있는 경우 별도의 호출로 효율적으로 다운로드할 수 있도록 다른 경로로 데이터를 분할하는 방법

데이터 이전

Firebase에서 데이터를 구조화하는 방식을 결정한 후에는 앱이 양쪽 데이터베이스에 모두에 쓰기 작업을 해야 하는 기간을 어떻게 처리할지 계획해야 합니다. 이때 다음과 같은 옵션을 선택할 수 있습니다.

백그라운드 동기화

이 시나리오에서는 두 가지 앱 버전이 있습니다. Parse를 사용하는 이전 버전과 Firebase를 사용하는 새 버전입니다. 두 데이터베이스 사이의 동기화는 Parse에서 Firebase로 Parse Cloud Code에서 처리되며 코드에서 Firebase 변경사항을 수신하고 Parse에 변경사항을 동기화합니다. 다음은 새 버전 사용을 시작하기 전에 해야 하는 일입니다.

  • 기존 Parse 데이터를 새 Firebase 구조로 변환하고 Firebase 실시간 데이터베이스에 작성합니다.
  • 이전 클라이언트에서 Parse 데이터에서 적용한 변경사항을 Firebase 실시간 데이터베이스에 작성하기 위해 Firebase REST API를 사용하는 Parse Cloud Code 함수를 작성합니다.
  • Firebase에서 변경사항을 수신하는 코드를 작성 및 배포하고 Parse 데이터베이스에 동기화합니다.

이 시나리오에서는 이전 코드와 새 코드를 확실하게 분리하여 클라이언트를 단순하게 유지합니다. 이 시나리오의 과제는 초기 내보내기에서 대형 데이터세트를 처리하고 양방향 동기화가 무한 반복되지 않도록 하는 것입니다.

이중 작성

이 시나리오에서는 Firebase와 Parse를 둘 다 사용하는 앱의 새 버전을 작성하고 Parse Cloud Code를 사용하여 이전 클라이언트에서 발생한 변경사항을 Parse 데이터에서 Firebase 실시간 데이터베이스로 동기화합니다. 충분한 인원의 사용자가 Parse 전용 버전 앱에서 이전한 경우 이중 작성 버전에서 Parse 코드를 제거할 수 있습니다.

이 시나리오에는 서버 측 코드가 필요하지 않습니다. 단점은 액세스하지 않은 데이터는 이전되지 않는 것이고 앱의 크기가 양쪽 SDK 사용량에 따라 증가하는 것입니다.

Firebase 인증

Firebase 인증은 비밀번호 및 인기 있는 ID 제공업체(예: Google, Facebook, Twitter 등)를 사용하여 사용자를 인증할 수 있습니다. 또한 UI 라이브러리도 제공하므로 모든 플랫폼을 대상으로 앱에 전체 인증 환경을 구현하고 유지하는 데 크게 자원을 들이지 않아도 됩니다.

자세한 내용은 Firebase 인증 문서를 참조하세요.

Parse 인증과의 차이

Parse는 사용자 계정 관리에 필요한 기능을 자동으로 처리하는 PFUser라는 특수한 사용자 클래스를 제공합니다. PFUserPFObject의 서브클래스이므로, 사용자 데이터는 Parse 데이터에서 사용할 수 있고 기타 PFObject 등 추가 필드를 사용하여 확장할 수 있습니다.

FIRUser에는 별도의 프로젝트 내 사용자 데이터베이스에 저장된 고정 기본 속성 집합(고유 ID, 기본 이메일 주소, 이름, 사진 URL)이 있습니다. 이러한 속성은 사용자가 업데이트할 수 있습니다. FIRUser 객체에 직접 다른 속성을 추가할 수는 없으며 대신 추가 속성을 Firebase 실시간 데이터베이스에 저장할 수 있습니다.

다음 예시는 사용자를 등록하고 전화번호 필드를 추가하는 방법을 보여줍니다.

Parse
PFUser *user = [PFUser user];
user.username = @"my name";
user.password = @"my pass";
user.email = @"email@example.com";

// other fields can be set just like with PFObject
user[@"phone"] = @"415-392-0202";

[user signUpInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (!error) {
    // Hooray! Let them use the app now.
  } else {
    // Something went wrong
    NSString *errorString = [error userInfo][@"error"];
  }
}];
Firebase
[[FIRAuth auth] createUserWithEmail:@"email@example.com"
                           password:@"my pass"
                         completion:^(FIRUser *_Nullable user, NSError *_Nullable error) {
  if (!error) {
    FIRDatabaseReference *ref = [[FIRDatabase database] reference];
    [[[[ref child:@"users"] child:user.uid] child:@"phone"] setValue:@"415-392-0202"
  } else {
    // Something went wrong
    NSString *errorString = [error userInfo][@"error"];
  }
}];

추천 마이그레이션 전략

계정 마이그레이션

Parse에서 Firebase로 사용자 계정을 마이그레이션하려면 사용자 데이터베이스를 JSON 또는 CSV 파일로 내보낸 후 Firebase CLI의 auth:import 명령어를 사용하여 파일을 Firebase 프로젝트로 가져옵니다.

우선 Parse 콘솔이나 자체 호스팅 데이터베이스에서 사용자 데이터베이스를 내보냅니다. 예를 들어 Parse 콘솔에서 내보낸 JSON 파일은 다음과 같습니다.

{ // Username/password user
  "bcryptPassword": "$2a$10$OBp2hxB7TaYZgKyTiY48luawlTuYAU6BqzxJfpHoJMdZmjaF4HFh6",
  "email": "user@example.com",
  "username": "testuser",
  "objectId": "abcde1234",
  ...
},
{ // Facebook user
  "authData": {
    "facebook": {
      "access_token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      "expiration_date": "2017-01-02T03:04:05.006Z",
      "id": "1000000000"
    }
  },
  "username": "wXyZ987654321StUv",
  "objectId": "fghij5678",
  ...
}

그런 다음 내보낸 파일의 형식을 Firebase CLI의 요건에 맞게 변환합니다. Parse 사용자의 objectId를 Firebase 사용자의 localId로 사용합니다. 또한 Parse의 bcryptPassword 값을 base64로 인코딩하여 passwordHash 필드에 사용합니다. 예를 들면 다음과 같습니다.

{
  "users": [
    {
      "localId": "abcde1234",  // Parse objectId
      "email": "user@example.com",
      "displayName": "testuser",
      "passwordHash": "JDJhJDEwJE9CcDJoeEI3VGFZWmdLeVRpWTQ4bHVhd2xUdVlBVTZCcXp4SmZwSG9KTWRabWphRjRIRmg2",
    },
    {
      "localId": "fghij5678",  // Parse objectId
      "displayName": "wXyZ987654321StUv",
      "providerUserInfo": [
        {
          "providerId": "facebook.com",
          "rawId": "1000000000",  // Facebook ID
        }
      ]
    }
  ]
}

마지막으로 bcrypt를 해시 알고리즘으로 지정하여 변환된 파일을 Firebase CLI로 가져옵니다.

firebase auth:import account_file.json --hash-algo=BCRYPT

사용자 데이터 이전

사용자 데이터가 더 있는 경우 데이터 이전 섹션에 설명된 전략을 사용하여 이러한 추가 데이터를 Firebase 실시간 데이터베이스로 이전할 수 있습니다. 계정 마이그레이션 섹션에 설명된 흐름을 사용하여 계정을 마이그레이션하는 경우 Firebase 계정이 Parse 계정과 같은 ID를 갖게 되고 사용자 ID로 입력한 모든 관계를 쉽게 마이그레이션하고 재현할 수 있습니다.

Firebase 클라우드 메시징

Firebase 클라우드 메시징(FCM)은 메시지와 알림을 무료로 안정적으로 전송할 수 있는 크로스 플랫폼 메시징 솔루션입니다. 알림 작성기는 모바일 앱 개발자를 위해 Firebase 클라우드 메시징을 기반으로 개발된 무료 서비스로서, 타겟팅된 사용자 알림을 제공합니다.

자세한 내용은 Firebase 클라우드 메시징 문서를 참조하세요.

Parse 푸시 알림과의 차이

알림에 등록된 기기에 설치된 모든 Parse 애플리케이션에는 연결된 Installation 객체가 있으며 알림 대상을 설정하는 데 필요한 모든 데이터를 여기에 저장합니다. InstallationPFUser의 서브클래스입니다. 즉, Installation 인스턴스에 원하는 데이터를 추가할 수 있습니다.

알림 작성기는 앱, 앱 버전, 기기 언어 등의 정보에 따라 사전 정의된 사용자 세그먼트를 제공합니다. Google 애널리틱스 이벤트 및 속성을 사용하면 좀 더 복잡한 사용자 세그먼트를 만들어 잠재고객을 구축할 수 있습니다. 자세한 내용은 잠재고객 도움말 가이드를 참조하세요. 이러한 타겟팅 정보는 Firebase 실시간 데이터베이스에 표시되지 않습니다.

추천 마이그레이션 전략

기기 토큰 이전

Parse에서는 APN 기기 토큰을 사용하여 알림에 대한 설치를 타겟팅하지만 FCM은 APN 기기 토큰에 매핑된 FCM 등록 토큰을 사용합니다. Apple 앱에 FCM SDK를 추가하기만 하면 FCM 토큰을 자동으로 가져옵니다.

FCM 주제로 채널 마이그레이션

알림을 보내는 데 Parse 채널을 사용하는 경우, 같은 게시자-구독자 모델을 제공하는 FCM 주제로 이전할 수 있습니다. Parse에서 FCM으로 전환을 처리하기 위해, Parse SDK를 사용하여 Parse 채널에서 수신을 거부하고 FCM SDK를 사용하여 해당하는 FCM주제를 구독하는 새 버전의 앱을 작성할 수 있습니다.

예를 들어 사용자가 'Giants' 주제를 구독하는 경우 다음을 수행할 수 있습니다.

PFInstallation *currentInstallation = [PFInstallation currentInstallation];
[currentInstallation removeObject:@"Giants" forKey:@"channels"];
[currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
  if (succedeed) {
    [[FIRMessaging messaging] subscribeToTopic:@"/topics/Giants"];
  } else {
    // Something went wrong unsubscribing
  }
}];

이 전략을 사용하면 Parse 채널과 해당하는 FCM 주제 모두에 메시지를 보내며 이전 버전과 새 버전의 사용자를 모두 지원할 수 있습니다. 충분한 인원의 사용자가 Parse 전용 버전 앱에서 이전한 경우 이 버전을 중단하고 FCM만 사용하여 메시지를 보내기 시작할 수 있습니다.

자세한 내용은 FCM 주제 문서를 참조하세요.

Firebase 원격 구성

Firebase 원격 구성은 사용자가 앱 업데이트를 다운로드할 필요 없이 앱의 동작과 모양을 변경할 수 있는 클라우드 서비스입니다. 원격 구성을 사용할 때는 앱의 동작과 디자인을 제어하는 인앱 기본값을 만듭니다. 그런 다음 나중에 Firebase Console을 사용하여 모든 앱 사용자 또는 사용자층의 특정 세그먼트에 대한 인앱 기본값을 재정의할 수 있습니다.

Firebase 원격 구성은 여러 솔루션을 테스트하려는 경우에 이전할 때 매우 유용하고 다른 제공업체에 더 많은 클라이언트를 동적으로 이동할 수 있습니다. 예를 들어 데이터에 Firebase와 Parse를 둘 다 사용하는 앱 버전이 있는 경우 임의 백분위수 규칙을 사용하여 Firebase에서 읽을 클라이언트를 결정하고 비율을 점차 늘려 볼 수 있습니다.

Firebase 원격 구성의 자세한 내용은 원격 구성 소개를 참조하세요.

Parse 구성과의 차이

Parse 구성을 사용하여 Parse Config Dashboard에서 앱에 키-값 쌍을 추가하고 클라이언트에서 PFConfig를 가져옵니다. 가져오는 모든 PFConfig 인스턴스는 변경할 수 없습니다. 이후에 네트워크에서 새 PFConfig를 가져올 때 기존 PFConfig 인스턴스는 전혀 수정하지 않지만 새 인스턴스를 만들고 currentConfig를 통해 사용할 수 있습니다.

Firebase 원격 구성을 사용하여 Firebase Console에서 재정의할 수 있는 키-값 쌍의 인앱 기본값을 만들고 규칙과 조건을 사용하여 사용자층 세그먼트마다 다른 앱 사용자 환경을 제공할 수 있습니다. Firebase 원격 구성은 앱에 키-값 쌍을 사용할 수 있게 해주는 싱글톤 클래스를 구현합니다. 처음에 싱글톤은 앱에서 정의한 기본값을 반환합니다. 언제든지 서버에서 새로운 값 집합을 앱에 손쉽게 가져올 수 있습니다. 새 집합을 가져온 후에 새 값을 앱에 사용할 수 있도록 활성화할 시기를 선택할 수 있습니다.

추천 마이그레이션 전략

Parse 구성의 키-값 쌍을 Firebase 콘솔로 복사하여 Firebase 원격 구성으로 이전한 다음, Firebase 원격 구성을 사용하는 앱의 새 버전을 배포할 수 있습니다.

Parse 구성과 Firebase 원격 구성을 둘 다 실험하려는 경우 충분한 인원의 사용자가 Parse 전용 버전에서 이전할 때까지 두 SDK를 사용하는 새 버전의 앱을 배포할 수 있습니다.

코드 비교

Parse

[PFConfig getConfigInBackgroundWithBlock:^(PFConfig *config, NSError *error) {
  if (!error) {
    NSLog(@"Yay! Config was fetched from the server.");
  } else {
    NSLog(@"Failed to fetch. Using Cached Config.");
    config = [PFConfig currentConfig];
  }

  NSString *welcomeMessage = config[@"welcomeMessage"];
  if (!welcomeMessage) {
    NSLog(@"Falling back to default message.");
    welcomeMessage = @"Welcome!";
  }
}];

Firebase

FIRRemoteConfig remoteConfig = [FIRRemoteConfig remoteConfig];
// Set defaults from a plist file
[remoteConfig setDefaultsFromPlistFileName:@"RemoteConfigDefaults"];

[remoteConfig fetchWithCompletionHandler:^(FIRRemoteConfigFetchStatus status, NSError *error) {
  if (status == FIRRemoteConfigFetchStatusSuccess) {
    NSLog(@"Yay! Config was fetched from the server.");
    // Once the config is successfully fetched it must be activated before newly fetched
    // values are returned.
    [self.remoteConfig activateFetched];
  } else {
    NSLog(@"Failed to fetch. Using last fetched or default.");
  }
}];

// ...

// When this is called, the value of the latest fetched and activated config is returned;
// if there's none, the default value is returned.
NSString welcomeMessage = remoteConfig[@"welcomeMessage"].stringValue;