Cookpad の新規事業と Firebase

国内事業開発部 iOS エンジニアの三浦です。私は17年新卒で入社したのですが、それ以来複数の新規事業の開発に携わってきました。 現在開発中のアプリでは、バックエンドに Firebase を用いた開発を進めています。 この記事ではなぜ Firebase を使っているのかと、そこで得られた知見についてまとめようと思います。

なぜ Firebase

みなさんご存知かと思いますが、Cookpad のレシピサービスでは主にバックエンドに AWS と Ruby on Rails が使われています。 なぜ新規事業ではその構成ではなく Firebase を使うのかということですが、以下のような理由があります。

基盤サービスが豊富

Firebase には RealtimeDatabase、FireStore といった Database を始めとして、CloudMessaging(Push通知基盤)、Authentication(認証基盤)といった開発のためのツールがあります。 これらの機能はサービス開発において大抵必要不可欠なものですが、サービスリリースまでの間ではメインの機能に時間を取られ、あまり時間を割くことができない部分になります。

これらの基盤部分が開発開始時から品質が担保された上で提供されていることで、本来のサービス開発に時間をかけることができ、開発スピード、アプリケーションの品質を高くすることができます。 また作っては壊しといったことを繰り返すリリース前の段階での開発においては、修正の範囲がクライアントのみで済むため非常にコストが低くアプリケーションの改修を行うことができます。

実際にどれくらい楽に実装できるか、簡単な iOS でのサンプルコードを載せます。

認証

ユーザーモデルを定義して Firebase に Facebook ログインするサンプルは以下のように書くことができます。 FireStore のモデルフレームワークとして Pring を利用し、モデルでは facebook のユーザーIDと名前をプロパティに持つとします。

// User model
import Pring

class Firebase { }
extension Firebase {
    class User: Object {
        @objc dynamic var name: String?
        @objc dynamic var facebookUserID: String?
    }
}

コントローラーから sign in するときにはこのような処理で実現できます。 実際は ViewModel や Helper を利用して処理を分割するのですが、サンプルなので1つのメソッドで処理を完結させています。

import UIKit
import Firebase
import FacebookLogin
import FacebookCore

class SignInViewController: UIViewController {
    private func signInWithFacebook() {
        let loginManager = LoginManager()
        // Facebook へログイン
        loginManager.logIn(readPermissions: [.email, .publicProfile, .userFriends], viewController: self) { result in
            switch result {
            case .success(_, _, let token):
                // Facebook からユーザー情報を取得
                GraphRequest(graphPath: "me").start { (response, result) in
                    switch result {
                    case .success(let response):
                        let userID: String = response.dictionaryValue!["id"] as! String
                        let username: String = response.dictionaryValue!["name"] as! String
                        let credential = FacebookAuthProvider
                            .credential(withAccessToken: token.authenticationToken)
                        // Firebase への認証
                        Auth.auth().signIn(with: credential) { (user, error) in
                            if let error = error {
                                // error handling
                                return
                            }
                            let user = Firebase.User()
                            user.name = username
                            user.facebookUserID = userID
                            // User モデルを FireStore に save
                            user.save { (_, error) in
                                if let error = error {
                                    // error handling
                                    return
                                }
                                // 認証成功
                            }
                        }
                    case .failed:
                        // error handling
                        break
                    }
                }
            case .cancelled: // Facebook へのログインがキャンセルされた
                break
            case .failed: // Facebook へのログインが失敗した
                break
            }
        }
    }
}

電話番号や、Twitter、メールアドレスとパスワードによる認証に関しても同じような処理で実装をすることができます。

通知

次は Firebase Cloud Messaging を利用して Push 通知を受け取れるようにします。

User の Model に Token を管理できるようにようにプロパティを増やし、現在のログインユーザーを取得するメソッドを追加します。

// User model
import Pring

class Firebase { }
extension Firebase {
    class User: Object {
        typealias DeviceID = String
        typealias Token = String

        @objc dynamic var name: String?
        @objc dynamic var facebookUserID: String?
        // DeviceID をキー、FCMToken をバリューに持つ Dictionary
        @objc dynamic var fcmTokens: [DeviceID: Token] = [:]

        static func current(_ completion: @escaping (Firebase.User?) -> Void) {
            guard let authUser = Auth.auth().currentUser else {
                return completion(nil)
            }
            self.get(authUser.uid) { user, _ in
                guard let user = user else {
                    completion(nil)
                    return
                }
                completion(user)
            }
        }
    }
}

あとは通知を登録したいところで Tsuchi(https://github.com/miup/Tsuchi) いう私の開発したライブラリを利用して Token を取得することができるので、 その Token をユーザーに DeviceID と共に保存します。

import Tsuchi

func saveFCMToken(_ token: String, completion: (()-> Void)?) {
    Firebase.User.current { user in
        guard let user = user else { return }
        user.fcmTokens[UIDevice.current.identifierForVendor!.uuidString] = token
        user.update { _ in completion?() }
    }
}

Tsuchi.shared.didRefreshRegistrationTokenActionBlock = { token in
    saveFCMToken(token)
}
Tsuchi.shared.register { granted in
    if !granted {
        // ユーザーが登録を拒否
        return
    }
}

通知を受け取ったときの処理に関しても Tsuchi を利用して以下のように書くことができます。

import Tsuchi

// payload の型
struct FCMNotificationPayload: PushNotificationPayload {
    var aps: APS?
    // custom payload data
}

// payload の型と通知受取時の処理を渡して push 通知を subscribe する
Tsuchi.shared.subscribe(FCMNotificationPayload.self) { result in
    switch result {
    case .success(let notification):
        let (payload, _) = notification
        print(payload)
    case .failure(let error):
        // error handling
        break
    }
}

// ログアウトなどで通知の受取を終了するとき
Tsuchi.unregister {
    Firebase.User.current { user in
        guard let user = user else { return }
        _ = user.fcmTokens.removeValueForKey(UIDevice.current.identifierForVendor!.uuidString)
        user.update()
    }
}

Growth もしっかりしている

リリース後に関しても Analytics や Crash Report が用意されていること、 ユーザーの行動を元に機械学習でユーザーのセグメント分けをしてくれる Prediction 機能、さらにそれを利用してABテストを行う事もできるなど、サービスのグロースに関する部分でもかなり強力なツールが揃っているため、将来的にも有用だろうということで技術選定をしました。

外部サービスとの連携

ここまで Firebase の利点についてお話してきましたが、当然 Firebase では用意されていないサービスも多くあります。

例えば現在開発しているサービスで必要なものだと、全文検索や決済機能などがあります。 それらの機能はすでに外部 SaaS が用意されているため、私たちは図のように Firebase の FaaS (Function as a Service) である CloudFunctions を利用してそれらのサービスとの連携を行っています。

f:id:MiuP:20180209102151p:plain

CloudFunctions では DB への変更をトリガーにして関数を発火することができるため、変更のあったオブジェクトから外部サービスへ渡すデータを構成し渡すだけの必要最低限の実装で外部サービスとの連携が行なえます。 実際にどのように連携を実装しているかという部分に関しては自分が Firebase.Yebisu #1 での登壇でまとめてありますので、こちらの記事も一緒に読んでいただければ詳しい部分についても理解できると思います。

Firebase コミュニティへのコミット

Firebase をフルでバックエンドに置くサービスは世界的にもあまり多くはありません。そのため開発において、壁に当たることも多くあります。私達のチームではそれらの問題と解決法に関して外部にアウトプットしていくことを積極的に行っており、Firebase.Yebisu といったイベントや、サンプルコードでもいくつか登場しましたが、ライブラリを OSS として Github 上で公開するなど、コミュニティへの貢献も進めています。 実際に私達のチームのメンバーが開発した Firebase 関連のライブラリは以下です。

  • Salada (Realtime Database model framework)
  • Pring (Cloud Firestore model framework)
  • Tsuchi (Firebase Cloud Messaging helper)
  • Lobster (RemoteConfig helper)

もし Firebase を利用した開発をする場合は、よろしければ一度使用してみてください。

まとめ

AWS + Ruby on Rails の会社だと思われがちな Cookpad ですが、社内外向けを問わず新規アプリケーションではサービス毎に特徴を考慮し様々なフレームワーク、言語を用いた開発が行われています。 先程も述べましたが Firebase をフルで利用しているサービスは業界でもそれほど多くはないと思いますので、今後もいろいろな形で経過を報告していけたらと思います。