207 Tech Blog

テクノロジーで物流を変える 207 (ニーマルナナ) 株式会社のテックブログ

GraphQLへ移行している話(中間報告編)

207株式会社でソフトウェアエンジニアをしている id:ryo-rm です。
今回は、ラストマイル配送を効率化する配送員の方向けのスマートフォンアプリ「TODOCUサポーター」にて、開発チームの生産性向上・アプリケーションの信頼性向上のために、GraphQLへ移行している話について書いてみます。

移行プロジェクトは長期のプロジェクトとして細々と続けており、直近のリリースで最も辛い箇所を完全にGraphQLへ移行することができたため、主にそちらの移行にあたって行ったことを書きます。

207のアーキテクチャについて紹介

207では、配送員の方向けのスマートフォンアプリ「TODOCUサポーター」を中心として、配送会社の方向けのWebアプリケーション「TODOCUクラウド」、受取人の方向けのアプリ「TODOCU」*1があります。 また、社内用の管理画面も存在します。

バックエンドは Ruby on Railsで構築していて、複数のクライアント(フロントエンド)が存在するという構成になっています。
APIRest APIとGraphQLが混在しており、GraphQL移行を少しずつ進めています。
GraphQLサーバーは graphql-ruby を利用しており、GraphQLクライアントは Apollo Client を利用しています。

「TODOCUクラウド」はほぼすべてがGraphQLへの移行が完了していますが、「TODOCUサポーター」は「TODOCUクラウド」に比べるとコードベースも大きく、Rest APIとGraphQLがまだ混在しています。

「TODOCUサポーター」について簡単に説明すると、地図上に配送先のピンを立てたり、配送する荷物の管理をすることができます。

TODOCUサポーターのホーム画面
TODOCUサポーター

「TODOCUサポーター」の技術スタックは以下の通りです。

React Native / TypeScript / Apollo Client / Mapbox

移行計画

地図画面上に表示するピンのデータは、歴史的経緯で以下2つのデータソースを利用していました。

  1. RDB (PostgresSQL)
  2. Firebase Realtime Database

Firebase Realtime Databaseを使っている理由としては、 (又聞きと想像ですが) 以下の理由があったようです。

  1. SMSで在宅確認をする機能があり、受取人からの回答をリアルタイムで地図上に反映させるため
  2. 企業向けに TODOCUクラウド を提供しており、TODOCUクラウド側で登録した荷物をリアルタイムで反映するため

コミットログを追ってみると、TODOCUサポーターの最も最初期*2から存在しており、当時の技術選定としては最適だったんだと思います。
とはいえ、データソースが複数存在することでデータの整合性が取れていないなど、バグの温床になっていました。

社内向け管理画面に初期から存在するボタン。データがずれる問題を解決するために用意されたもの

フロントエンドのコードも複雑になっている・リアルタイム性は重要ではないということで、Firebase Realtime Databaseを撲滅するための計画が立ち上がりました。

複数の画面で複雑に依存した実装になっているため、一度に全部書き換えるのは現実的ではありません。
そのため、以下の方針で移行するという計画を立てました。

  1. すべてを一度に書き換えるのではなく、各画面単位でデータの取得元をGraphQLに書き換える
    • 新規作成のコンポーネントについては、 Fragment Colocation で必要なデータを宣言し、 graphql-codegen で自動生成された型をReactのPropsにする
    • 各画面の移行では、レンダリングに必要なすべてのデータを画面にわたすのではなく、主キーを用いてGraphQLで再取得する
  2. 各画面の移行が完了したら、Firebase Realtime Databaseのデータをlistenしている大元のコンポーネントをGraphQLに書き換える
  3. フロントエンドの移行が完了したら、バックエンドからFirebase Realtime Databaseに保存している処理を削除して完了

移行のためのGitHub Issue。ZenhubのEpicとして管理

各STEPで行ったことをいくつかピックアップしてみます。

Fragment Colocation + graphql-codegen

Fragment Colocation を利用して各コンポーネントに必要なデータを宣言しつつ、 graphql codegen を組み合わせてTypeScriptの型を自動生成することで、 GraphQL化する前でもfragmentに合うようにPropsを用意するだけで良くなります。

export const FRAGMENT = gql`
  fragment ProductStatusLabelFragment on Product {
    currentProductActivity {
      key
    }
  }
`;

export type Props = {
  fragment: ProductStatusLabelFragment;
};


export const ProductStatusLabel: React.FC<Props> = ({ fragment }) => {
  // 略

コンポーネントと型は密接に関わっており、codegenによって生成されたファイル群はコンポーネントのすぐ近くに置いておきたいため、 near-operation-file を採用しました。

例えば上記のコンポーネントでは、下記のようなツリーで生成されるようにcodegenの設定しています。

src/components/molecules/ProductStatusLabel
├── __generated__
│   └── index.generated.tsx
└── index.tsx

207で利用している codegen.yml ファイルも公開しちゃいます *3*4

codegenの環境は下記のものを使っています。少し古いので更新しないと……

"@graphql-codegen/near-operation-file-preset": "^2.2.2",
"@graphql-codegen/typescript": "^2.4.1",
"@graphql-codegen/typescript-operations": "^2.2.1",
 "@graphql-codegen/typescript-react-apollo": "^3.2.2",
overwrite: true
schema:
  - '../Todocu/schema.graphql' # schema file
  - 'src/api/graphql/local-schema.graphql' # Apolloの Local-only fields 用
documents:
  - './src/**/*.ts'
  - './src/**/*.tsx'
  - '!(src/**/*.generated.ts)'
  - '!(src/**/*.generated.tsx)'
generates:
  types/graphQL.ts:
    plugins:
      - typescript
  src/:
    preset: near-operation-file
    presetConfig:
      baseTypesPath: ~types/graphQL
      extension: .generated.tsx
      folder: __generated__
    plugins:
      - typescript-operations
      - typescript-react-apollo
    config:
      withHOC: false
      withComponent: false
      withHooks: true
hooks:
  afterOneFileWrite:
    - yarn prettier --write

GraphQLに書き換えていく

画面単位での置き換えをほぼ隔月のペースでリリースし、2022年9月、ついに最後のプルリクエストがマージされました。

アプリからFirebase Realtime Databaseを削除する最後のプルリクエストのスクリーンショット。差分が1000行以内に収まった
アプリからFirebase Realtime Databaseを削除する最後のプルリクエスト。差分が1000行以内に収まった

移行にあたり、参照系はとくに躓くことなく進めることができましたが、更新系でかなり躓きました。

207には社内にQAチームが存在し*5、移行にあたりバグの洗い出しをかなり綿密に行ってもらえたため、大変な作業でしたが無事にリリースを終えることができました。

移行結果

社内で寄せ書きを募集しました。*6

寄せ書きを募集

yosetti.com

コメントを抜粋します。喜びの声続々!

  • ryo
    本当におつかれさまでした。障害を起こしたりもしたけど、実装が簡単になってとてもいいヤツでした。新天地でも頑張ってください!

  • gain
    Firebaseさんがフロントチームからなくなることでバックエンドの速度にも大きな影響が出て寂しいです。狭い界隈ですのでまたどこかでお会いしましょう!

  • vibes
    今まで文句ばっかり言ってたが、実際にいなくなると寂しくなるぜ.まあしかし長い人生だ. また会うこともあるだろう。達者でな.

  • chikara
    もう「Firebaseからの脱却待ち」って言えないのが悲しいです...何度、あなたのデータをリセットしたか。これまでありがとう!実際、何度も何度も救われました。

  • Kevin
    フロントエンドチームからFirebaseRDBがなくなるっとの「悲しい~」って言いたいんですが、長い間で私達のライバルになってありがとう。さらばじゃ

  • haraguchi
    約2年間お疲れ様でした!話す機会はあまりありませんでしたが、活躍ぶりは噂で聞いていました。新しい職場でも活躍できると信じています。お身体に気をつけて!

今後

207のエンジニアリングチームではRest APIからGraphQLへの移行を段階的に進めています。
まだ道半ばですが、直近のリリースでFirebase Realtime Databaseへの依存を無くすことができ*7、これからもGraphQLへの移行は加速していくと思います 🥳

今回は "中間報告編" にしているので、移行が完全に終わったらきっとブログを書いてくれることを期待。

最後にいつもの宣伝で、

207株式会社では、レガシーな物流業界の変革に挑む配達員向け効率化アプリ「TODOCUサポーター」を開発しています。 GraphQLでガンガン開発していきたいメンバーを大絶賛募集中です!

もし少しでもご興味がありましたら以下のnotionをご覧ください!

207-inc.super.site

*1:当初はスマートフォンアプリがありましたが、現在は提供されておらず、一部機能をWebサービスとして提供しています

*2:Pull Requestの番号が#3(現在は1000を超えている)になっていた

*3: Apollo ClientのLocal-only fieldsを使っているので、スキーマファイルが2つある。どこかで記事を書くかも。

*4:ymlファイルはもう少しきれいに書けそう。

*5:QAチームが書いてくれた記事はこちら

*6:ヨセッティというサービスを利用しました。もうちょっとオシャレなデザインにしてもよかったかも。サクッと寄せ書きを作れて便利

*7:正確にはチャットなど一部の機能でまだ利用していますが、移行する必要がないのでそのままにしています