production.log

株式会社リブセンスでエンジニアをやっている星直史のブログです。

転職ドラフトで11年ぶりの転職が決まりました - 転職ドラフト体験記 -

概要

先日退職エントリを書いたのですが、転職活動は転職ドラフトを中心に動いていました。

job-draft.jp

結果から先に述べると、転職ドラフトを使用することで、多くの企業から指名を受け、希望年収に届き、希望する仕事内容を提示いただいた企業に転職することができました。
この記事では、今回の転職活動において、転職ドラフトというサービスに限定した体験記という位置付けで書こうと思います。

こちらの記事は転職ドラフトエージェント体験談投稿キャンペーンに参加しています。

転職ドラフトの概要と流れ

概要

転職ドラフトは、転職者(エンジニア)が書くレジュメを元に、企業が仕事内容と共に年収も提示する競争入札型の転職サービスです。
また、どの企業が誰にいくらの年収を提示したかという情報も分かるので、

  1. どんな会社がどういう人にいくらで何人指名しているのか
  2. どういう経歴を持った人がいくらで指名されているのか
  3. 自分の年収の相場はいくらか

ということがはっきりと分かるサービスです。

転職者としては上記の3.が一番の関心だと思いますが、1.の情報に加えて企業のIRやエンジニアの情報発信を見ると「その企業のイケイケ具合」や「その企業の抱えてる問題感やフェーズ」も読み取ることできることや、2.の情報から自分と他者との差分を測ることもできるので、様々な意味で透明性が高いサービスだと思いました。

参加の流れ

大まかな流れは以下の通りです。

  1. レジュメを書く
  2. 審査申請する
  3. 審査チームからレジュメのフィードバックが来たら1に戻る。審査に通過したら4へ。
  4. 企業からの指名期間が開始し、指名される
  5. 指名に応じて面談 / 面接に入る
  6. 何度か面接を繰り返し内定!!

1. レジュメを書く

企業はレジュメを見て任せたい仕事内容と年収を提示し指名をするので、転職ドラフトにおいて一番の肝となるステップだと言えます。
先述の通り、私は11年分のキャリアの棚卸しとなったため、まとめるのに6~8時間くらいかかり、非常に骨の折れる作業でした。
まっさらな状態からレジュメを書くと「何を書いたらいいの?」となるのですが、転職ドラフト側でTIPSとして、以下のような書き方を推奨しています。

レジュメに何を書けばいいか?
レジュメに何を書けばいいか?

また、自分が書き手として意識したことはSTARです*1
STARとは、Situation(状況)、Task(課題)、Action(行動)、Results(結果)の頭文字を取ったもので、具体例を含めた簡潔でわかりやすい内容にするために効果的な方法です。
このSTARを採用した理由は、面接時の質問に対する回答としてこの形式で答えることが多いだろうと想定していたため、あらかじめレジュメに記載しておくことで双方の理解度を上げ切った状態で面接に取り組めると判断したためです。

基本的には上記の型に従って経歴を書いていくことが"キホンのキ"となると思います。
また、私の場合は

  • 特定の企業での在籍期間が長い
  • 在籍中にメンバー、リーダー、マネージャー、部長、CTOと役割が変わってきた

上記2点が他の方と比較して特異な点であることが明白であったため、フォーマットを以下のようにアレンジしました。

# 役割 (例 メンバー期20xx年~20yy年 
## 会社、開発部、自分の背景
## 重要な取り組みn
### 取り組みの名称
#### 背景
#### 問題と課題
#### 具体的な取り組み
#### 結果

上記の手法をとったためか、レジュメ審査は1度で通過したことや、様々な方に「レジュメが読みやすかった」と評価されたのは良かったです。

4. 企業からの指名期間が開始

当初、指名期間が開始されたらすぐに指名が入るだろうと思っていたのですがそんなことはなく、大半は最終日に指名されました。
体感的には指名の90%は最終日に集中していました。

私自身、転職ドラフトに企業側として参加していたのですが、他社の指名状況を踏まえて、自社の指名をしたいという思いが少なからずありました。
というのも、

  • 他社の指名より低い年収を提示したあとに、再提示しにくい
  • 社内メンバーと比較した場合に提示できる年収の上限がほぼ決まるものの、どうしても来てほしい候補者さんの場合はその限りではない
    • 90%ルールを行使すれば理論上可能だが、行使した場合の候補者さんの温度感が低くなってしまう恐れがある。
    • であれば、自分達が提示できうる年収のギリギリを攻めたい
  • 上記の相談はできれば役員や人事に事前に相談したいが、レジュメを見ただけでその根拠や確信を持って説得することが難しい

など、様々な理由がありました。
そのため、他社の指名状況を参考に指名したいというケースは少なくはなく、これが最終日に指名が集中する理由の1つだと思います。

そのため、転職ドラフトに初参加される方は、指名期間開始直後に指名が入らないからといって過度に不安になることはないかと思います。 また、ダッシュボードに指名された平均年収と、n件指名が入った人数のグラフが表示されるのですが、他人の指名状況を知っても何もメリットはないどころか、他者と比較してしまい、焦ってしまう可能性もあるため、閲覧したくない方は注意が必要です。

5. 企業から指名される

指名された企業がどのような企業かについて説明します。 転職ドラフトに登録している企業は大雑把に以下の通りです。

  • Web界隈の事業会社
  • 投資ラウンドのシリーズC,Dを終えた企業と上場企業が半々

PMF*2と資金調達を終えたタイミングで採用強化して事業を加速していこう!!という状況の会社が多い印象で、人数は30名以上100名未満の企業が多い印象でした。 また、上場済みの企業も多く、上場を牽引したサービスのさらなるグロース、PPM*3の一環で新規事業のテックリード、CTO/VPoE候補募集などなど、多種多様でした。 その逆に、ラウンドB以前のスタートアップと呼ばれるような小規模な会社は少ない印象でした。

転職ドラフト エージェントについて

先述の通り、転職ドラフトは競争入札型の転職サービスがメインですが、それと共にエージェントサービスがありました。

agent.job-draft.jp

私は当初、他のエージェントを利用しようと考えていたのですが、転職ドラフトでの経歴書の作成で燃え尽きてしまい、他のエージェントで同じ内容の経歴書を作成する気力がなくなってしまったため、転職ドラフトのエージェントサービス(以下、エージェントと表記)を利用することにしました。

エージェントと並行した転職ドラフトのルールは以下の通りです。

  1. エージェントよりも先に転職ドラフト上で企業とコミュニケーションをした場合は、エージェントはその企業に対してノータッチとなる(自分で予定調整などする必要がある)。
  2. その逆に、エージェントから先に企業を紹介された場合はエージェント経由でやりとりが行える
  3. エージェント経由の場合、一次面接通過後に年収提示される(もちろん先に募集要項が確認できるため、年収レンジは大まかに把握できる)。

要は「どちらが先に企業とファーストタッチを行なったか?」によって分岐します。
後述しますが、エージェントを使った方が何かと楽であるため、以下の手順を迅速に行うと転職ドラフトというサービスを有意義に使えると思います。

  1. 転職ドラフトのn月回の募集が始まったらすぐにエントリー
  2. 経歴書を仕上げる & 審査に通る
  3. 企業からの指名期間よりかなり余裕を持って、エージェントサービスに申し込む
  4. エージェントから紹介されなかったけど、気になる企業 & 指名を受けた企業は自分で調整して進める

※3に期間的な余裕がない場合は、直近の開催は経歴書の作成までにしておき、次回開催で3からスタートするのも有効だと思います。

これは他の(ダイレクトリクルーティング + エージェントのハイブリット型)転職サイトでも言えます。
というのも転職サービスに登録している企業の母集団が同一だとした場合、企業利用状況は以下の4つとなるためです。

  1. アクティブにダイレクトリクルーティングをしていて、エージェントも利用している企業
  2. アクティブにダイレクトリクルーティングをしているが、エージェントは利用していない企業
  3. アクティブにダイレクトリクルーティングをしていないが、エージェントを利用している企業
  4. アクティブにダイレクトリクルーティングをしていないし、エージェントも利用していない企業

そのため、採用候補者としてエージェントを利用することで得られるメリットは以下の通り整理されます。

  • 3の企業を漏れなく転職対象企業にすることができる
  • 1の企業はエージェント経由でやりとりができるので楽ができる
  • 4の企業はどうしても出会えないことが判明する(機会損失しているのでは?という不安から逃れることができる)

故に、自力で頑張るのは2の企業、もしくはエージェントから紹介されなかった企業だけど魅力的に見えた企業に限定されるため、大きな網を広げながら最小限のエネルギーで企業と接触することができました。

ここが転職ドラフト エージェントのメリット

その他実際にエージェントを利用して具体的に良かったポイントは以下の通りです。

  • 企業との日程調整の手間がなくなる
  • 面談 / 面接実施の事前に企業の様々な情報を伝えてもらえる(どんな人が面接担当者か、面接ではどのようなポイントを重要視して見ているか、面接にあたり目を通しておくと良い媒体)
  • 接触した企業への所感をエージェントに伝える中で、自身の転職軸や思考の整理になったり、エージェントからアドバイスがもらえる
  • 転職における相談相手となるため、孤独な戦いを避けることができる。

私が実際に大きくメリットだと感じたのは、面談 / 面接前に事前情報をもらえたことです。
特に、面接ではどのようなポイントを重要視して見ているかを知れることです。
自分の意見を曲げてでも面接に通りたいという思いは微塵もない*4のですが、お互いのマッチ度合いを迅速に確認するにあたって、事前に的確な答えを想定できたことが良かったです。
また、面接担当者のプロフィール、SNS、登壇実績、紹介記事を参考にどのような志向の人なのか、何が好きなのか推測をして、面談前のアイスブレークに話題を用意しておき、場を温めることを再現性を持って行えたのも良かったです。

ここが転職ドラフト エージェントの あと一歩!

エージェントを利用して良かったポイントもある一方、あと一歩頑張ってほしい!!と思う点もありました。
それは、エージェントとは基本的にメールでやりとりすることです。

今回エージェントさんと初めてやりとりしてから転職先が決まるまで、約2ヶ月前後かかりました。
7社前後を並行して進めていたため、1つのメールの中に複数企業の情報が記載され、かつ、そのメールに返信していくことで12往復、15往復するメールもありました。
この状態だと「A社の事前情報が知りたいんだけど、どのメールの何番目のレスだ...」と探す時間が多少なりとも生じてしまいました。
少なくとも、面談や選考に進むようになった企業の場合、企業ごとにメールとその件名を分けて送信するとSlackにおけるスレッドのような感覚で使うことができるのではないか?と思いました。*5

とはいえ、上記はエージェント利用におけるデメリットではなく、あくまで「こうだったらより良いのになぁ」と感じた部分であるため、エージェントを使わない理由にはなりえなさそうです。

転職ドラフトに参加した結果

私の転職ドラフトでの結果は以下通りです。

  • 転職ドラフトエージェント経由
    • 紹介企業: 14社
    • カジュアル面接: 7社
    • 一次面接: 3社
    • 二次面接以降: 3社
    • 最終: 2社
    • 内定: 2社
  • 転職ドラフト
    • 指名: 48社
    • カジュアル面接: 3社
    • 一次面接: 0社
    • 二次面接以降: 0社
    • 最終: 0社
    • 内定: 0社

前述の通り、私はエージェントを先行して進めていました。 また、エージェントに紹介された企業で大勢が決したため、転職ドラフトで多数の指名を受けたものの、指名は辞退することにしました*6

その他、諸条件についても十分に満足に行く結果や、評価をいただけたため、11年ぶりの転職は大満足の結果となりました。 後ほど、転職ドラフトを含めた転職活動全体を包括した転職活動の模様を書いていきたいと思います。 以上、私の転職ドラフト体験記でした。

*1:オレの名字がSTARだ!!

*2:Product Market Fit

*3:Product Portfolio Management

*4:その時点で企業とフィットしないため

*5:事業上様々な制約があることは理解しており、熟考した結果現在の形になっているとは思います!

*6:貴重なお時間を割いていただいたにもかかわらず、このような形となってしまい、誠に申し訳ございません

AWS Elasticsearch Service(AES)のデータノードが消失しStatusがRedになった場合の対応方法

概要

info.snapmart.jp
私が運用しているサービスの一つであるSnapmartで障害が発生しました。
原因はAmazon Elasticsearch Service(以下AES)のデータノードが消失しStatusがRedになったためです。
Elasticsearchのデータノードが消失しているので、検索周りで多大な影響が出てしまいました。
この記事では上記の障害を振り返り、類似の問題が発生した場合のトラブルシューティングの一助としたいと思います。

Elasticsearchの基本概念

まずはざっくりElasticsearchの構成で出てくる用語を整理しましょう!
今回の障害の登場人物は以下の通りです。

  • クラスタ
  • ノード
  • インデックス
  • ドキュメント
  • シャード&レプリカ

詳しい解説は本家ドキュメントとクラスメソッドさんの解説に譲ります。

www.elastic.co
dev.classmethod.jp

障害内容

タイトルの通りAESのデータノードが消失しStatusがRedになったのですが、具体的には以下の問題が発生しました。

日本時間3月8日10時20分:

  1. 特定のノードに問題が発生。このノードは検索システムで使用しているインデックスのプライマリシャードが割り当てられていた。
  2. 1のノード(プライマリシャード)とそのレプリカがノードに割り当てられず、クラスターヘルスがRedに

検索システムのプライマリシャードに問題が発生してしまったため、以下のような不具合が発生しました。

  • (1/ノード数)%の確率で読み込み/書き込みが失敗
  • (1/ノード数)%のデータが消失

対応内容

プライマリシャードが障害により消失してしまったため、復旧しなければなりません。
また、StatusがRedになった場合は、まずは問題を取り除き、ステータスGreenに戻す必要があります。
無闇にクラスタに変更を加えてProcessing状態にすると復旧不可になる可能性があります。

docs.aws.amazon.com

ドメインを再設定する前にAmazon ES、赤のクラスター状態を解決することです。赤のクラスター状態のドメインを再設定すると、問題が複雑化し、状態を解決するまで、設定状態が [Processing (処理中)] のままドメインがスタックする可能性があります。

StatusがRedのインデックスを特定

該当クラスタにアクセスできる環境から以下のコマンドを実行し、インデックスの状態を確認します。

$ curl -X GET http://<endpoint>/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open index1 p2mhaaaCA4uId7GEZzN-AQ 5 1 12345 0 1.2mb 3.4kb
red open index2 GLwW836AOm61FOQp6jIgre 5 1 67890 0 6.7mb 5.6kb

出力結果を確認し、healthがredになっているものが問題が発生しているインデックスです。

スナップショットの確認

消えてしまったデータはスナップショットから復元するしかありません。

以下のドキュメントを参考に復元をしていきます。

docs.aws.amazon.com

スナップショットの情報は以下のコマンドで確認します。
まずはリポジトリから確認します。

$ curl -XGET 'elasticsearch-domain-endpoint/_snapshot?pretty'
{
  "cs-automated" : {
    "type" : "s3"
  }
}

今回問題となったクラスタでは自動でスナップショットを取る設定をしていました。 そのため1時間ごとに新たなスナップショットが作成されていきます。
1時間ごとのスナップショットの情報を確認します。

$ curl -XGET 'elasticsearch-domain-endpoint/_snapshot/{スナップショットの一覧が見たいリポジトリ名}/_all?pretty'
...
, {
    "snapshot" : "スナップショット名-001",
    ...
    "indices" : [ "インデックス一覧" ],
    "state" : "SUCCESS",
    "start_time" : "2021-03-08T00:52:06.288Z",
    "start_time_in_millis" : 1615164726288,
    "end_time" : "2021-03-08T00:52:39.672Z",
    "end_time_in_millis" : 1615164759672,
    "duration_in_millis" : 33384,
    "failures" : [ ],
    "shards" : {
      "total" : 36,
      "failed" : 0,
      "successful" : 36
    }
  }, {
    "snapshot" : "スナップショット名-002",
    ...
    "indices" : [ "インデックス一覧" ],
    "state" : "FAILED",
    "reason" : "Indices don't have primary shards [問題のインデックス名]",
    "start_time" : "2021-03-08T01:51:56.473Z",
    "start_time_in_millis" : 1615168316473,
    "end_time" : "2021-03-08T01:51:57.678Z",
    "end_time_in_millis" : 1615168317678,
    "duration_in_millis" : 1205,
    "failures" : [ {
      "index" : "問題のインデックス名",
      "index_uuid" : "問題のインデックスのuuid",
      "shard_id" : 1,
      "reason" : "primary shard is not allocated",
      "status" : "INTERNAL_SERVER_ERROR"
    } ],
    "shards" : {
      "total" : 36,
      "failed" : 1,
      "successful" : 35
    }
  },
...

上記コマンドで、以下のことがわかります。

  • 問題のあったインデックス名とその原因
  • 復元対象のスナップショット名
  • スナップショットから復元後、どの時点からドキュメントを復旧するか

スナップショットから復元

スナップショットから復元するにはまず、既存の問題のあるインデックスを削除する必要があります(なぜなら、リストアするコマンドを実行すると同一のインデックスを作成しようとしてしまうため、インデックス名が重複してエラーになるためです)。
特定のインデックスのみ削除するコマンドは以下の通りです。

$ curl -XDELETE 'elasticsearch-domain-endpoint/{削除対象のインデックス名}'

続いて、スナップショットを指定して、インデックスを復元します。

$ curl -XPOST 'elasticsearch-domain-endpoint/_snapshot/{スナップショットのリポジトリ名}/{スナップショット名}/_restore' -d '{"indices": "問題となったインデックス名"}' -H 'Content-Type: application/json'

これで2021-03-08T00:52:06.288Zまでのデータが復元できます。 2021-03-08T00:52:06.288Zから先のデータについては、自前で復旧する必要があります。

また、AESでは追加料金なしで自動でスナップショットを取得します。自動スナップショットは1時間ごとにスナップショットを取得し、14日間保持されます。 そのため、インデックスが壊れたとしても、14日までに復元できれば被害を少なくすることができます。 14日間以上保持したい場合や、クラスター間でのデータの移行は手動でスナップショットを取得する必要があります。 今回は自動スナップショットに助けられました...。

注意点

ドキュメントを参考にした一連の流れは、インデックス削除~スナップショット復元まで、検索機能が失われます(インデックスに登録されているドキュメントが消えるため)。
そのため、巨大な検索データの場合は、インデックス削除もスナップショット復元も時間がかかると思うので、スナップショットから復元時に別名で復元し、それをアプリケーションから参照するようにすると良いと思います。

まとめ

Elasticsearchのプライマリシャードがいきなり消失したので非常に驚きました。
今回は「データが消えた場合の対応」であるため、基本に忠実に以下を行います。

  1. 問題となったインデックスの特定
  2. スナップショットの取得状況の確認
  3. 問題が発生前のスナップショットの特定
  4. スナップショットからデータ復元

データ復旧でのポイントは2点です。「スナップショットを取得しているか、復元できるスナップショットは存在するか」です。

今回がAESの自動スナップショット機能に助けられました。
バックアップは重要ですね...!

VPCにあるAWS Elasticsearch ServiceのPrivate IPをALBに追加するシェルスクリプト

概要

Snapmartの検索サーバーはAmazon Elasticsearch Service(以下ESS)で運用しているのですが、Kibanaを使ってデータ分析をしたくなりました。

ESSはVPC内にあるため、アプリケーションサーバーからElasticsearch APIで検索実行は問題なく行えますが、 管理者PCからKibanaのエンドポイントへのアクセスはできません。

そのため、ESSの前段にALBを配置してアクセスする必要があります。 設定は以下の記事を参考にしました。 dev.classmethod.jp

しかし、

ESSのPrivate IPは可変なので、定期的にALBのターゲットとなるPrivate IPをメンテナンスする必要があります

と記載があるため、この処理を行うシェルスクリプトを書きました。

シェルスクリプト

#!/bin/sh
ALB_ARN=arn:aws:elasticloadbalancing:**

# ALBに登録されているIPを全て削除
aws elbv2 describe-target-health \
--target-group-arn $ALB_ARN \
--query 'TargetHealthDescriptions[].Target.Id' \
--output json \
| grep -o '".*"' \
|awk '{ print "aws elbv2 deregister-targets --target-group-arn '"$ALB_ARN"' --targets Id="$1"" }' \
|sh

# ESSのPrivateIPをALBに登録
aws ec2 describe-network-interfaces \
--filters "Name=status,Values=in-use" \
--query 'NetworkInterfaces[?Description==`ES ドメイン名`].PrivateIpAddress' \
--output json \
| grep -o '".*"' \
|awk '{ print "aws elbv2 register-targets --target-group-arn '"$ALB_ARN"' --targets Id="$1"" }' \
|sh

解説

ALBに登録されているIPを全て削除

現在ALBに登録されているターゲットのIDを全て取得するAWS CLIコマンドを実行し、その結果を使ってALBからターゲットを削除していきます。 現在ALBに登録されているターゲットのIDを全て取得するAWS CLIコマンドはaws elbv2 describe-target-healthです。 ALBからターゲットを削除するためには、IDが必要になるので、--queryオプションでIDを取得します。

aws elbv2 describe-target-health \
--target-group-arn $ALB_ARN \
--query 'TargetHealthDescriptions[].Target.Id' \
--output json

この部分が、ALBからターゲットを削除するAWS CLIコマンドです。 aws elbv2 register-targetsを使用します。

|awk '{ print "aws elbv2 register-targets --target-group-arn '"$ALB_ARN"' --targets Id="$1"" }' \

ESSのPrivateIPをALBに登録

現在ESSがVPC内で使用しているPrivateIPを取得します。AWSマネジメントコンソール-EC2-Network interfacesの画面で確認できる項目なので、AWS CLIはaws ec2 describe-network-interfacesを使用します。
絞り込み条件はStatusがin-useのPrivateIPでとします。 また、ESSで使用しているPrivateIPはDescriptionが"ES ドメイン名"となるため、--queryオプションでそれを指定しつつ、PrivateIpAddressを取得します。

aws ec2 describe-network-interfaces \
--filters "Name=status,Values=in-use" \
--query 'NetworkInterfaces[?Description==`ES ドメイン名`].PrivateIpAddress' \
--output json

上記のIPをALBに登録します。

|awk '{ print "aws elbv2 register-targets --target-group-arn '"$ALB_ARN"' --targets Id="$1"" }' \

これをcronなどで定期実行することで、手作業がなくなります。

Managed workflowのExpoアプリに導入するE2Eテストフレームワークの比較と導入

概要

運用しているアプリの規模が大きくなってくると、ちょっとした修正が思わぬところに影響してしまったり、ツールのバージョンアップをする際に組織全体を巻き込んだ大規模な手動テストを行わなければならないことがしばしばあります。

自分が運用しているSnapmartアプリも例外ではありません。 SnapmartアプリはManaged workflowのExpoアプリであり、ejectする予定はありません。 今回はManaged workflowのExpoアプリに導入するE2Eテストフレームワークの比較と導入についての記事を書きます。

比較

React NativeにおけるE2Eテストフレームワークは以下の3つです。

  1. Appium
  2. Detox
  3. Cavy

SnapmartアプリをManaged workflowのExpoアプリとして運用していくことにこだわる理由は大きく2つです。

  1. iOSやAndroidに知見があるエンジニアが社内に少ない
  2. そもそもエンジニアが少ない

面倒な処理は極力Expoに押しつけ、iOS/Androidからは距離を置くことで、主戦場であるWebで相撲を取るという選択をしています。

そのためツール選ぶ際の制約として「expo ejectコマンドを避ける」が出てきます。
それを踏まえて考えてみます。

Appium

github.com

AppiumはSeleniumを使用して、クロスプラットフォームテストフレームワークです。 Seleniumを使ったことがあるエンジニアの方は多いのではないかと思います。

しかし、iOS/Android共にビルドしなければならず、Expo Clientで動かしながら開発が進められなさそうなので、断念しました。 これについて、ハックもできるようですが情報が少なすぎるので避けました。

Testing Your React Native App With Expo & Appium | by Colin Wren | The Startup | Aug, 2020 | Medium

Detox

github.com DetoxはWiが開発しているツールです。 Detoxの特徴の一つとしてFlakyなテストに対しての対策をツールが用意していることです。 Flakyなテストとは、例えば非同期処理や処理速度の問題で、テスト実行のたびに成功/失敗が分かれてしまうテストです。 Detoxはこれにたいして、内部で非同期処理を検知し、終了まで待機することで、回避しているようです。

当初Expoもhelperを用意していたようですが、2018年から開発が止まっています。 また、DetoxもExpoアプリのサポートをしていないと明言しています。

試しにdetox-expo-helpersを使って動かしてみましたが、そもそも64bitのSimulatorで動作しなかったので、断念しました。

Cavy

github.com CavyはReactのRefsを使用してテストを行うフレームワークです。 *1 テストランナーはjestに限定されます。

純粋なJavaScriptで書かれているので、yarn addをすればManaged workflowのExpoアプリでも導入することができます。 消去法になりましたが、Cavyに決定です。

Cavyの導入

基本的にはGettingStartedに書かれている内容と同じです。 ダイジェストでどうぞ!

cavy.app

前提

$ node -v 
v12.18.1

$ expo -V
3.27.4

$ cavy -V
1.1.0

インストール

$ yarn global add cavy-cli
$ yarn add cavy --dev #TypeScriptを使用している場合は、@types/cavyも追加
$ cavy init

設定

Cavyの登場人物は、大きく3つです。

  1. RootComponentをラップするTesterコンポーネント
  2. テスト対象(コンポーネント)
  3. テストコード

Cavyの動作

Cavyはcavy-cliを使用してテストを実行すると、以下のスクリプトが走ります。

$ cp App.js App.notest.js
$ mv App.test.js App.js

そして、テストが実行された後に、元に戻す処理が走ります。

$ cp App.js App.test.js
$ cp App.notest.js App.js
$ rm App.notest.js

1. RootComponentをラップするTesterコンポーネント

CavyはApp.jsをApp.test.jsで置き換えた後にテストを実行するため、まずはテストを動かすApp.test.jsを作ります。

import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { StyleSheet, Text, View, TextInput } from 'react-native';
+++ import TestableScene from './src/TestableScene';
+++ import { AppRegistry } from 'react-native';
+++ import { Tester, TestHookStore } from 'cavy';
+++ import ExampleSpec from './specs/exampleSpec';

+++ const testHookStore = new TestHookStore();

export default () => {
  return (
+++     <Tester specs={[ExampleSpec]} store={testHookStore}>
      <View style={styles.container}>
+++         <TestableScene />
        <Text>Open up App.js to start working on your app!</Text>
        <StatusBar style="auto" />
      </View>
+++     </Tester>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
+++ AppRegistry.registerComponent('cavyTest', () => AppWrapper);

2. テスト対象(コンポーネント)

次に、テスト対象のコンポーネント(例としてTestableSceneを用意しました)を修正します。

import React from 'react';
import { View, TextInput } from 'react-native';
import { hook } from 'cavy';

class Scene extends React.Component {
  render() {
    return (
      <View>
        <TextInput
          ref={this.props.generateTestHook('Scene.TextInput')}
          onChangeText={console.log("")}
        />
      </View>      
    );
  }
}

const TestableScene = hook(Scene);
export default TestableScene;

Cavyの特徴は、ReactのRefsを使用すると先に述べましたが、ここで登場です。
CavyはgenerateTestHookを使用して、TestHookStoreにRefsを渡すことで、テスト時にTextInputコンポーネントに対してテストを行える様にしています。

また、generateTestHookの引数として渡した文字列が、テスト実行時の対象コンポーネントの識別子となります。

上記のコードでは、hook()を使用しましたが、この他にも、React Hookを使用する方法や、refを割り当てることができない関数(Textコンポーネントとか)をwrap()でアプローチすることもできます。

cavy.app

3. テストコード

最後にテストコードです。

export default function(spec) {

  spec.describe('Logging in', function() {

    spec.it('works', async function() {
      await spec.exists("Scene.TextInput");
    });
  });
}

これはシンプルですね。 CavyがTest Helpersを用意しているので、わかりやすくなっています。

テスト実行

テスト実行はExpo Clientで行えます。

expo start --clearでSimulatorを立ち上げます。

その後、cavy run-ios --skipbuild --file App.jsを実行します。 ポイントとしては、--skipbuildオプションと--file App.jsでエントリーポイントを指定することです。 --skipbuildがなければ、ReactNativeのbuild用の処理が走ってしまうため、スキップをします。 --fileオプションは、Expoを使用している場合は、エントリーポイントがApp.jsとなっているため、Cavyのデフォルトであるindex.jsから変更するために指定します。

テスト結果はこの様に出力されます。 f:id:watasihasitujidesu:20200914211933p:plain

まとめ

AppiumとDetoxはReactNativeアプリのE2Eテストフレームワークの候補として挙がりましたが、Managed workflowのExpoアプリに導入するE2EテストフレームワークはCavy一択となりました。 Cavyはテスト実行がやや恐ろしいですが、簡単導入することができました。 ただ、用意されているTest Helpersがやや貧弱な印象*2を受けたので、少しお試しで使ってみようと思います。

*1:あと可愛いです

*2:例えば、スクリーンショットが使えないとか

Expo SDK v36.0.0で追加されたMediaLibraryのお気に入り写真の読み込みが遅い問題と回避策

概要

React Native 0.61からCameraRollが削除されました。
そのため、Expo SDK v36.0.0以降でExpoマネージドアプリを作成している場合は、MediaLibraryを使用して写真を読み込まなければならなくなりました。
CameraRolleでは、「お気に入り」がアルバムの一つとして識別されていたため、.getPhotosのオプションとして"Favorites"を渡せば一度で取得できました。
しかし、MediaLibraryは「お気に入り」はassetsの属性としてしか取得できないため、全ての写真を読み込まなければ「お気に入り」写真を全て表示することができません。
この処理が非常に遅いので、この記事ではExpo SDK v36.0.0で追加されたMediaLibraryのお気に入り写真の読み込みが遅い問題と回避策について、書こうと思います。

MediaLibraryの仕様

MediaLibrary - Expo Documentation

MediaLibraryの全件取得はMediaLibrary. getAssetsAsync()で行えます。
上記の返り値のassets(assetの集合)をループで回し、MediaLibrary.getAssetInfoAsync(asset)を実行します。
このMediaLibrary.getAssetInfoAsync(asset)の返り値にisFavoriteが含まれています。

問題点

await MediaLibrary.getAssetInfoAsync(asset)が異様に遅いです。

上記をコードに表すとざっくり以下のようになります。

const assets = await MediaLibrary.getAssetsAsync(options).assets
for (const asset of mediaLibrary.assets) {
  const assetInfo = await MediaLibrary.getAssetInfoAsync(asset) // <= ここの応答速度が問題
  if(assetInfo.isFavorite){
    // お気に入りの場合の処理
  }
}

具体的には、await MediaLibrary.getAssetInfoAsync(asset)の呼び出しに750ms ~ 1,000ms程かかります

回避策

Promise.allを使用する

const assets = mediaLibrary.assets.map((asset) => {
  return MediaLibrary.getAssetInfoAsync(asset)
  });
});
await Promise.all(assets).then(function(assetInfos) { 
  assetInfos.forEach((assetInfo, key) => {
    // お気に入りの場合の処理
  });
});

MediaLibrary.getAssetInfoAsync(asset)を並列で実行しようと試みるも、iPhone XSでは全てのUIスレッドをPromiseの処理に使用されてしまいます。端的に言うと固まります
そのため、戻るボタンの操作、画面遷移も全くできない状態が続く上、端末がめちゃくちゃ熱くなります*1 スマホではこの処理は向かないため断念せざるを得ません...。

hasNextPageとendCursor-afterを使用する

MediaLibrary.getAssetsAsync()の返り値にhasNextPageとendCursorが含まれます。
これは以下の役割があります。

  • hasNextPage: 取得するアセットが他にあるかどうか。(totalCount >= assets.lengthであるか)
  • endCursor: 最後に取得したアセットのID。afterオプションにこの値を渡すと、直後のアセットが取得できる。

要は、hasNextPageがtrueの間、endCursorを使用したページングをhasNextPageがfalseになるまで使用することができます。
これはwhile文とforEach文で実装が行えるため、forEach文の終わりにユーザーに何らか変化が行ったことを知らせることができます。

例えば以下のような処理のイメージです。

let mediaLibrary = await MediaLibrary.getAssetsAsync(options)
while (mediaLibrary.hasNextPage) {
for (const asset of mediaLibrary.assets) {
  let assetInfo = await MediaLibrary.getAssetInfoAsync(asset)
  if(assetInfo.isFavorite){
    // お気に入りの場合の処理
  }
}
Alert.alert("ユーザーに対しての何かしらの処理")
mediaLibrary = await MediaLibrary.getAssetsAsync({...options, after: mediaLibrary.endCursor})
}

処理開始から終了までの時間は、当初と変わりありません。 しかし、画像表示処理を行う場合は、1スクロールで表示される画像には限りがあるため、ある小さな塊内で表示できるものがあれば、すぐに返し、それを繰り返すことで、ユーザーが「遅延している」と感じさせないようにすることも可能であるはずです。

キャッシュを使用する

assetInfo.isFavoriteがTrueとなったアセットのIDをAsyncStorageで格納する方法です。 2回目に表示する場合に、Keyが存在すれば、それを使用することで、表示速度を速めることができます。 しかし、懸念点は以下の通りです。

  1. お気に入りから削除された場合に対応できない
  2. 古い写真をお気に入りに追加された場合に対応できない

新しく追加された写真かつ、お気に入りに追加された写真しか対応できません。
そのため、速度面以外でのお問い合わせが増えてしまう可能性があります。

お気に入りから写真を選択することをやめる

CameraRolleからMediaLibraryの切り替えによって、お気に入りからの写真選択以外は全て高速化されます。*2 また、写真アプリでお気に入りに追加する手間と、アルバムに追加する手間はさほど変わりません。
ただ、ユーザーが「アルバムを作成し、そこ写真を入れることができる」というiOSの機能を認知していない可能性があります。(社内比) その点は、お問い合わせが増加する可能性もあります。

ただ、

  • お気に入りから選択ができなくなったことを明示
  • 代替案として、アルバムを作成し、そこに追加することができる操作方法を示したページへのリンクを設置し、ユーザーに認知させる
  • 上記を事前告知

といった、導線を確保することで、お問い合わせは増加を未然に防げることは可能なのではないかと思います。(私は一人では決めきれませんでした)

結論

まずは、現時点で最も良いUXを提供できるであろう、「hasNextPageとendCursor-afterを使用」し、実装を行うのが、現時点で最良の選択だと思います。
また、「キャッシュ使用する」実装をした場合のお問い合わせ(=懸念点を実行するユーザー数)がどれだけ増加するかは未知数であるため、段階的リリースなどで、様子をみつつ、リリースチャレンジをするのも検討できるではないかと思います。

*1:熱くなってきたぜ🔥💪🔥💪🔥

*2:Expoバージョンアップ時の記事を参照

Snapmartアプリで使用しているReact Navigationをv4からv5にアップグレードした際の対応

概要

2020年2月にReact Navigationのv5がリリースされました。
React Navigation 5.0 - A new way to navigate | React Navigation

Snapmartアプリでは、React Navigation v4を使用しています。
React Navigation v4では、Stack NavigatorにJavaScriptを使用していましたが、ネイティブのような感覚とパフォーマンスが低下する場合がありました。
React Navigation v5では、Native navigation primitivesを使用するStack Navigatorに変更することで改善がされました。

Stack Navigatorは多くの画面遷移で使用するため、このタイミングでアップグレードすることにしました。

今回は、Snapmartアプリで使用しているReact Navigationをv4からv5にアップグレードした際の対応についてまとめます。

手順

React Navigationはアップグレードガイドが非常に丁寧に書かれています。

Upgrading from 4.x | React Navigation

このリストの中で、実際に使用している処理については対応をしていく必要があります。 対応したことは以下の通りです。

  1. パッケージ名の変更
  2. createAppContainerからNavigationContainerに変更
  3. コンポーネント化されたN Navigatorに変更
  4. navigation propから分割されたroute propを使用
  5. navigationOptions を各Screenで設定するように修正
  6. Navigation eventsの変更
  7. ディープリンクの記述修正
  8. CommonActionsの変更

1. パッケージ名の変更

各パッケージ名が変更されたので、記載されている内容にしたがって変更をしていきます。

Upgrading from 4.x | React Navigation

若干ハマった点としては、パッケージ名を変更して起動をするとこのようなエラーが発生しました。

I'm getting "SyntaxError in @react-navigation/xxx/xxx.tsx" or "SyntaxError: /xxx/@react-navigation/xxx/xxx.tsx: Unexpected token"

というエラーが表示されたので、以下のコマンドで対応します。

$ rm -rf node_modules
$ rm yarn.lock
$ yarn

2. createAppContainerからNavigationContainerに変更

こちらは単純にcreateAppContainerからNavigationContainerに変更します。
Upgrading from 4.x | React Navigation

3. コンポーネント化されたN Navigatorに変更

React Navigation v5の大きな変更として、コンポーネント化があります。

React Navigation v4では以下のような記述でした。

const RootStack = createStackNavigator(
  {
    Home: {
      screen: HomeScreen,
      navigationOptions: { title: 'My app' },
    },
    Profile: {
      screen: ProfileScreen,
      params: { user: 'me' },
    },
  },
  {
    initialRouteName: 'Home',
    defaultNavigationOptions: {
      gestureEnabled: false,
    },
  }
);

React Navigtaion v5からは以下のような書き方になります。

const Stack = createStackNavigator();

function RootStack() {
  return (
    <Stack.Navigator
      initialRouteName="Home"
      screenOptions={{ gestureEnabled: false }}
    >
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{ title: 'My app' }}
      />
      <Stack.Screen
        name="Profile"
        component={ProfileScreen}
        initialParams={{ user: 'me' }}
      />
    </Stack.Navigator>
  );
}

変更を自動化できなさそうなので、粛々と修正していきました。

4. navigation propから分割されたroute propを使用

React Navigation v5からは、navigation propからroute propが切り出されました。 影響を受けた点としては、画面遷移時に渡すparameterがroute propから取得しなければならなくなったことです。

- this.props.navigation.state.params.id
+ this.props.route.params.id

こちらの対応は、sedで一括置換で対応しました。

find src/ -type f -print0 | xargs -0 sed -i -e "s/navigation\.state/route/g"

5. navigationOptions を各Screenで設定するように修正

navigationOptionsもv4では各画面で設定していましたが、v5からはScreenコンポーネントで設定します。
Upgrading from 4.x | React Navigation

こんな感じです。

v4

class ProfileScreen extends React.Component {
  static navigationOptions = {
    headerShown: false,
  };

  render() {
    // ...
  }
}

v5

<Stack.Screen
  name="Profile"
  component={ProfileScreen}
  options={{ headerShown: false }}
/>

これも自動で変換はできなさそうなので、手で粛々と直します。

6. Navigation eventsの変更

Upgrading from 4.x | React Navigation

v4では、Navigation evenetが4種類ありました。

  • willFocus
  • didFocus
  • willBlur
  • didBlur

v5ではシンプルにfocusblurだけになります。これは、v4におけるwillFocuswillBlurです。

また、書き方も変わります。 Class Componentにおいてはこのようになります。

+ componentDidMount() { 
+   const unsubscribe = this.props.navigation.addListener("focus", e => {
+     // 処理
+   });
+ }
render() {
    return (
      <View style={styles.container}>
-        <NavigationEvents onWillFocus={payload => { console.log("onWillFocus") }} />

7. ディープリンクの記述修正

Deep linking | React Navigation
ディープリンキングは、pathのハンドリングが変わります。

import { Linking } from 'expo';
import { NavigationContainer, useLinking } from "@react-navigation/native"; 

const prefix = Linking.makeUrl('/');

function App() {
  const ref = React.useRef();

  const { getInitialState } = useLinking(ref, {
    prefixes: [prefix],
    config: {
      HomeStack: {
        path: 'stack',
        screens: {
          Home: 'home',
          Profile: 'user',
        },
      },
      Settings: 'settings',
    }
  });

  const [isReady, setIsReady] = React.useState(false);
  const [initialState, setInitialState] = React.useState();

  React.useEffect(() => {
    getInitialState()
      .catch(() => {})
      .then(state => {
        if (state !== undefined) {
          setInitialState(state);
        }

        setIsReady(true);
      });
  }, [getInitialState]);

  if (!isReady) {
    return null;
  }

  return (
    <NavigationContainer initialState={initialState} ref={ref}>
      {/* content */}
    </NavigationContainer>
  );
}

refを定義し、NavigationContaineruseLinkingに渡します。 また、pathの定義は、useLinkingconfigで画面を紐づけを行います。

上記の例は、HomeStackとSettingsの2つのタブがあり、 HomeStackタブには、HomeとProfileという2つのスクリーンが存在するイメージです。

この定義だとmyapp://homeでHomeスクリーンにリンクすることができます。

8. CommonActionsの変更

CommonActions reference | React Navigation
最後に、CommonActionにも変更がありました。
resetを使っていた箇所があったので、修正をします。

+ import { CommonActions } from '@react-navigation/native';
...
-    let resetAction = StackActions.reset({
-      index: 0,
-      actions: [NavigationActions.navigate({ routeName: "Account" })],
-    });
-    this.props.navigation.dispatch(resetAction);
+    this.props.navigation.dispatch(
+      CommonActions.reset({
+        index: 0,
+        routes: [{ name: "Account" }],
+      })
+    )

まとめ

対応は以上です。

  1. パッケージ名の変更
  2. createAppContainerからNavigationContainerに変更
  3. コンポーネント化されたN Navigatorに変更
  4. navigation propから分割されたroute propを使用
  5. navigationOptions を各Screenで設定するように修正
  6. Navigation eventsの変更
  7. ディープリンクの記述修正
  8. CommonActionsの変更

Snapmartは78画面で構成されるアプリですが、手作業の部分が多く、骨が折れました...。
特に、コンポーネント化されたN Navigatorに変更とnavigationOptions を各Screenで設定するように修正する作業は、全ての画面を開いて修正しなけばならなかったのが大変でした。

SnapmartアプリのExpo SDKのバージョンをv35.0.0からv37.0.0にアップグレードで対応したこと

概要

4月1日にExpo SDK v37.0.0がリリースされました。
Expo SDK 37 is now available - Exposition

自分が開発に携わっているSnapmartアプリではExpo SDK v35.0.0を使用していました。
Expo SDK v36.0.0がリリースされた際は、Snapmartアプリにとって、アップグレードを行う強いモチベーションがなかったのですが、Expo SDK v37.0.0では、以下の機能が追加されたため、いち早く導入するためにアップグレードを行いました。

  1. Firebase Analyticsが使用可能になった
  2. SMS認証が使用可能になった

これまで、Expoマネージドのアプリ(ejectをしていないアプリ)では、アプリの解析に使用できるサービスはAmplitudeに限定されていました。
新興サービスであるため、情報が非常に少ないことや、安定性(=サービスの継続性)も非常に不安でした。
しかし、Expoマネージドアプリを運用する限りは、Amplitudeを使うしか道がない。という状態です。
今回のExpo SDKのアップデートでFirebase Analyticsを使用可能になったため、筆者はあえてAmplitudeを選択する理由はないと思い、既存のバージョン v35.0.0からv37.0.0のアップグレードを行うことにしました。

この記事ではSnapmartアプリのExpo SDKのバージョンをv35.0.からv37.0.0にアップグレードで対応したことを書き記したいと思います。

大まかな対応手順

対応は大まかに以下の手順にしたがって行います。

  1. Expoから提供されているexpo upgradeコマンドで、機械的にアップグレードを任せる
  2. 修正箇所を把握するために、changelogを確認し、Breaking changesを確認 & 修正を行う
  3. Expo Clientをアップグレードして動作確認
  4. 動作確認中に発生したエラーと警告を修正

ざっくりこんな感じです。作業の多くは、2, 4です。以下に説明を書いていきます。

Breaking changesを確認 & 修正

今回はExpo SDK v35.0.0 => v37.0.0のアップグレードとメジャーバージョン2つ分確認する必要があります。
影響の範囲を狭くしたほうが、問題の切り分けをスムーズに行えるため、まずはv35.0.0からv36.0.0と一つずつバージョンアップすることを強くオススメします。

Breaking changesを確認すると、Snapmartアプリで影響を受けそうな変更は以下でした。 *1

  • CameraRoll: Removed CameraRoll from react-native core, developers are encouraged to use expo-media-library instead

これは「react-native coreからCameraRollから削除されたため、expo-media-libraryせよ」という内容です。
Snapmartアプリではカメラから写真を選択し、アップロードする機能があり、これにCameraRoll.getPhotosを使用していました。

CameraRollからMediaLibraryへ変更

CameraRollからMediaLibraryへ変更する際、よかったことは、MediaLibraryはCameraRollの不具合が修正されていたことと、機能的に良くなっている点です。
具体的には、以下の通りです。

  • ファイルの種類の絞り混みで不具合が発生しない
  • アルバム名で絞り込みが正常に動作するようになった(不具合の修正)
  • 写真のソートができるようになった
  • アルバムの一覧を取得できるようになった

特にアルバムの一覧を取得できるようになったのは、かつて全ての素材をforEachでブン回してアルバム名を取得していたため、大幅な改善だと思います。

一方、MediaLibraryの唯一最大の難点は、写真の詳細情報をMediaLibrary.getAssetInfoAsync(asset)を使用しなければならないことです。
もちろんhasNextPageとendCursorを使用すれば、少しずつ取得できるため、オーバーヘッドは少なくなるはずなので、問題ありませんが、
全件取得し、全件詳細が必要な場合は、処理速度が問題になります。

動作確認中に発生したエラーと警告を修正

Breaking changesを使用したあとは実際に動作確認をしてみます。ここで発生したエラーと警告を修正していきます。*2

具体的に遭遇したエラーと警告で大きなものは「React 17で廃止されるunsafe lifecycle methodsの移行」です。 これは、React 16.9から警告が出るようになったため、このタイミングで対応することにしました。
もちろん、まずはバージョンアップしたアプリをリリースし、この警告は別途対応するという方針も有りだと思います。

手順は以下の通りです。

  1. 自前で書いているunsafe lifecycleを修正する
  2. インストールしているパッケージのライブラリのバージョンからlatestのバージョンまでのリリースノートに問題が修正されている確認
  3. 2で修正されている場合は、対応されているバージョン以上にupgradeする

今回は、警告が出た全てのパッケージがunsafe lifecycleの対応がされていたので、非常に助かりました。改めて、コントリビューターが多いOSSを選択することの重要さを感じました。

まとめ

今回は、SnapmartアプリのExpo SDKのバージョンをv35.0.0からv37.0.0にアップグレードで対応したことをまとめました。 大きくは、以下の対応を行いました。

  1. Breaking changesの修正
  2. React 17で廃止されるunsafe lifecycle methodsの修正

修正しなければならない箇所が比較的少なかったので対応期間も短くで済んだのは良かったです。 反省点としては、メジャーバージョンを一つ飛ばしてアップグレード対応をしたことです。
今回はそれによる影響が少なかったから良かったものの、問題の切り分けなどが難しくなりそうであるため、バージョンアップ対応は一段階ずつ行うのが良いと思います。

*1:以外と少なくてよかったです

*2:厳密には警告であるため修正する必要は必ずしもありませんが、多くは「次のメジャーバージョンで削除予定だよ」という類が多いので、今のうちに修正しておくことをオススメします

今日最終出社日を迎えたインターン生の前で泣かずに済んだ

概要

今日は、ピクスタの開発部サポート(開発部の総務的なチーム)のインターン生の最終出社日だった。
そのインターン生は、何の変哲もない大学生であったが、弊社に多大な貢献をし、最終出社を終えた。
今回は、そのインターン生が何故そのような成果が出せたのか考察を記す。

素直さと内省習慣

上司からの評価、指摘、期待していることなどを素直に受け止め、自分の改善点として真摯に受け止めて、取り組んでいた。
また、常に「どうすればより良くできるか?」を考え、自分なりの解を導き、改善点と同様に意識的に取り組んでいた。
最初は、インターン生からアクションを聞いたときに「一体何の意味がある活動なのか?」と疑問に思ったことがあったが、そのインターン生の中では筋が通っており、自分の課題点を明確にしていたようで、結果的に質も量も優れた行動になっていた。

責任感とメタ認知

「自分の仕事はどこに繋がっているのか?」を考えながら取り組んでいた。入社当初は、自分の仕事がどの道に通じているのかわからないままであったが、ある成功体験を通じて、仕事の楽しさと喜びを知ることができた。
以降は、会社や開発部の全体感がある中で、自身の業務の位置関係と、職務を全うすると、どこに貢献することになるかを理解しながら取り組んでいた。
これを理解してからの動きが別人であった。依頼された仕事は何を意図しているのかを自ら掴みにくるので、依頼者が気づかぬ点の改善提案が出るようになった。

まとめ

これらは、どの企業に勤めたとしても、活躍するための汎用的な要件であることや、20歳を超えてから育成することが非常に難しい点を既に持ち合わせているので、4月からの会社でも何の問題なく活躍できると確信している。
最終出社日にしては、私からインターン生への言葉は非常に少なく、素っ気なく写っていたのだろうと思うが、これほどまでに安心し、感謝の念を抱きながらの別れは清々しささえある。
1年半、お疲れ様。ありがとう。

ReactNativeでカメラロールから写真を複数枚選択する処理の実装

概要

自分が作っているプロダクトであるSnapmartは、スマホのアプリから誰でも手軽に写真を売り買いできるサービスです。
スマホアプリでは、カメラロールの中から写真をアップロードをする機能があります。
この写真をアップロードする機能ですが、カメラロールから写真を複数枚選択する処理の実装に非常に苦労しました。
この記事では、ReactNativeでカメラロールから写真を複数枚選択する処理をどのように実装しているのか / 実装の前に何を比較検討したのかを紹介します。

仕様の説明

まずSnapmartアプリにおける、写真の選択画面の仕様について、説明をします。
大まかな流れは以下の通りです。

  1. アップロードしたい写真があるアルバムを選択
  2. アルバムの中からアップロードしたい写真を複数選択

アルバム選択画面

f:id:watasihasitujidesu:20200213184903j:plain

こちらのアルバム選択画面の仕様は、以下の通りです。

  1. BottomNavigationの出品アイコンをタップするとスクリーンの表示
  2. カメラロール内の以下の項目を取得し表示(iOSの場合)
  3. 最近の項目
  4. お気に入り
  5. 存在するアルバム全て

写真選択画面

f:id:watasihasitujidesu:20200213184918j:plain

こちらの写真選択画面は以下の通りです。

  1. アルバム選択画面で選択したアルバム内の全ての写真を表示
  2. 表示される写真は撮影日の降順
  3. 複数枚選択し、アップロード

仕様だけなら非常にシンプルですね。

試してみたこと & 問題点

先述の仕様を実装するために、以下のことを試しました(実際にリリースもしました)。

  • アルバム選択画面を実装せず、直近1万枚の写真を表示し選択してもらう画面のみ実装
  • アルバム選択画面を実装せず、直近10万枚の写真を表示し選択してもらう画面のみ実装
  • ImagePickerを使用する

アルバム選択画面を実装せず、直近1万枚の写真を表示し選択してもらう画面のみ実装

元々のアプリはSwiftで実装されていたのですが、これをReactNativeに刷新しました。

blog.naoshihoshi.com

当初いち早くリリースするために、軽微(だと錯覚していた)機能は実装せずにリリースしてしまいました。
既存のユーザーさんからすると以下のような状態になってしまいました。

  • SwiftだろうがReactNativeだろうが、見た目は変わらない
  • それなのに、これまで使っていた機能が無くなってしまい不便になった

また、直近1万枚の写真しか表示されないので、1万件を超える写真が端末に保存されていた場合に画面に表示されない問題も発生しました。 これはテスト端末機に写真が1万枚も保存されていなかったため、事前に見抜くことができませんでした。

その結果、お問い合わせが急増してしまったり、アップロード率が低下してしまいました。

アルバム選択画面を実装せず、直近10万枚の写真を表示し選択してもらう画面のみ実装

お問い合わせが急増してしまったので、急いで修正する必要があります。 まず、1万枚の制限を10万枚に緩和してリリースしました。

しかし、10万枚を表示させたことによって、10万件の写真のソート処理に時間がかかってしまう問題に引っかかってしまいました。
基本的には、React NativeのCameraRoll APIを使用して実装をしています。 ところが、CameraRoll APIには、写真の並び順を指定するオプションが存在しないため、自前で実装する必要があります。 以下のようなイメージで実装します。

CameraRoll.getPhotos(options)
  .then((obj) => {
    let images = [];
    obj.edges.forEach(asset => {                                                                                 
      images.push(asset.node);
    })
    images.sort(function(a, b) {                                                      
      return b.timestamp - a.timestamp;
    })
  })

最大10万ループするので、写真を多く持つユーザーさんほど処理が遅くなってしまいます。
こちらでリリースした結果、以下のような問題点がありました。

  • ソートしている間は画面が真っ白になる
  • ソートにリソースが持っていかれるため
    • アプリがクラッシュしてしまう
    • 充電の減りが早い

ImagePickerを使用する

自前で実装しない方法を模索していたときに発見したのが、Expoが提供するImagePickerです。

ImagePicker - Expo Documentation

上記のページにある通り、このように実装するだけで、カメラロールからの写真選択が実装できます。
非常に素晴らしいAPIです。

ImagePicker.launchImageLibraryAsync

ただ、このAPIは写真を一枚ずつ選択しなければならないため、Snapmartにおける複数枚アップロードしたいという要望は満たすことができません。 試しにImageaPickerバージョンで社内メンバーに触ってもらったところ、見事にお蔵入りになりました。

悩んだ末の実装

結局、CameraRollで処理を頑張ることにしました。
また、CameraRoll.getPhotosにはオプションでgroupNameを指定することができます。
このgroupNameは写真が属しているアルバム名です。
そのため、Snapmartアプリにアルバム機能を実装して、CameraRoll.getPhotosに渡すオプションで、選択されたアルバム名をgroupNameで渡すことで、 ソートする写真を減らすというアプローチにしました。

ただ、アルバム名を取得しようにも、カメラロールに保存されている最後の1枚までアルバム名を取得しなければならず、結局ループする回数は減らすことはできませんでした。
苦肉の策として、以下を実装しています。

  • 一度取得したアルバム名(groupName)はAsyncStorageに格納する
  • AsyncStorageにデータがあった場合はそれを表示
  • アルバムを作成/削除した場合のために再読み込みボタンを設置
let folderNames = [];
CameraRoll.getPhotos(options)
  .then((obj) => {
    obj.edges.forEach(asset => {
      if(!folderNames.includes(asset.node.group_name)){
        folderNames.push(asset.node.group_name);
      }
    })
    AsyncStorage.setItem("hogehoge", folderNames.join(","));
  })

また、アルバム名(asset.node.group_name)で絞り込む場合はCameraRoll.getPhotosのオプションで指定します。

const options = {first: 99999, assetType: "Photos", groupTypes: "Album", groupName: "アルバム名"}
CameraRoll.getPhotos(options)

ReactNativeでカメラロールから写真を複数枚選択する場合のベストエフォート

  • カメラロールから写真を複数枚選択する
  • 写真の並び順を変更する

という要件は、簡単に実装できるように見えて、ReactNativeにおいては自前で実装が必要です。 また、CameraRoll APIは写真の並び順の指定ができないため、全ての写真をループで回し、timestampの降順/昇順、ファイル名の昇順/降順を決定する必要があります。

この問題点を少しでも緩和するために、写真選択画面の前に、アルバム選択画面を置くのが、今のところ最善策のように思えます。 理由は、アルバム選択をしてもらった場合、写真のループ処理の母集団が少なくなる(=それだけ処理が早く終わる)ためです。

ReactNative製AndroidアプリからさらにiOSアプリ実装が終わったのでKPTを行う

この記事はReact Native アドベントカレンダーの23 日目の記事です。

概要

SnapmartのAndroidアプリはReact Nativeで実装されています。
iOSアプリはSwiftで実装されているのですが、この度、React Nativeで置き換えることにしました。

手順は、まずAndroidアプリを作り、それからiOSアプリを置き換えることにしました。*1
今回は、ReactNative製AndroidアプリからさらにiOSアプリ実装が終わったので、そのKPTを行います。

Keep

動作しない部分の洗い出しと、修正が迅速に行えた

iOSのエミュレーターで動作させたときに、すぐに動いた訳ではないのですが、下記切り分けがスムーズだったので、滞りなく修正ができました。

  • AndroidOnlyのオプション使用
  • React Nativeが提供するコンポーネントのOS別の差分

Android Onlyのオプション使用

こちらは単純にAndroidしか動作しないオプションを使用していたため、iOSでは動作しないという問題でした。
今回頻繁した具体的な問題は、Text Input コンポーネントの複数行入力で使用するnumberOfLinesオプションです。 当初何の気なしにAndroid Onlyオプションを使っていた自分を呪いました。 OSによって実装を分けることはクロスプラットフォーム開発の利点を殺しかねないので、用法用量は適切に!

React Nativeが提供するコンポーネントのOS別の差分

動作確認を行なっていると、しばしば「Androidと微妙に挙動が違うよね?」というコンポーネントがありました。 最も差分があったのはPickerでした。

Androidでは、Pickerと選択後の値表示が一つのコンポーネントで完結するのですが、iOSでは、Pickerは単純に選択肢を表示するだけのコンポーネントでした。
そのため、iOSでは別途Textコンポーネントを用意し、onPressでPickerを発火させるといったことをしなければなりません。
問題の解は見えているのですが、腑に落ちない*2ため、react-native-picker-selectを使用することにしました。
(その他、小さい問題は、Textコンポーネントのstyleとして、borderColorが動作しないというものもありました。)

ほとんどが、上記2パターンで切り分けできたので、

  1. 不具合報告がある
  2. コンポーネントのオプションを調べる
  3. 2で問題ない場合、OS別の差分であると断定し、修正

という感じで、サクサク修正することができました。

Problem

動作確認の負担が重い

Snapmartアプリは比較的小規模なアプリであるとはいえ、手動テスト項目は250件にもなります。 これらをデバイス別(iPhone 6, 8, X)で行うため750件になります。 通常業務も行いながら、テストを実施をしているため、全て完了するのに、おおよそ1週間ほど必要になります。

負担は非常に大きいです。

Appleの審査になかなか通らなかった

いざ、Appleへ審査申請をしましたが、7度却下されました。 具体的な却下理由は下記の3点です。

  1. メタデータ(App Storeで使用する写真の解像度)が不適切(ガイドラインに沿っていない)
  2. ログインできない
  3. ボタンが動作しない(!)

1,2はしょうもないミスだったのですが、3の指摘で5度の却下と1ヵ月前後レビュアーさんとのやりとりや修正作業を行いました。

ボタンが動作しない指摘の修正

Appleの審査はiPadで行われ、*3最新のOS(この記事執筆時点では12.4)で動作させているようです。
また、最新のエミュレーターではOSが12.2までしか上げられませんでした。
さらには、Expo Clientアプリを経由したアプリの動作と、ビルドした.ipaファイルを実機にインストールしたものでは、若干挙動が異なりました。

そのため下記差分が重なり、不具合を特定するのに時間がかかってしまいました。

  • iPhoneとiPadの筐体の違い
  • OSのパッチバージョンの差分
  • .ipaファイルでの動作確認不足

また、これらは一見落ち着いて切り分けできているように見えますが、「Appleのレビュー担当者のミスなのでは?」「なぜiPadを使っているのか?」「もう一度ビルドしたら挙動が変わるかも?」といった、問題を直視せず、憶測で修正するといった愚行を重ねてしまいました。
まず、他人(Appleのレビューの方)が行なってくれたことは何一つ間違っていなかったことと、親切に受け答えしてくれたので、「自分はなんてことをしてしまったんだ...」という思いになりました。
それと同時に、未知の物に対する不安は判断精度を低くしてしまうことも自分なりの発見でした。

事実を基に落ち着いて判断。問題解決の鉄則ですね。

Try

Detoxを導入

今回のiOS実装だけではなく、Expo SDKのバージョンアップでも全体的な動作確認は行います。また、Expoのバージョンアップの頻度は非常に短いです。
規模が大き目のリリースのたびに、動作確認をメンバーにお願いするのは負担、効率、スピードなどを考慮すると、極力避けたいです。
また、動作確認に関しては、E2Eテストである程度自動化できます。
それを実現するのがDetoxです。

github.com

アプリの様々な機能のうち、主要機能から攻めていきたいと思います。

まとめ

React NativeでAndroidアプリを作成したのちに、iOSのReact Nativeチャレンジしましたが、Androidと同様の仕様で挙動させること自体は簡単でした。
自分の場合、初のApple審査申請で日和ってしまい、落ち着いて的確な判断ができなかったことが、遅延した原因です。
また、リリースサイクルの中で手動によるボトルネックが問題として認識できました。

*1:当初そもそもAndroidアプリがなかったため。

*2:OS別の処理は極力書きたくない

*3:iPadで提供する/しないに関わらず

AWS障害が発生した場合に確認するページやサイトまとめ

概要

2019年8月23日 13時頃からAWS EC2の接続ができなくなる障害が発生しました。
このような大規模障害は滅多にないので、障害の情報収拾する際に「どこみりゃいいんだ?」となるので、この機会にまとめることにしました。

この記事ではAWSで障害が発生した場合に確認するページやサイトをまとめます。

公式情報

公式の情報は正確性はあるものの、速報性には欠けます。
そのため、後述する非公式情報と並行して確認する必要があります。 公式情報からは、下記2つの情報が得られます。

  • 何がなぜ障害に繋がっているのか
  • いつ復旧する見込みなのか

この情報から、障害を回避するための方法や、自サービスの復旧見込みのアナウンス*1に役立てることができます

AWSサービス全体の障害情報

AWSサービス全体の障害情報はAWS Service Health Dashboardで確認することができます。

status.aws.amazon.com

このページは、大きく最新の情報(Recent Events)と過去7日間の障害履歴(Status History)が表示されます。 また、Recent Eventsには障害の状況も確認することができます。

f:id:watasihasitujidesu:20190823144855p:plain

自分のアカウントで影響を受けているAWSサービス

AWSサービス全体のうち、自分のアカウントで影響を受けている障害情報は、AWS Personal Health Dashboardで確認することができます。

aws.amazon.com

f:id:watasihasitujidesu:20190823150903p:plain

ここで表示される情報は、基本的にはAWS Service Health Dashboardと同一です。 また、AWS Personal Health Dashboardは、CloudWatch Eventsと統合することができるので、障害が発生した場合にいち早く通知を受け取ることも可能になります。

AWS CLI

先述した2つのダッシュボードで障害情報を確認することができますが、CLIでも確認することができます。

docs.aws.amazon.com

CLIでは下記オプションを指定することで、細かくフィルタリングすることができます。

  • describe-affected-entities
  • describe-entity-aggregates
  • describe-event-aggregates
  • describe-event-details
  • describe-event-types
  • describe-events

用途としては、下記の場合に使えるかと思います。

  • 7日以上前の障害状況と復旧までの時間をログとして残しておきたい
  • 特定の情報のみ操作したい

非公式情報

非公式の情報は正確性には欠けるものの、速報性があります。
AWS障害と思われる事象が観測された場合に、他のAWSユーザーも同様の状況に陥っているかを素早く確認することができます。 そのため、AWSの障害なのかを切り分けるための初手としては良い手だと思います

Twitter awsハッシュタグ

基本的には、Twitterを見れば誰かしらつぶやいていることが多いです。
この場合、最新タブに切り替えて、みんなの速報を確認します。

https://twitter.com/search?q=%23aws&src=typed_query&f=live

Twitter非公式アカウント

AWSの非公式アカウントで障害情報の速報を流しています。 今回の障害では見ることはありませんでしたが、「こういうのもあるよ」程度で知っておくと良いと思います。

まとめ

情報には、公式情報と非公式情報があります。 それぞれ、メリットとデメリットは下記の通りです。

  • 公式: 正確性はあるものの、速報性には欠ける
  • 非公式: 正確性には欠けるものの、速報性がある

そのため、障害が発生した場合、公式情報と非公式情報をうまく使い分けて、情報収拾をする必要があります。
また、公式の情報には、何がなぜ障害に繋がっているのか、いつ復旧する見込みなのかという情報が公開されます。
この情報から、障害を回避するための暫定対応策と、自サービスや社内メンバーへのアナウンスを考える事ができます。

*1:ユーザーさんや社内メンバーに向けて

SnapmartにPull Pandaを導入しました

概要

6月18日(火)にGitHubがPull Pandaを買収したと話題になったので、早速Snapmartにも導入しました。
今回は簡単にPull Pandaの紹介をします。

Pull Pandaとは

pullpanda.com

Pull Pandaとは、GitHubのPull Requestにおいて下記の機能を提供しています。

  • PRのリマインド
  • PRの分析
  • レビュアーの自動アサイン

PRのリマインド

PRのリマインドはSlackと連携し、特定のチャンネルや、DMで通知を行うことができます。
また、時間、曜日、フィルターの設定もできます。
設定は画面はこんな感じです。

特定のチャンネル宛てへのリマインド設定

DM設定

PRの分析

PRの分析は下記の画面が用意されています。

  • Review turnaround
  • Reviewer workload
  • Open PRs
  • PR merge time
  • PR throughput
  • PR size
  • Code churn

Code churn画面

これらは時間の経過と共に価値が上がる情報だと思うので、導入したタイミングの数値がどうこうというよりは、定期的に振り返って確認して整理するといった使い方になるのかと思います。

レビュアーの自動アサイン

レビュアーの自動アサインは、GitHub Appsで提供されます。
設定は、アプリをインストールし、自動アサインを有効化したいリポジトリを選択するだけです。

レビュアーの自動アサイン

所感

レビュアー自動アサインと、PRリマインドのReal-time messagesやDMの設定により、下記やりとりがbotで解消できそうです。人間による温かみのあるメッセージのやりとりがなくなるのは非常に残念です

  • レビューお願いします
  • レビューしました
  • lgtm!!

分析については、導入直後なので正直そこまで使用感がわからなかったのですが、少し時間を置いて見てみようと思います。

ReactNative版Snapmartアプリ開発の舞台裏

概要

先日、SnapmartのスマホアプリをReactNative + Expoで実装を行いました。

texta.pixta.jp

ピクスタ開発ブログに書いた内容だと、カッコ良いところしか書けなかった気がするのと、協力してくれた方々について全く触れることができなかったので、この記事では「当時の心境や協力してくれたみなさんの活躍など」に触れながら、自分視点で書こうと思います。 そのため、誰かのためになるような書き方ではないので、チラ裏思い出日記くらいに思っていただければと思います。

2018年11月末

スナップマート社に関わり始めたのは2018年11月末あたりからでした。
代表には「スマホアプリのAndroid版を作って欲しい」という話を聞きました。*1

当時の自分

自分は、バックエンド(Rails)、Webフロントエンド(React)、インフラ(AWS)と、Webに関しては一通り手を動かせるスキルは持ち合わせていました。
これらの領域、個別具体的なスキルの習得は、

  1. アンテナを張る
  2. 特定の技術の戦国時代をゆっくり眺める
  3. 戦国時代を駆け抜けた特定の技術がデファクトスタンダードとなる
  4. デファクトスタンダードとなった技術を習得する

という感じで、「最新の技術動向は追いかけるけど、習得は安定化されてから」といった具合でした。
ただ、Webについては比較的カバー範囲は広めですが、スマホアプリの開発ができないことがずっとコンプレックスになっていました。

4年前の自分

もっと突っ込んで話すと、時を遡ること2015年8月。
ピクスタでメインエンジニアとしてやっていた頃、スマホプロジェクトに携わることになりました。(当時は自分のスキルはRailsしか書けませんでした)
当時はSwift 2.x系だったのですが、この偶発的機会を活かすべく、学習にかかろうとしたのですが、様々な理由から断念しました。

  • Xcodeの扱い方がわからない。故にSwiftを学ぶモチベーションが湧かない
  • 同プロジェクト内にiOSアプリ開発経験豊富なエンジニアがいる
  • Knowledge Transferを目的にSwiftを触ることもできたが、自分がSwift実装した場合にスマホプロジェクトを完遂させる自信がなかった

上記の点を考慮して、iOSアプリ開発経験豊富なエンジニアに任せることにしました。外野から見てもこの意思決定は合理的だと思います。 しかし、開発中もずっと「自分もSwift書きたいなぁ〜」と思っていました。自分で意思決定しながら、開発中に後悔し始めていました。

まぁ、簡単にいうと、失敗を恐れて、自分ができる範囲で気持ちよく実装することを選択したわけです。
そのプロジェクトは多少スケジュールが押したものの、成功と呼べるものになりました。
ただ、その中で、逃げの選択をした自分に対して激しい怒りと失望をしたのを今でも覚えています。

そんなこんなで舞い込んできたスマホアプリプロジェクト。
依然としてスマホアプリ開発の知識は0でしたが、このチャンスを逃したら後悔することは自明であるため、二つ返事で「やります」と返答しました。

あと、色々言い訳をして逃げるような状況を作りたくないというか、、退路を断つために「3ヶ月くらいでイケるんじゃないですかね」と、各方面に風聴しました。*2
自分は心の中でやり切ると決めたことは、完全勝利を収める以外の着地は考えられないので、自らを奮い立たせることができました。

2018年12月

12月は主に技術選定と、技術調査を行ないました。
ピクスタ開発ブログでも書いた、あるべき姿と現状の乖離を埋めるためにReactNativeを選定したってやつです。

技術選定

技術選定する際も、公式ドキュメントやAPIの充実具合、そこからくる実現可能性はみるのですが、

  • ReactNativeは日本語の情報が皆無
  • 不具合が多数存在している
  • 不具合に遭遇した場合はIssue, Pull Request, Release Note, feature branchなどを読まなければならない
  • 自分は英語が読めない/(^o^)\

という、不安しかない状況でした。
ただ、それと同時にExpoという開発ツールの存在を知りました。
Expoの公式ドキュメントを見るとAPIが充実していることや、JavaScriptの世界だけで生きることが可能であることがわかりました。
Expoに負んぶに抱っこしてもらえるのであれば、なんだからイケそうな気がしてきたので、このタイミングでスナップマートの代表と役員に「12月中は技術調査をします。これでダメだったらReactNativeはやめる。」ということを話しました。

技術調査

技術調査は、大きく2つ行いました。

  • 既存iOSアプリを模倣するための手段の確立
  • 既存のSnapmartアプリの主要機能をReactNativeで実装していけるか検証

技術調査は、フィジビリティスタディの技術版と考えて良いと思います。
特定の技術について、実行可能性や様々な要因を検証することが目的です。中でもReactを学習したエンジニアが実装していけるかがピクスタ開発部に逆輸入できるかを分けるのではないかと考えていました。
今回の開発においては、上記2点が調査ポイントであると同時に最大の障壁であるわけですが、この2点に「できる」と答えられるのであれば、あとは問題は作業レベルに粒度が下がるため、時間との戦いになります。
問題が時間になっているということは、リリースに対して他に障壁がなく確実に到達できるとも言えるので、この状態をまずは目指しました。

既存iOSアプリを模倣するための手段の確立

ピクスタ開発部ブログで書いたこちらの手段の確立(検証)です。

  • APIサーバーのRoutingを知る
  • 元々存在するSwift製iOSアプリのエミュレーターからlocalに立てたAPIサーバーにリクエストできるようにする
  • リクエストの内容を確認する
  • APIの処理を読む
  • レスポンスの内容を確認する
  • エミュレータ上のアプリの動作を確認する
  • ネイティブの処理を読む

実は、これを検証するだけで12月の半分が過ぎました。
と、いうのも、既存iOSアプリの実装者は既に退職済みであるため、エミュレーターの立ち上げ方がわからず、スマホ側の問題は全て自分で解決していかなければならなかったからです。
一応、iOSアプリのリポジトリのREADME.mdに3行程の導入手順が書き記されていたものの、すんなり動くわけもなく、大量のエラーが発生していました。*3
非常にまずい状況ですね。

ただ、土地勘のないSwift周りのモジュールでも、不思議と、Issue, Pull Request, Release Note, feature branchをひたすら読んだら、徐々に何が問題なのかが分かるようになるんですね。

そして、やっとこさエミュレーターが動いたのが、12月17日週でした。 そこからはRailsの世界の話なので、数日で模倣することが可能であることがわかりました。
ただ、残り4営業日中にもう片方の技術調査をしなければならない状況です。

既存のSnapmartアプリの主要機能を実現できるか検証

時間がないので、世の中がクリスマスムードで煌めいていようが、会社の忘年会が行われていようが、それ横目にひたすら検証です。

こちらは、「既存機能の確認 <=> ReactNative + Expoのドキュメントを読んで実現できるかを小さいアプリを作って検証」をひたすら行いました。

とにかく機能要件を満たせるかが分かれば良いので、見た目はガン無視で進めました。 この時点で主要機能である下記が満たすことができれば、OKとしました。

  • Facebook認証
  • カメラロールから写真選択および、アップロード
  • カメラを使った写真撮影
  • ナビゲーション
  • スワイプ処理
  • Push通知

時間が足りない

とはいえ、時間が圧倒的に足りませんでした。
自分がスナップマートへ関われる時間の比率50%程度(週20時間)でした。

  • 50%: ピクスタの部長業務
  • 50%: スナップマート対応

という感じです。
また、スナップマートの対応は完全にAndroidに集中できるわけはなく、

  • 業務改善タスク(Rails)
  • 業務仕様把握
  • インフラ仕様把握
  • スナップマート開発方針策定
  • 開発フローの是正
  • Android化を見据えたサイト高速化
  • AWSコスト削減
  • 不具合/障害対応
  • エンジニア / デザイナー採用業務
  • アルバイトフォロー

上記の業務を週20時間の制約の中で並行して行う必要があります。
そのため、技術調査は上記が終わってからやっと行えるという感じです。

幸い、自分は平日3時間、休日8時間は自己研鑽を10年間くらい行なっていたことと、残業と深夜対応を物ともしない体力がありました。 そこで、時間を捻出するために可処分時間を全てReactNativeに充てたらどうか?という計画を立ててみました。

平日: (3時間自己研鑽 + 残業4時間 + 所定4時間) * 5営業日 = 55時間 休日: ちょっと多めに12時間 * 土日2日 = 24時間

1週間で79時間をReactNative調査に充てることができる計算になります。
所定8時間で働く人の倍の時間が使えることになります。 そうなると「3ヶ月でリリースします」というのは、普通の人の6ヶ月相当となるのでなんだか勝てるような気がしてきました。
今振り返ると、ピクスタの業務と合わせると作業時間が月400時間近くになるのと、それを4ヶ月継続したのには流石によくやったなと思います。*4*5

2019年1月

年末年始は通常業務が完全停止するのでボーナスタイムです。
通常業務が剥がれる代わりにReactNative調査に時間を割くことができます。
しかし、正月は実家に帰省していたのですが、開発環境が貧弱である為、進捗はあまりでず検証が済みませんでした。*6

スナップマートの週次定例は毎週火曜日に実施するのですが、1月8日(火)はまだリハビリ期間なので、1月15日(火)の定例で技術調査の結果を報告することになります。
この時点でFacebook認証とPush通知は実装方法がよくわかっていませんでした。ただ、両方ともExpoでサポートされているので、実装は不可能ではなさそうでした。
そして迎える1月15日の定例、多少不安がありましたが、大方問題なく動作することがわかっていたのでReactNativeで行くことに決めました。
リリースまでの期間は変えず、3月末です。

最初期のReactNative + Expo版Snapmartアプリ
最初期のReactNative + Expo版Snapmartアプリ

1月以降の実装の大まかな流れは、下記の通りです。

~1月20日: 各種ライブラリ類の整備 ~2月20日: フロントエンド処理の実装 ~3月20日: レイアウトの調整 ~4月20日: 動作確認

ReactNativeは基本的にReactで開発を行うので、1月中はモダンなライブラリを導入していくことにしました。 具体的には下記です。

  • TypeScript
  • Redux
  • axios
  • redux-saga
  • React Navigation

State管理、API処理(非同期処理)、ルーティングさえ最初に整備しておけば、あとは整備されたお作法に則って実装すれば良いという状況を作りました。 基本的にはJavaScriptの世界で実装できるので、特に大きな問題はありませんでした。

2019年2~3月

2019年2~3月は特に大きな変化もなく、12月確立した開発方法(エミューレータ<=>APIのデバッグ)と、1月整備しておいたお作法に則り、粛々と実装を行うのみです。
だいたいそうだと思いますが、数画面実装を行えば勝手がわかるので淡々と実装するだけのマシーンになります。そのため、大量のタスクをモチベーションを下げずに長時間実装を行うのには、コツが必要です。
一人で実装していて、方向を見失うことはまずないと思いますが、全部で70画面くらいあるのでタスク管理はしました。

  • 全体のタスク量
  • 現在消化したタスク量
  • 今週タスクすべきタスク量
  • 日進率の乖離

これらを管理するのは当たり前として、
自分は易きに流れるので、自分の気を引き締める為にタスク管理を通して自分を管理下に置きました。

Androidアプリ開発に使っていたTrello
Androidアプリ開発に使っていたTrello

期間中、合計1,244件のカードを対応しました。 最初は全てのカードがTodoに存在するので、その数に圧倒されましたが、2~3週間もすれば自分が1週間でどれほどのカードをDoneにできるか、何時間でどれくらいの処理が書けるのかが把握できてきます。
この情報さえ把握できれば、バーンダウンチャートを作成することが可能なので、現在の状況や進捗を的確に把握し実装完了までの見込みを話すことができました。

見せることができない状態

先述の通り、開発の進め方は処理の実装 => レイアウトの調整 => 動作確認と進めていく予定でした。
しかし、この進め方は不確実性の排除を第一に目的とした実装順序です。 そのため、自分以外の者はレイアウト調整が終わり、デバイスを用意し終えた後の動作確認の段階にならなければ、成果物が見えません。
開発者しか進捗がわからない完全ブラックボックス状態なので「本当に実装できているのか?進捗は虚偽ではないか?」と思われかねません。

幸いにして、スナップマートの代表や開発時に連携するメンバーは、自分の進捗報告を信じて待ってもらったのには本当に助かりました。
もし、進捗を事細かに報告しなければならない状態(雑務が増えた状態)であれば、時間を浪費してしまうことや、モチベーションが低下していたのかもしれません。

今回はこの進め方がたまたまうまくいったことは重々承知ではあるのですが、

  • PJの特性
  • 周りの方々の自分に対する信頼

がうまく噛み合った結果だと思います。 スナップマートの代表が「早く見せて」「いつ動かせるの?」と、一切口出ししなかったのにはシビれました。最高でした。

2019年3~4月

3月末から4月は実装が終わり、いよいよ動作確認です。 当初、自分がテストシナリオを作成して、メンバーに対応してもらうと考えていたのですが、実装の多さからテストシナリオを書く余力(時間)が残っていませんでした。

というより、月400時間前後を仕事に関連することを行なっていると、流石に疲労がピークとなり、気力もなくなりかけていました。
そんな状態だったので、テストシナリオの作成が滞ってしまいボトルネックになりかけていました。その状況を救ってくれたのはスナップマートのWebディレクターの方でした。
「テストシナリオが今の自分では作れなさそう」ということを申し出るまえにその方が一気に作り上げてくれました。
これにはとても救われました。
その方の当事者意識やガッツ溢れる姿をみて、自分にも活力が湧いてきたことを今でも覚えています。

3月末から4月上旬にかけてのラストスパート
3月末から4月上旬にかけてのラストスパート

GitHubのコミット数を可視化すると、上記の通り3月末から4月上旬にかけての追い込みがわかります。

インターン生が大活躍した動作確認

テストシナリオは400件弱の項目がありました。
また、異なるAndroidバージョンで実機による確認を行いたかったので、4端末で確認することにしました。

  • 6.1
  • 7.0
  • 7.1
  • 8.0

1デバイスあたり400件全て確認するので、合計1,600件のテストです。
この膨大な量を一人で短期間でこなすのは困難であるため、Webディレクター、インターン2名の計3名に応援をお願いしました。

この3名が非常に粘り強く、責任感ある3名でした。
自分が決めた無理のあるリリーススケジュールに対して、前向きに取り組んでくれました。
休日に対応してくれていることを褒めるわけではないですし、自分が休日に開発を行なっていることを彼等彼女等は知っているので、それが圧力になったのは本当に申し訳ないと思っているのですが、プライベートの時間を削ってまで対応をしてくれました。

この3名の頑張りがなければ、予定通り実装が終わらなかったのではないかと思います。
本当に感謝しかありません。

Webディレクターの柔軟な意思決定とラストスパート

彼ら彼女らの動作確認のおかげで、短期間で不具合を発見することができました。
その数、140件でした。一瞬怯みそうになりましたが、3ヵ月全ての機能を自分一人で実装していたので、プログラムが全て頭の中にロードされている状態でした。
この状態になると、不具合の内容をみた瞬間に、どのファイルの何が原因で不具合が起きているから、どのように修正するかが瞬時に判断できるようになります。
そのため、多少量が多くとも1件あたりの難易度が低いので、ほぼ瞬殺対応することができました。

また、不具合の中でも対処するか判断に迷うものもありました。
それは、下記のような内容です。

  • ReactNativeでは実装ができない
  • 実装に時間がかかる
  • 実装の難易度が高い
  • 完全にiOS版アプリの挙動を模倣できていなくとも、今は問題ない
  • 機能として欠落しているが、まだ必要にならない

これらは、基本的にエンジニアのスキルが高ければ相談するまでもなく対応できるのでしょうが、当時の自分には全て実装することは無理だと感じていました。
そこで、Webディレクターやメンバーと協議をしながら進めることにしました。

ピクスタの開発ブログで、

チーム全体が「ReactNativeでは実装できません」を許容し、柔軟に仕様や要件を変更しながら落とし所を決めていくことが求められます。

と太字にして強調しましたが、上記の意思決定プロセスがチームで自然とできていたので、迅速に取捨選択を行うことができました。
つい完璧を求めてしまい、ずるずるとリリースが伸びることも想定していましたが*7、これほどまで柔軟に変更を許容できるメンバーをみて感服しました。

2019年5月

実装~動作確認~不具合修正は4月15日(月)に完了しました。1月15日(火)本実装開始から、ちょうど3ヵ月くらいの着地です。
実際のリリースは2019年5月7日(火曜)なので、実装完了~リリースまで2週間のタイムラグがありますが、

  • RubyKaigiへの参加
  • ゴールデンウィーク期間中

無理にリリースして上記の期間中にお問い合わせが発生した場合の対応を考えると、ゴールデンウィーク明けに安心してリリースしたいよね。となりました。
もちろん、この2週間はリリース後対応などを行なっていました。駄目押しの実装という感じです。

まとめ

2018年末、スナップマートの代表も「3ヵ月でできるわけがないだろう」と思っていたようですが、自分も同じことを思っていました。

  • 目標を掲げて見つめ続ける
  • 置かれた状況に怯まない
  • 自分ができることを整理して実行する
  • 限界まで懸命に頑張る
  • メンバーに頼る

困難な道のりでも、常にこれを着実に実行することで目標は達成できるものだと身を以て体験できました。
振り返ると、20代最後のチャレンジが人生最大のチャレンジになりました。
全力で取り組んで目標達成できるのは本当に楽しいですね。仕事の醍醐味だと思います。

このリリースが終わったら2ヵ月くらい休むんだ...。とか思っていましたが、休んでいるより自分が作っているサービスを育てている方が何倍も楽しいことにも気づきました。

今後

無事にReactNativeでAndroid版のリリースができました。程なくしてiOSアプリもReactNative製になります。
先に書きましたが、自分は元々Webの世界しか知らなかったわけですが、WebエンジニアでもReactNative + Expoによってスマホアプリ開発が可能だと検証できました。

話が少しだけ飛躍するかもしれませんが、ウチの会社の長期成長戦略として"様々な分野でクリエイティブプラットフォーム"を生み出そうとしています。

pixta.co.jp

近い将来「新規事業ではスマホアプリを最初に作りたい」というビジネス要求は十分にありうるのではないかと考えています。
現在の開発部はWebエンジニアがメインであるため、この要求を迅速に満たすことができない可能性が高いです。
その場合、ただでさえ成功確率の低い新規事業がエンジニアの力量不足による制限プレイでさらに成功確率が低くなってしまいます。
これは許しがたい状況なので、なんとかして突破口を開こうとしたのが、実はこのSnapmartのReactNative実装だと勝手に思っていました。

今なら「新規事業ではスマホアプリを最初に作りたい」に対して「3ヵ月くらいでできるんじゃないですかね?」と答えることができそうです。

*1:意思決定の背景や理由の説明はもちろんありました

*2:これは本当に良くないと思っています

*3:Xcodeも爆発するし大変でした

*4:自分の長所が一生懸命頑張ることです!

*5:自由意思によるものです。エキサイティングな4ヶ月だったので楽しかったです。

*6:こちらも自由意志によるものです。友人が極少であるため。

*7:自分がPOだったらそうなりそう...

Redashのバージョンアップで消えたデータソースを復活させる方法

Redashのバージョン2系から3系にupgradeした際、DataSourceのプルダウンから色々なものが消えてしまいました(!)

  • BigQuery
  • Athena
  • Google Analytics
  • Google Spreadsheet

業務でも使っているデータソースだったので、これが使えなくなるとなかなか厳しいです。
今回は、Redashのバージョンアップで消えてしまったこれらのデータソースを復活させる方法を書きます。

続きを読む

rakumoのカレンダー登録を効率化するための4つコツ

会社での予定管理はrakumoを使っていて、他のメンバーに「rakumo入れといて〜」という指示を出すときに、 意図しない予定を入れてしまうことがあったりします。

自分は業務上、rakumoを結構使っているので、この操作を知ってると便利だよ〜ってことが多々あります。

今回は業務効率を少しでもあげるためのrakumoの操作方法を紹介します。

空き時間を見る

複数人でMTGをする場合、全員が空いている時間を探さなければなりません。 これをしないと、「予定が被ってるんですけど><」という問題が発生します。

これをやるには「空き時間を探す」機能を使います。

f:id:watasihasitujidesu:20180305200626p:plain f:id:watasihasitujidesu:20180305200734p:plain

この機能を使うと、

  • 赤色の部分: 既に誰かしらに予定が入っている
  • 空白の部分: 全員の予定が空いている

というようになります。 便利ですね!

空き設備を見る

ウチの会社では、MTG参加者の他に、どの会議室を使うのか?という設備の予約もできるようにしています。 その設備は、MTG参加者よりも空き検索が簡単にできます。

f:id:watasihasitujidesu:20180305200912p:plain

このチェックボックスを入れると、表示されている設備は使用可能な設備のみになります。 簡単ですね!

予定管理者の変換

  • メンバーの予定を入れたいけど、自分は参加できない><
  • 親切心でメンバーのルーティーンを忘れないように登録したい・・・!(でも自分の予定には入れたくない)

という時は、予定管理者の変更をしましょう。

f:id:watasihasitujidesu:20180305200957p:plain f:id:watasihasitujidesu:20180305201029p:plain f:id:watasihasitujidesu:20180305201059p:plain
rakumoは内部的に予定管理者と参加者を明確に分けているように見えます*1
また、管理者は単独にしかできません。

この仕組みを使うと、他のメンバーを管理者にすると、自分はただの参加者になるので、メンバーから外すことができます。 つまり、他の人の予定を、しれっと入れることができます。

予定を入れる時、登録完了するまでは、デフォルトで自分が管理者なので、管理者が管理者を変更して、管理できなくなるなるのはおもしろいですね。

自分以外が予定管理者の予定を変える

管理者が自分でない予定は、基本的に予定を変更することができません。 ところが、予定管理者の予定の詳細ページから変更しようとすると、これができます。

f:id:watasihasitujidesu:20180305201219p:plainf:id:watasihasitujidesu:20180305201254p:plainf:id:watasihasitujidesu:20180305201325p:plain f:id:watasihasitujidesu:20180305201351p:plain

素晴らしい。 こうすると予定管理者が忙しい><って時に代わりに予定変更して差し上げることができます。
優しい世界^^

まとめ

rakumoは便利機能を知って入れば、結構自由度高く予定を組み替えることができます。
予定を入れることが多い人はこれらの機能を有効活用して効率よく仕事しましょう!!

*1:おそらく!