Cloud FunctionsをGoで書く。またはFirebaseのためのマイクロサービスアーキテクチャ

f:id:laiso:20191216155312j:plain

Firebase Advent Calendar 2019 の17日目です。16日目はKesin11さんの「Firebase Emulator Suiteをフル活用してTDDで開発しよう」でした。

はじめに

FirebaseプロジェクトでCloud Firestoreを利用する時は通常Node.jsによるCloud Functionsでトリガーとなる処理を記述します。その他には関連するAPIサーバー、WebアプリのフロントエンドのSSR、バックエンドの非同期処理など、多くの場面でCloud Functionsが活用されています。

この開発→デプロイサイクルをお手軽に行ってくれるのがfirebase-toolsというnpmモジュールです。JavaScriptでFunctionを実装し、firebase deployコマンドを実行するだけでFirebaseプロジェクト用のCloud Functionsが自動で登録されます。

解決したい問題

firebase-toolsは便利に使っていたのですが、私たちのプロジェクトでは日々いくつものCloud Functionsのモジュールが増えてゆく過程で以下の問題が発生しました。

  1. デプロイ対象となるソースコードの量が増え、デプロイ完了までの時間がかかるようになった(--onlyオプションを付けても遅い)
  2. SSRを含むフロントエンド向けビルド、APIサーバーとして機能するFunction、Firebase固有の処理、などの依存が単一の package.json で管理されていてモジュール更新の影響が大きかった
  3. 複数のFirebaseプロジェクトでプライベートリポジトリにある自作モジュールを共有したかった
  4. Cloud Tasksや複数Functionへの分散した水平スケールでは実現できない、リアルタイムの大量データ処理を垂直方向にスケーリングしたかった

そこで私たちはまずはじめにfirebase-toolsの仕組みやCloud Functionsがどのように動作しているのかを理解して、最終的にfirebase-toolsで構成していたFunctionのビルドを徐々にマイクロなモジュールに分割していくことにしました。

Cloud Functions for Firebaseの仕組み

Cloud Functions for Firebase*1

Cloud Functionsは - リソース - イベント - 関数名

という組合せでFunctionを管理しています。特定のリソースAに発生したイベントαにアップロードしてビルド済みのプログラムから指定の関数を実行します。

/**
 * [Google Cloud Firestore トリガー](https://cloud.google.com/functions/docs/calling/cloud-firestore?hl=ja)
 */

const Firestore = require('@google-cloud/firestore');

const firestore = new Firestore({
  projectId: process.env.GCP_PROJECT,
});

exports.makeUpperCaseOne = (data, context) => {
  const {resource} = context;
  const affectedDoc = firestore.doc(resource.split('/documents/')[1]);

  const curValue = data.value.fields.original.stringValue;
  const newValue = curValue.toUpperCase();
  console.log(`Replacing value: ${curValue} --> ${newValue}`);

  return affectedDoc.set({
    original: newValue,
  });
};

という関数を持つ index.js のみのコードを用意して

# package.json を生成する
npm init && npm i @google-cloud/firestore

gcloud functions deploy makeUpperCaseOne --runtime nodejs8 \
  --trigger-event providers/cloud.firestore/eventTypes/document.create \
  --trigger-resource "projects/$GCP_PROJECT_ID/databases/(default)/documents/messages/{docId}"

というコマンドでデプロイすると(gcloudコマンドは別途セットアップします)

projects/$GCP_PROJECT_ID/databases/(default)/documents/messages/{docId} のリソースに対して providers/cloud.firestore/eventTypes/document.create というイベントが発生した時に 関数 makeUppercaseOne() をトリガーする。

というCloud Functionsのリソースが登録されます*2

試しにコンソールからFirestoreのドキュメントを作成してみます

実行されドキュメントが更新されました。

同等の処理を行うFunctionをCloud Functions for Firebaseで実装してみることにします。

以下のようにfirebase-functionsモジュールを使ってFunctionを定義すると

const functions = require('firebase-functions');

exports.makeUppercase = functions.firestore.document('/messages/{documentId}')
    .onCreate((snap, context) => {
      const original = snap.data().original;
      console.log('Uppercasing', context.params.documentId, original);
      const uppercase = original.toUpperCase();
      return snap.ref.set({uppercase}, {merge: true});
    });

firebase deploy --only functions の内部で先程のgcloudコマンド同様のリソースができるわけです。

gcloud functions deploy makeUpperCase --runtime nodejs8 \
  --trigger-event providers/cloud.firestore/eventTypes/document.create \
  --trigger-resource "projects/$GCP_PROJECT_ID/databases/(default)/documents/messages/{docId}"

元のFunction用のコードからソースコードをそのままに1つの関数だけを抽出して、フォルダ構成としてはこのような独立したnpmパッケージとして管理できるようになります

functions/makeUppercase/
├── index.js
├── node_modules/
├── package-lock.json
└── package.json

Goランタイムへの置き換え

Cloud Functionsへ直接デプロイすることができたので、次にFunctionをGo言語で実装してCPUメモリあたりの実行速度の高速化を図ります。ボトルネックがI/Oではない典型的なデータ処理のFunctionはこれだけでスループットが向上が期待できます。

var projectID = os.Getenv("GCLOUD_PROJECT")
var client *firestore.Client

func init() {
    conf := &firebase.Config{ProjectID: projectID}

    ctx := context.Background()

    app, err := firebase.NewApp(ctx, conf)
    if err != nil {
        log.Fatalf("firebase.NewApp: %v", err)
    }

    client, err = app.Firestore(ctx)
    if err != nil {
        log.Fatalf("app.Firestore: %v", err)
    }
}

更新した値をFirestoreに書き込み更新する場合、Firestore Clientの準備をします。

init() はランタイムにより指定されているインスタンス毎の初期化処理で、Node.js版でいうグローバル変数にセットアップ済みの値を入れておく方法を似ています。

type FirestoreEvent struct {
    OldValue   FirestoreValue `json:"oldValue"`
    Value      FirestoreValue `json:"value"`
    UpdateMask struct {
        FieldPaths []string `json:"fieldPaths"`
    } `json:"updateMask"`
}

type FirestoreValue struct {
    CreateTime time.Time `json:"createTime"`
    Fields     Message    `json:"fields"`
    Name       string    `json:"name"`
    UpdateTime time.Time `json:"updateTime"`
}

type Message struct {
    Original struct {
        StringValue string `json:"stringValue"`
    } `json:"original"`
}

func MakeUpperCaseGo(ctx context.Context, e FirestoreEvent) error {
    fullPath := strings.Split(e.Value.Name, "/documents/")[1]
    pathParts := strings.Split(fullPath, "/")
    collection := pathParts[0]
    doc := strings.Join(pathParts[1:], "/")

    curValue := e.Value.Fields.Original.StringValue
    log.Printf("Uppercasing: %q", curValue)

    newValue := strings.ToUpper(curValue)
    data := map[string]string{"original": newValue}
    _, err := client.Collection(collection).Doc(doc).Set(ctx, data)
    if err != nil {
        return fmt.Errorf("Set: %v", err)
    }

    return nil
}

Function本体の処理です。Go言語の型システムの仕様上Firestore内の値のパース処理を、Node.js版でいう firebase-functions にあたるユーティリティがないためドキュメントパスの解決などを自分で実装する必要があります。

デプロイします。

gcloud functions deploy MakeUpperCaseGo --runtime go111 \
  --trigger-event providers/cloud.firestore/eventTypes/document.write \
  --trigger-resource "projects/$GCP_PROJECT_ID/databases/(default)/documents/messages/{documentId}"
func TestFunc(t *testing.T) {
    var projectID = os.Getenv("GCLOUD_PROJECT")
    jsonStr := `{
      "original": {"stringValue": "hello"}
  }`
    var message Message
    var err error
    err = json.Unmarshal([]byte(jsonStr), &message)
    if err != nil {
        log.Fatal(err)
    }
 
    value := FirestoreValue{
        Name:   "projects/" + projectID + "/databases/(default)/documents/messages/1",
        Fields: message,
    }

    result := MakeUpperCaseGo(context.Background(), FirestoreEvent{Value: value})
    if result != "HELLO" {
        t.Error()
    }
}

動作確認をGoのユニットテストとして記述できます。

写真のリサイズFunctionをGoで実装してみる

Functionのイメージ*3

ユーザーがアプリケーションから写真を登録したらシステムで必要なサイズの画像を自動で生成するようなFunctionをGoに置き換えてみます。

  1. Document更新トリガで関数を実行
  2. パースしてきたURLから画像をダウンロードしてくる
  3. リサイズを実行
  4. 画像をCloud Storageに保存

という一連の流れです

conf := &firebase.Config{ProjectID: projectID}
opt := option.WithCredentialsJSON([]byte(os.Getenv("SERVICE_ACCOUNT_JSON")))
app, err := firebase.NewApp(ctx, conf, opt)

Firebase Admin SDKの初期化時にクレデンシャルを含むJSONファイルのパスを指定するのではなく、環境変数から読み込むようにします(confも同じ形式にできるのですが、秘密情報を含まないためコードで指定しています)。

gcloud functions deploy Resizing --set-env-vars SERVICE_ACCOUNT_JSON=$(cat secretkey.json)
// var fbStorage *storage.Client
fbStorage, err = app.Storage(ctx)
if err != nil {
    log.Fatalf("app.Firestore: %v", err)
}

Cloud Storageのクライアントも init() で初期化しておきます。

type User struct {
    ProfileImageUrl struct {
        StringValue string `json:"stringValue"`
    } `json:"profile_image_url"`
}

ドキュメントの構造体をこのように定義しました。profile_image_url というキーに画像がアップロードされたURLが保存されます。

func Resizing(ctx context.Context, e FirestoreEvent) error {
    url := e.Value.Fields.ProfileImageUrl.StringValue
    cli := http.Client{}
    resp, err := cli.Get(url)
    if err != nil {
        log.Fatal(err)
    }
    src, _, err:= image.Decode(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    size := 320
    img := imaging.Resize(src, size, 0, imaging.Lanczos)
    encoded := &bytes.Buffer{}
    jpeg.Encode(encoded, &*img, nil)

    bucket, err := fbStorage.Bucket(bucketName)
    if err != nil {
        log.Fatal(err)
    }
    path := fmt.Sprintf("resized_images/%dx.jpg", size)
    obj:= bucket.Object(path)
    writer := obj.NewWriter(ctx)
    io.Copy(writer, encoded)
    defer writer.Close()

    return nil
}

Function本体です。imaging(https://github.com/disintegration/imaging )を使い 320x にリサイズしてアップロードします(別途Storageへの追加をトリガーにしてUserドキュメントにパスをセットします)

検証してみたところNode.js版での公式ドキュメントでの解説にあるようなImageMagickのconvertコマンドを外部で実行するような方法*4と比べて、ファイルに書き出しがない部分がうまく効いて大量のリサイズが一度の実行でできそうでした(リサイズ処理の品質に差がでるかもしれないので別途評価が必要です)。

デプロイ速度やFunction起動速度について

Cloud Functionsのデプロイはローカルにあるソースコードを対象として、依存モジュールが記述されている package.jsongo.mod から自動的にクラウド環境でビルドが走るようです。

firebase-tools を使ったデプロイを行っていた時は、functions/ にあるすべてのファイルが対象になり1つのFunctionのリソースへアップロードされていました(GCPコンソールからアップロード済みファイルが取得できるので確認できます)。

またFirebaseユーザーの間でFunctionの高速化テクニックとして環境変数から探索して、Node.jsの依存モジュールの動的読み込み制御する方法が知られています。*5

これらの方法と比べてアーキテクチャ的に改善する可能性はあるなと思いつつも、まだ安定性やアーキテクチャの評価中なので詳しくは比較できていない状態です。

ビルド+デプロイツールの改善

firebase-toolsを使わくなることで、複数のFunctionの依存を管理するための方法や開発やデプロイを楽にする方法を別途用意しなければいけません。

Functionを分割して複数の依存を管理するパッケージができたことで、ソースコードはmonorepoの状態になります。そのためlerna(https://lerna.js.org/ )やBazel(https://www.bazel.build/ )のようなツールが機能する環境になるかもしれません。

ただfirebase-toolsを使った環境は並行して維持できるので、段階的に移行して検討するつもりです。

GCPサービスを使ったさらなるFirebaseアプリケーションの拡張

※Firebase & Google Cloud Platform*6

Cloud FunctionsはGoランタイムの他にはPythonランタイムもあり、そちらでも同様にトリガーFunctionが書けるので何か活用法があるかもしれません。

また各FirebaseやGCPサービスには対応したREST APIが公開されていることも多く、SDK対応言語以外でもクライアントを自作して拡張することができます。

もちろんCloud Functionsですべてを行うことにこだわらずとも、HTTPリクエストをトリガーとしたAPIサーバーのFunctionや、SSRを実行して動的HTMLを返す常に待ち受けしているFunctionは、同時処理数に優れたCloud Runに移すことができそうです。

またCloud Run同士で連携して(RESTやunary gRPC)サーバー間通信でシステムを拡張の目的でも利用できそうです。

他にはCloud SchedulerとCloud Tasks、Cloud Dataflowを使ったバッチ処理。Firebase AnalyticsとCloud FirestoreをBigQueryにエクスポートして分析し、その結果をシステムに反映させるなどを私たちも既に行っています。

まとめ

このようにFirebaseはGCPの既存の仕組みを使い易くラップしたものなので、必要に応じてGCPのリソースを活用して最適化することができます。

Firebase Advent Calendar 2019 - Qiita

明日の担当はVexus2さんです。お楽しみに。

参考文献

いい感じにiOSアプリ開発プロジェクトを立ち上げる方法を考える

f:id:laiso:20160718030202j:plain
pxhere.com*1

ここ数年、新規に開発するモバイルアプリのリードになる機会が何回かあり。プロジェクトの開始時期に毎度、README.md に開発方針を記述していたのだけど、いつも似たような内容になり公共性がありそうなのでそのままブログにした。

普段から「今回はアーキテクチャは何を採用しましょうか?」みたいな段階から議論がはじまるのを避けた方が建設的だと思っているので、その思想が反映されている。

想定する状況

我が社はこれからゼロからモバイルアプリを使ったサービスを提供するところであり、人手は少ないが開発初期段階から技術に投資する意気込みはあり、いずれはサービスの大ヒット、組織の大規模化を見据えて段階的に成長するアーキテクチャをバーンとやっていきたい。

事業領域は例えば決済サービスやビジネス系のモバイルアプリを想定している。

これは何?

iOSアプリのソフトウェア設計面についての現時点での考えを未来のメンバーに共有するための文書

基本方針

「ふつうのことを普通にやる」

ふつうとは—— ふつうの〇〇プログラミングというシリーズの良書があったり、Railsコミュニティなどでコンテキストとして利用されていたりする便利なイディオム。*2

僕の解釈では問題に対して多くの開発者が共通認識で選択する解決手法を採用すること。それによって環境要因や個人の嗜好によって偏りができるのを防ぐことができる。

なぜそうしたいのかというと、先の見えない開発なので技術的に凝ったことや挑戦に労力を使うよりプロダクトのUXに大きく寄与する部分に注力するため。技術的な挑戦がUXに直接的に関わる時は別。個人やチームの成長のための技術的投資は基本的にサイドプロジェクトでやることにしてる。

プロダクトの特性上、モバイルのアプリケーションの表面上でできる体験より。バックエンドのビジネスシステムの信頼性や運用にかかる効率化が価値となると考える。 (一方TikTokやインスタみたいな没入型の体験を提供するアプリケーションの開発に従事する人たちにはまた別の価値観があるのかもしれない)

決めたこと

  • React Nativeを使わない
    • 開発対象が素朴なUIを持つツールである。ドラゴンも出てこないしアイドルのライブ配信もない
    • ユーザーがKyashやメルカリみたいなものを期待するとギャップが生じガッカリさせてしまう
    • アップデート・保守面のコストが許容できない
    • PMF検証フェーズであり、クロスプラットフォーム提供の旨みがない。アーリーアダプタもほぼiOSユーザーと予想
  • RxSwift/MVVMでがんばらない
    • 「大いなる力には大いなる責任が伴う」 (意訳: 強い人は勝手にやってくれ)
    • 経験上FRPや宣言的View+データバイディングをがんばりはじめると、受け入れる複雑さがコードの保守性や品質向上にペイしないため
    • 非同期処理の問題解決のためだけに採用されるケースもある。将来のSwiftでasync/await世代の方法論が出てきた時のために、サードパーティ依存な部分を減らしたい
    • 小さく使う方法はあるのかもしれない。RxCocoaデータバインディングだけ抜き。とか。
  • Storyboardを利用。Feature単位でファイルを切る
    • 多人数開発ではないのでコンフリクトのリスクが低い
    • 標準コンポーネントを使って、デザイナ作業が不要である程度楽に開発が進む
  • 単体テストは書く
    • 状態管理フレームワークを検討するぐらい複雑なら必須!
    • TDDはがんばらずに、脳内でシュミレートして開発を促進させそうな部分だけ書く(自己判断)
    • CIサーバーの運用とかがんばらずに最初は手元で実行できればいい
    • 一方デプロイ自動化は今後何百回もやることがわかり切っているので最初からやる
    • モック化とかも考えずにE2Eになる部分がでてしまってもよい(むしろ設計に役立つ)
    • ドメイン/プレゼンテーション分離の設計を意識するため(あとからまとめてリファクタリングするのは難しい)

迷っていること

  • サーバーサイドとのデータやりとり
    • 受信のみ。送信のみ。双方向。リアルタイム性。などの性質を加味して検討してる
    • 巨大なHTTPライブラリ: Alamofireのようなものを採用するのか。範囲を絞って小さく使うのはありかもしれない
    • またはAPIKitの仕組み: ユーザーは少ないけど筋がいい
    • REST API: protobufやswagger-codegenみたいなものである程度自動生成するのか。実践したことないのでわからない
    • gRPC: サーバーがPaaSなど制限があると検討外になりそう
    • Firestore: 最近うまくいった(レイテンシ以外は) *3 APIクライアント層を書くかわりにサーバーエンジニアがCloud Functionsを書くことになりがち
    • AppSyncやApollo: GraphQLツール群次第

Firebaseを使った成長するモバイルアプリのための高速なプロダクト開発 / Rapid Mobile Application Development using #Firebase

「Firebaseを使った成長するモバイルアプリのための高速なプロダクト開発」というプレゼンテーションのスライドを公開します。

2018年にバンコクにきて以来取り組んでいたプロジェクトについて技術的な内容をトークにしました*1。以下が概要です

モバイルアプリケーションの開発プロジェクトはあらゆる意味で速度との闘いです。

複雑化するシステムアーキテキチャやプラットフォーム、開発ツールのエコシステム。数多くある選択肢の中から私達は2018年にFirebaseを使い、1つのモバイルアプリケーションを開発しました。

本トークではその時の経験を元に、プロダクト開発を高速化するために技術者視点でどのような貢献ができるのか? という知見をシステムアーキテクトの立場としてお伝えします。

開発中はFirebaseコミュニティの情報などが非常に助けになりました。この場をお借りして感謝の御礼を申し上げます。

*1:実はDroidKaigi 2019 応募用に書いていたやつ

WEB+DB PRESS Vol.108 に「フルスタックエンジニアの憂鬱」というエッセイを書いた

いつものごとくインターネットでイキっていたところ、突然発言数0でデフォルトアイコンのTwitterアカウントから執筆依頼のDMが送られてきたので快諾しました。

でも初見では完全に信用していなくて、この話に乗っかると僕の暗号通貨ウォレットの秘密鍵がコインをチェックされてしまうやつ*1だと疑っていたので、とりあえず締切前まで放っておきました。

しかしその後の担当の人のメールの文体がいかにも編集者っぽい口調だったので本物っぽいなと思いながらやり取りしていました。万が一、入稿用のバイナリ実行ファイルが送られてきたら、たぶん話は終わっていましたけど……

どういう理由で執筆依頼が来たのかはよく理解していません。インターネットで見かけました、と聞きました。界隈の知り合いやその知り合い・他が既に執筆していたことがあるので、リファラルが効いてるのか上位互換の面子の誰かが受けられなかったのか、技術書書きたいと言ってたのを拾ってもらったのか、最近数ヶ月に1回は真面目にブログを書いていたからなのか、カンファレンスなどに出席してまともに対話できる相手だということがバレてしまったのか。

どうやったらWEB+DB PRESSに執筆できるのか?(コネか?) とよく周りで話題になっていたので、僕の結論としてはとりあえずインターネットでイキるのがよいのでは。という答えになりました。

閑話休題。エッセイは「壁の先に見えたもの」というテーマがあり、4人の著者がそれぞれフリーで2ページの文量を書いているそうです。独立した執筆なので他の人のことはよく知らないので発売当日に読むのが楽しみです。あとZOZO開発の特集も面白そう。

企画の「壁の先に見えたもの」というテーマを聞いた時の印象としては、なんかエモい感じの原稿が収集できそうな題材だなと思いました。ので素直にそれに乗っかりました。今考えると絶妙なお題だったのかもしれない。

書いた内容は要約すると、少数派な技術的志向を持つソフトウェアエンジニアでも強く生きろというもので、いつも説教臭くボヤいてるようなことです。「フルスタックエンジニアの憂鬱」というタイトルはとくに助言されたわけではなく自分でウケそうなやつを嗅覚で選びました。別に I'm フルスタックエンジニア. というわけではなくて、専門性を1つに絞れない立場である人(CTOとか)、または難儀な性格な人(自分)、を対象にしていて、詳しくは読めばわかります。短かいし。

WEB+DB PRESS Vol.108 にどうぞご期待ください。

WEB+DB PRESS Vol.108

WEB+DB PRESS Vol.108

ボツネタ

  • 詐欺師症候群について延々と説いていたらそれだけで埋まってしまったので全部消しました。Web検索してください

*1:NEM強奪犯は半年前から社員と交流を重ね、信用させたうえでウイルスを仕込んだメールを送りつけていた http://b.hatena.ne.jp/entry/s/www3.nhk.or.jp/news/html/20180512/k10011436321000.html

iOSエンジニアが知るべきProgressive Web Apps開発のエッセンス #iOSDC 2018

f:id:laiso:20180918235716p:plain

iOSDC Japan 2018 に採択されたトーク「iOSエンジニアが知るべきProgressive Web Apps開発のエッセンス」のブログ版記事です。

CfP提出後に内容は二転三転しまして、当初は最新Web開発事情について技術的に突っ込んだ内容にしようと目論んでいたのですが、「iOSエンジニア」へ「伝えたいこと」という軸で作っているうちにこの内容に落ち着きました。

当日聞きに来てくださった方、声をかけていただいた方。このような場を下さったiOSDCスタッフの方々にはたいへん感謝しています。

Introduction📛

f:id:laiso:20180918224204p:plain

昨年末 SafariにService Workersが実装され「iOSがPWAをサポートした」というニュースが業界内で話題になりました。 (※Progressive Web Apps 以降 PWA という略称で話します)

f:id:laiso:20180918224304p:plain

この時話題になっていたSafariのPWAサポートとはすなわちService WorkersのAPIの実装を指していたようです(Service Workersについては後ほど解説します)

このニュースは憶測や曲解を呼び、時には「ネイティブアプリの時代が終わった」という強い主張になって話される場面もありましたので、みなさんもどこかで目にしたことがあるかもしれません

f:id:laiso:20180918224347p:plain

状況を整理しましょう。SafariでService Workersが使えるようになったは事実です。これは既にその時点での開発計画にありましたので規定路線でした。

そして「ホーム画面に追加」でお馴染のSafariのWeb Clipの機能がWeb Manifestファイルに対応してアイコンなどを指定できるようになりました

しかし実際のアップデートはこれだけで、間違ってもAppleの誰かが「PWAに対応しました」などの声明を出したわけではありません

f:id:laiso:20180918224649p:plain

このようにPWAを取り巻く情報の解釈は複雑です。

私が思うに業界内で刺激的な話題が先行してしまったのは、Webとネイティブアプリ開発それぞれの側面で技術をただしく評価できていなかったからではないかと感じました。

ですから先ずWebアプリとネイティブアプリの開発者の目線で、Service WorkersとPWA、iOS Safariのことを理解するのが必要だと考えています。

f:id:laiso:20180918224804p:plain

このトークの前にiOSアプリ開発をしている知り合いのエンジニアたちへ「PWAという言葉を聞いてどう思うか」と質問してみました。

そこではUIやUXのクオリティやアプリストアでのファイダビリティの懸念などの意見をいただきました。

そこで気づいたのですが、PWAと名前が付けられたことによってネイティブアプリと比較して優劣を語る人の多さでした。ここの思い込みを先ず解かないといけなさそうです

f:id:laiso:20180918224911p:plain

また現状はAndroidのChrome上での体験とSafariでの体験は全く別物であると言えます。

その為、同じPWAという言葉を使って話していても、iOSユーザーとAndroidユーザーで捉えている像がそもそも噛み合わないということが起りえました

f:id:laiso:20180918225012p:plain

「PWAがネイティブアプリを置き換える」というストーリーは本質的ではありません

まず私が主張したいのがこれです。

その根拠となる理由を今からお話していこうかと思います

f:id:laiso:20180918225055p:plain

まず私がどういう立場でお話するのかというのを明確にするために、ここで自己紹介をします

私は普段スタートアップで Webエンジニアとして 仕事をしています。

そしてスマートフォンのアプリ開発も長く手がけているので、 iOS エンジニアのみなさんと近い価値観でお話できると思います

新しい技術全般が好きで、特に GUI のアプリケーション開発環境のことをよく調べています

PWAについて本質的な部分を知る

f:id:laiso:20180918225201p:plain

PWAという言葉は、広義ではWebアプリのUXを向上するムーブメント(運動)の通称です

最初はGoogle Chrome開発者らによって提唱されました

PWAに限らずGoogleは一枚岩の組織ではありませんから、Googleのどのチームの人達が言っているのかというのに注目するのがよいと思います

f:id:laiso:20180918225300p:plain

次に各プラットフォーマーにとってPWAがどのような意味を持っているのかというのに触れたいと思います。

Googleは古くはGearsやIE拡張、DartVM、Chrome Appsなどあらゆる方向からWebをよりよくする為の改善を続けています、その延長線上にPWAがあるのは不思議ではありません。

MicrosoftもWindows向けのHTMLアプリの強化を近年進めていて、先日正式にMicrosoft StoreへのPWAの登録を可能にしました。タブレットやデスクトップ向けのアプリ開発市場を活性化させたい意図が見えます。

MozillaにとってWebであることはNativeなのでWeb技術でネイティブアプリを作るのは当然のことです(ex: Firefox OS)

f:id:laiso:20180918230051p:plain

しかしAppleさん、完全にノーポジです。

WebKitにService Workersは実装されましたが、Appleは基本的にPWA関連の戦略を発表していません。

f:id:laiso:20180918230452p:plain

余談ですがiOS SDK誕生以前、アップルはサードパーティの開発者がWebアプリとして自分のアプリを公開するような構想を持っていました

これはセキュリティ上の理由による決定だったらしいのですが、開発者らの猛反発にあい現在のネイティブSDKの提供となったわけです

f:id:laiso:20180918230559p:plain

PWAとは元々、ネイティブアプリでは実現できるがWebアプリではできない体験の溝を埋めるために、ブラウザ側に実装された新機能を使うべしという側面があります。

Service Workers は「Webページ」という層より更に低レイヤー・ネイティブに近い層として処理を記述できる仕組みが用意されています。

そのため、PWA関連機能はService Workersと組み合わせて使うという例がよく出てきます。

f:id:laiso:20180918231106p:plain

では Service Workers とは何ものでしょうか? それは「ブラウザのオフライン基盤」と捉えるのがより本質的だと思います。

「プログラミング可能なネットワークプロキシ」という表現もされています。

f:id:laiso:20180918231204p:plain

この「ローカル」というのを「アプリがインストールされる」と考えるとiOSエンジニアにも理解しやすいと思います。

実際Service Workersの登録・アップデートの管理をブラウザが透過的に行ってくれる仕組みを含んでいます。

これによってWebページによるアプリの表の面とシステム上で動く「裏の面」が表現されているのです。

f:id:laiso:20180918231504p:plain

その他のブラウザの新機能(API)の例も見ていってみましょう

f:id:laiso:20180918231608p:plain

Android端末ではこのようにManifestとService Workersが定義されたWebアプリを開くとインストールを訴求するバナーが出現し、ホーム画面に追加できるようになります

一方Safariについてですが——

「ホーム画面に追加」 と Safari

f:id:laiso:20180918231654p:plain

PWA関連の記事で、まるで最新のアップデートでWebアプリのアイコンをホーム画面に追加できるようになったかの記述を見かけます

しかし、これは「Web Clip」という機能で、iPhone 3G時代から存在しました。

f:id:laiso:20180918231813p:plain

旧来、独自メタタグをHTMLヘッダに埋め込む形式の仕様だったものがService Workers実装後にWeb Manifest(JSON形式)をサポートしました。

f:id:laiso:20180918231946p:plain

このようにホーム画面に登録したページはSafariから切り離された単独のウィンドウとして管理されます(しかしcredentialの管理など既知の問題もあります)

f:id:laiso:20180918232040p:plain

次にPush Notificationです、Safariは今のところPush APIに非対応です。

デスクトップ向けにSafari Push Notificationsという独自の仕様があり、これとiOS Safari+APNsを統合しないことにはPush APIはiOSにくる未来はおそらくないのでしょう。

f:id:laiso:20180918232323p:plain

Payment - 支払い関連のAPIも最新のSafariで利用できます。

iOS SafariでもApple Payは利用できますし、この機能はうまくWeb標準とAppleの仕様が噛み合った例としてうまく機能しています。

f:id:laiso:20180918232602p:plain

つまりPWAとは、ブラウザに実装された、新機能を使うと、従来のWebアプリに不足していたUXが実現できる。

というだけの話なんです。

もしくは、WebアプリのUX向上のために新たなルートが引かれたと解釈すると良いかもしれません

f:id:laiso:20180918232749p:plain

なのでネイティブアプリと比較して優劣を語るのではなく、すべてのブラウザ共通で動作する従来のWebアプリと比較するべきなのです

Webアプリとネイティブアプリの比較

そうしたことをふまえて、次に「Webアプリとネイティブアプリの比較」です

f:id:laiso:20180918232815p:plain

Webアプリの利点として「動作環境が多用 」ということと「柔軟な配布方法」できる。ということがあげられるかと思います

Webアプリは外部アプリのWebViewの中からでも起動できますし、ブラウザさえあればどこでも動作します(そしてホーム画面からでも)

f:id:laiso:20180918232956p:plain

Webアプリの柔軟な配布方法についてです。Webアプリは常にサーバー上の最新版を参照し、Service Workersによってイントールする単位をインクリメンタルに管理することも可能になりました。

f:id:laiso:20180918233208p:plain

ここでいうオフラインファーストとは何でしょう。 それは、データを取得しなくてもUIを構成できるということです。

具体例としてはキャッシュされたアセットやレスポンスから読み込むことができます。

f:id:laiso:20180918233340p:plain

App ShellモデルというのもChromeチームが提唱したWebアプリのアーキテクチャの名前で、ネイティブアプリのメンタルメデルに近いのでiOSエンジニアには理解しやすいかと思います。

f:id:laiso:20180918233441p:plain

しかしiOS開発でオフラインファーストを意識する場面はどの程度あるでしょうか?

ネットワークのリトライ処理やキャッシュのハンドリングなどを独自に実装されている方も多いと思います。

f:id:laiso:20180918233628p:plain

WebアプリがWebであるがゆえに根本的に抱える問題というのもあります。

OSが持つ機能との接続はAPIが存在するか次第でありますし、ネイティブに見た目を近付けるほどその違和感が出てくるジレンマなどもあります。

UI遷移などはおもたる例で、ブラウザ上のレンダリングエンジンでOS権限でグラフィックAPI叩けてハードウェアに近い最適化のできるネイティブな実装に近付けるのは指南の技でしょう。

f:id:laiso:20180918234558p:plain

一方Safariの課題としては、Windows版Safari廃止以降に進んだOS独自機能の取り込みというのがあげられます

機能によってはWeb標準への準拠の動きはコンフリクトするため、うまく統廃合する必要があるでしょう

f:id:laiso:20180918234654p:plain

またiOSではChromeやFirefoxなどのサードパーティのブラウザはWebKitの利用を強制され、システムのデフォルトにできません。

ホーム画面にショートカットを置けるのもSafariだけです。

このためAPIの実装の進んだブラウザを使うという選択をとることができません

f:id:laiso:20180918234712p:plain

これは個人的な予想と願望なのですが、Appleがなぜ今頃になってWeb標準の実装を進め出したのかというと

既に世界的にシェアがあるモバイルブラウザ向けに作られたWebアプリをiOSで動かし利用者を取り込みたいのだと思います

つまり、AppleはAppleで自社デバイスのUX向上のためにSafariを改善していくのではないか? ということです

iOSエンジニアがWebアプリ技術に注目しておいた方がいい理由

f:id:laiso:20180918234740p:plain

2014年に話題になった「ロードマップ指向とエコシステム指向」というブログ記事があります。

ここでは、大きな企業が主体となって推進する技術的なトレンドが、オープンソースのコミュニティを中心とするエコシステム=生態系が技術を進歩されるということが示されています

f:id:laiso:20180918234833p:plain

エコシステム指向とはなんでしょうか? それは技術の革新を牽引する存在がプラットフォーマーなどの大きな企業から、それぞれの意思を持つ小さなコミュニティに移り変わったことだと思います

例えば、Googleのように。モバイルOSの開発をしつつブラウザ体験をよくする、しかしそれを置き換えるような新プラットフォームも開発している。という企業はそれぞれの方向性は違うが時には統合して進化している。という点が、非常にエコシステム的であるといえます

f:id:laiso:20180918234921p:plain

ここで注目したいのは現代でエコシステムを中心に急成長してきた技術としてのWeb開発環境です

ブラウザベンダーは標準化された仕様の実装でそれぞれ競争し、サードパーティのライブラリは各分野で御互いのいいところを取り込んだり、もっと良いやり方を提案したり巨大なOSSプロジェクトを戦略的に企業が運用・参加しています

f:id:laiso:20180918234953p:plain

Web技術のコミュニティではエコシステムで進化を促進する仕組みが機能しているのです。

iOSエンジニアの皆さんに近年のWebアプリ開発技術の進化の速度がすごい! というのを本日は知っていただきたいのです。

f:id:laiso:20180918235109p:plain

なぜWeb技術の進化が早くなるのか。それは、Webに関わる人口の多さが関係しているのだと思います。

ウェブブラウザやデバイスの利用環境は多様化していますし、Webアプリのフロントエンド開発技術はJavaScriptに集約されています。

f:id:laiso:20180918235125p:plain

思い起してみると今から10年前の2008年に「iOSエンジニア」という職業は存在しませんでした。

この10年、CocoaからiOS開発の環境は目紛しく変化しました。毎年OSやIDEが一新され、アプリは壊れ、プログラミング言語すら切り替わってしまいました。

AppStoreとわたしたちデベロッパーが作り出したアプリ開発のエコシステムが「iOSエンジニア」を作りだしたわけです

それと同じようなことはWeb開発の世界では今起っています。

f:id:laiso:20180918235332p:plain

AppleのロードマップによるiOS開発環境の進化を待つだけでは、もはや劇的な進化は訪れません。

私たちデベロッパーがエコシステムを乗りこなし、iOSの開発環境を進化させるべきだと思います。

例えばServer-side Swiftなどはまだまだ未成熟なので、非常にコミットする余地のある領域だと思います

f:id:laiso:20180918235359p:plain

10年後の「iOSエンジニア」を思い浮かべてみてください。Swiftで変わらずにアプリを開発しているでしょうか? それともFlutterでDartを書いているでしょうか?

10年後もiOSプラットフォームがユーザーにとって魅力的であるように…iOSエンジニアという職業を残すには、自分たちで未来を作ることが最良の選択です

そのために最新のWebアプリ開発技術を正しく理解して評価し、iOSアプリ開発に活かすのは重要なことだと思います

f:id:laiso:20180918235454p:plain

スタートアップの技術選定とアプリケーションプラットフォーム

f:id:laiso:20180819210731j:plain photo by pexels.com *1

この記事を書いたきっかけ

niconegoto.hatenadiary.jp

「PinQulをクローズします」にて事業のふりかえりをしている文章の中に「アプリビジネスは完全にダウントレンドにある」という一節があって、ここから話題が広がっていったのを機に上記の記事を読みました。そして色々思うところがあったのです。

(Twitter上で多くの共感を集めた投稿)

例えば「モバイルアプリがWebに負けはじめた理由」ではWebアプリがモバイルアプリに比べて優れているでろうという点を分析させています。

そして僕が注目したのは以下の部分について

もっとサービスレベルの話でいくと、初めからwebでやればよかったというのもミスだったと感じている部分です。

最初にアプリで出すことにこだわりすぎてしまい、プロダクトの検証が遅くなってしまいました。ライブコマースの視聴・購入体験をよくするためにアプリでなければならないと思っていましたが実際にはwebでもほとんど遜色ないものを提供することができました。

PinQulをクローズします - niconegoto Blog

PinQul のこれまでの軌跡をみると、2017年10月にiOS版リリース。2018年04月にAndroid。5月にWeb版。という情報がありました。

つまりiOS版をリリース後に少なくとも半年はAndroid版やWeb版を開発しつつ、iOSアプリの方向性を変えPMF(プロダクト・マーケット・フィット)を探っていたことになる。 その期間を3プラットフォーム並行開発するよりWeb版一本でフォーカスしたかったな、という後悔が出てしまうのは共感できた。なぜなら我々も同じような状況で悩み、現在モバイルアプリを開発しているからです。

そんなPinQulのIF...を元に、改めてスタートアップが新規プロジェクトなモバイルアプリを今0から作りはじめるとき、技術的な観点でどの道具を選択するのか、というのを考えてみたかった。

新規プロジェクトというのは俗に言うスタートアップでなくても1企業の新規事業部が0から作り出すソフトウェア製品のことでもいいです。

2012年のビーンボール

最初にPinQulの記事を読んだ時に2012年のマーク・ザッカーバーグのインタビューから起ったネイティブ/Web論争を思い出してしまいました。

これは当時Firebug作者でありiOS向けフレームワークthree20開発者でもあるJoe Hewittを中心に開発されたネイティブ実装で評判もそこそこ良かったFacebookの公式iOSアプリを、ある時に全面的にWebベースのハイブリッドアプリに置き換えようとした所、パフォーマンス問題などが顕著になりネイティブアプリに作り替えた時のものです。

マーク・ザッカーバーグのこのきわどい発言は、その場では投資家向けの弁明も含むもので、補足もされたのですが。様々な反論や憶測を呼び、業界を騒がせることになりました。

その後FacebookはPHPの魔改造Javascript拡張からReactを生み出し、2度3度ネイティブアプリ開発への移植に挑戦した後にReact Nativeを軌道に乗せているわけで、先の弁どうりWeb技術を諦めていない。というのはガチであったことが伺えます。

www.publickey1.jp

この一件は、PinQul とFacebook のビジネスのフェーズが全く異なるにも関わらずダブって見えます。おそらく2012年時点で「Webアプリはダウントレンドで、我々はネイティブアプリにフォーカスする必要がある」という空気が業界を襲ったことへの揺り戻しを感じさせるからでしょう。

当時の時代背景を知る手段として「Webはクソ。ブラウザはマジなんとかしろ」の記事をあげます。ここでは進化の遅く足並みの揃わないWebアプリ開発プラットフォームはネイティブアプリに比べて遥かに劣っているという意見が述べられています。

blog.mirakui.com

いま、Webは重要な転換期の中にいると私は思う。Webはデスクトップアプリケーションと同等になるのに10年以上かかってるし、しかもイマイチだ。そしてネイティブアプリが加速していく中でWebは絶滅の危機に瀕している。

PosterousのCEO「Webはクソ。ブラウザはマジなんとかしろ」 - 昼メシ物語

——そして話は現代に立ち戻ります

モバイルWebファーストと貧者のツール

「貧者のツール(道具、技術)」とは、より少ないリソースしか消費できない環境で、なんとか道具を工夫して製品開発を達成するための考え方の通称で、「人ナイ物ナイ金ナイ」のスタートアップ向きであると言えます。

この貧者のツールとして、スタートアップが新規プロジェクトにモバイルWebファーストを採用する利点としては以下があげられると思います。

チームのエンジニアや開発するコードベースを最小にして高速に開発できる

通常のアプリ開発プロジェクトではWebバックエンド/iOSアプリ/Androidアプリ/フロントエンド。など技術やプラットフォームによって開発担当者をアサインすることが多いかと思います。

モバイルWebファーストにすることで、iOSアプリやAndroidアプリの開発に1-2人ぶんのリソースが取られることはなくなります。またWebアプリ開発者はバックエンド開発の片手間にフロントエンド開発も身に付けている人材が多いので、API→アプリクライアントサイドのコミュニケーションも短縮できる可能性があります。

高速に仮説検証できる

変更する対象コードが少なくデプロイも早い。担当する開発者間のコミュニケーションも省ける。となると高速にプロダクトを仮説検証できる 、というのが想像できると思います。

留意すべき点

しかしモバイルWebファーストの良い面の裏に、思わぬ落し穴が隠されているケースもあります

高速な開発を実現する為に選んだモバイルWebファーストが逆に開発の遅延を招く可能性も考慮すべきです。以下はその一例です

  • Webアプリの方が実現が難しい機能
  • デバイスに近いAPIを活用するアプリ
  • ネイティブアプリで楽に実装できるパターンがよく知られている機能

例えば「QRコード決済アプリ」。これを僕が最初に想像した時はMediaDevices系のインターフェイスでWebでもサクっと実現できそうだなー、という印象でした。

しかし現実の QRCode Scanner のデモコードを見るとC言語のライブラリをJavaScriptに変換し、バックグラウンドでカメラをオーバーレイしたcanvasから抜き出した画像データを送りつける、という過激な実装で実現されています。

またデバイスに近いAPI、OSの機能を呼び出して実装するアプリはウェブで自力実装しづらいだけでなく、実現不可能=詰み状態が訪れる危険性もあり、注意が必要です。

その機能要件はネイティブアプリのが実現しやすいのか? Webで充分に開発できそうか? というのは結局両プラットフォームに精通した視点が要ります。貧者の組織にそのような人物の助けを借りることができるか? というのは怪しかと思います。

PinQulの開発が開始されたであろう2017年の春頃にライブECの要件(明日にはガラっと変わるかもしれない!)はWebアプリで充分に満たせるはずだ! と0ベースの状態で断言できるエンジニアはいないと思います。

スタートアップはスピードが命ですから、もしそれをやるにはプロジェクトの開発が決定する前からアイデアの技術的な検証を普段から行っていないとできません。

Komercoの技術選定

Backend as a Service (BaaS)を活用してサーバーサイド開発の工数を減らし、徹底的に仮説検証して高速に新規サービスを開発するぞ! という考え方があります。

理想的なプロジェクトではバックエンド開発に携わる工数を大幅に削減でき、貧者のツール的であると言えます。

この思想でFirebaseをフル活用して作られたサービスに、クックパッド新規事業「Komerco」というアプリがあります。

jp.techcrunch.com

Komercoは2018年6月にリリースされ、現在はiOSアプリしか提供されていません。

Komerco開発チームがこのままAndroidアプリやWeb版を開発するつもりなのか、iOSだけでサービスを検証してゆくのかは注目どころです。

※ 同じ手法で開発された「Cookin'」というプロジェクトはiOSアプリだけをリリースした後に、7ヶ月でサービスが終了しました*2

螺旋の原則

「技術選定の審美眼 」という名スピーチがあります。

ソフトウェア業界の技術トレンドは、一見振り子のように同じ場所に戻ってくるように見えるが、螺旋状に変化している。螺旋を作り出す技術要素の差分と、変化の中でも変らない抽象的な考え方に注目することの重要さが説かれています。

www.publickey1.jp

この螺旋の原則でいうと。モバイルシフト、アプリファーストの後にWeb回帰というストーリーに中に以下のような差分が潜んでいるのだと思います

  • アプリがコモデティ化して、ネイティブアプリであること自体の競争力が弱くなった
  • PCのWebブラウザからモバイルのブラウザへ主戦場が移った
  • ブラウザやフロントエンド開発環境が劇的に進化した

例えばフリルやメルカリが世にリリースされたのは奇しくもFacebook HTML5ショックの2012-13年です。この時期にモバイルアプリファーストの開発に注力しているサービスは少なかったと記憶しています(そしてフリルはTitanium Mobileによるハイブリッドアプリ技術を選択し、メルカリはネイティブアプリSDK開発を選びました)。

多くのサービスは先ずデスクトップ向けサイトがケータイ対応し、それを元にスマホ対応→アプリリリース。と開発うるのが通例だったと思います。

この時期にネイティブアプリのUXを高めることにすべてを投資したプロダクトは現在も伸びています。

はたして2018年にモバイルWebファーストで開始するサービスはどうなるでしょうか、非常に気になるところです。

私たちのケース

締め括りに身の上話をさせていただくと、先日私たちもPinQulと同じような状況でネイティブアプリ開発に舵を切りました。

その時は直前までWebアプリ以外にもReact NativeやFlutterでの開発を考えて綿密にプロトタイプ開発をして準備を行っていましたが、スタートアップにありがちな「ある日状況が変わった」というやつが発生しました。

その時の意思決定の理由は以下です

  • 一番手を動かしてコミットしてくれそうな新入社員のエンジニアがネイティブアプリ開発が得意だった
  • それをサポートするエンジニア(僕)がだいたい何でも対応できた
  • AWSからGCPに切り替えることができ、Firebase(BaaS)を全面採用できた
  • 開発対象の要件がだいたい想像できた

そこから学んだことは以下です

  • 仮説検証やPMFは1プラットフォームで行った方が良い
  • 選択するプラットフォームは人的要素で決める
  • 「隣の芝生は青い」を乗り越えるにはプロトタイプを作り続けるのが重要

P.S. 現実のプロジェクトとして実際に手を動かし、そのふりかえりを共有してくださったPinQulチームと井手さんに心から感謝します

iOSDC Japan 2018 に laisoから 1名 が登壇いたします

f:id:laiso:20180808124405p:plain

iOSDC Japan 2018

iOSエンジニアが知るべきProgressive Web Apps開発のエッセンス」と題しまして、PWAムーブメント以降のWebアプリ開発のお話しをさせていただきます。

10年ビジネスとしてiPhoneアプリ開発を、個人の楽しみとしてウェブ開発とクロスプラットフォームのフレームワーク研究を嗜んできた私が現在関心を持っているのがこの最新のWebアプリ開発手法についてです。

おそらく以下のような内容になるかと存じます

「PWAとは?」でウェブ検索してキャッチできないような技術者的観点からレビューしたPWA論

マーケット、ビジネス的、組織的観点のPWA評はおそらく期待できません。ウェブに情報が溢れているので自身で見てもらった方が良いと思います。

クロスプラットフォームツールとして見たWeb開発

近年最も高速に進化しているGUI開発環境であるWebアプリのフロントエンド開発について、私見を交えiOS開発者にとって参考になるような内容にします。

抽象的で観念的なGUI開発の蘊蓄

好きなので。

複数のプラットフォームに精通するとGUI開発を多面的に捉えることができ、そこから発見できる自己理論を持ち出すことができます。

以下は本トークの雰囲気に近そうな窪塚洋介氏のインタビューです。