こんにちは。スナックミーでフロントエンドエンジニアをやっている前田です。
以前 新規サービスの技術選定についての記事 を書きましたが、今回は定期便のマイページについてのお話をしようと思います。
snaq.me の定期便とは
弊社のメインサービスである snaq.me は、8個のおやつを定期的にお届けするサブスクリプションサービスです。100種類以上あるおやつのレパートリーの中から、お客様の好みに合うおやつが自動的に選ばれるのですが、その選定はマイページで登録していただいた情報をもとに行われます。
マイページはお客様の情報を得るための重要なツールであるため、その歴史も古く、リポジトリが作成されたのは2017年1月でした。
(どうやっておやつが選択されているかを詳しく知りたい方は、 ユーザーに最適なおやつを届ける「アサイン」の歴史 などの記事をどうぞ!)
初期の技術選定と時代背景
2017年というと、つい最近のような、ずいぶん昔のような不思議な感覚を覚えますね。皆さんは当時どのような技術スタックを使用されていましたか?当時の私は、素の JavaScript で Next.js (まだ zeit で、v4.0.0!!!) を使ったり、flow-typed で Vue.js を使ったりしていた形跡が古い PC から見つかりました。
TypeScript は2016年の9月に v2.0 がリリースされ、 strictNullsCheck がサポートされた時期です。世間的にも、フロントエンドを Ruby on Rails (2016年に v5.0 がリリースされて API モードをサポート) などのサーバーサイドフレームワークから分離する設計が広く受け入れられるようになった時期だと記憶しています。
私が入社したのが 2021年なので、選定理由などはあまり把握していませんが、snaq.me のマイページはリポジトリ作成時から React, Redux を使用しています。それらは今でも活発にメンテナンスされているので、とても息の長い技術選定をされたと考えています。
昨今のフロントエンドの風潮と、マイページの課題
そうして走り出したマイページのプロジェクトですが、今でも日々多くの試行錯誤を行っています。我々のようなスタートアップにとって「少ない人的リソースの中でもとにかく早くお客様に価値を届け、フィードバックを得る」 ということは何より重要であり、エンジニアに限らずすべての社員が持っているマインドです。
しかしそのような試行錯誤の連続の中では、一度立ち止まって技術選定や設計の正しさを問うことは非常に難しいものです。徐々に問題が蓄積されていきました。
私が入社した時点ではリポジトリ作成から4年以上が経過しており、主に以下のような課題があったと記憶しています。
- 素の JavaScript で書かれており、静的型解析を利用できない
- 自動テストが少なく、変更の安全性が低い
- JSDoc などのドキュメントが少なく、コンポーネントや関数の使い方を調べるために多くのコードを読む必要がある
- ライブラリが古くアップデートしたいが、静的型解析や自動テストの恩恵をあまり受けられないことが原因で一歩を踏み出せない
昨今のフロントエンドでは TypeScript による型安全な開発が好まれるケースが多く、弊社でも 1 を解決することで 3, 4 の大部分も解決することができると考え、 TypeScript の検討を開始しました。
なお、2 の自動テストについては本記事では言及しませんが、静的型解析の有無にかかわらず書いていかなければなりません。(自戒も込めて…)
意思決定
さて、 TypeScript 導入を決意しましたが、事業としての優先度を決めるために社内での合意を得る必要があります。
スタートアップで大きな技術スタックの変更を行う場合、最初の壁となりがちなのが社内での合意形成ですが、当時から API のフルリプレイスを進めていた背景もあり、スムーズに合意を得ることができました。
特別なことは何もしていません。ですが、毎週のスプリント振り返りで型がないことの問題点を説明し、Slack の times チャンネルで TypeScript の素晴らしさを説き、本番環境を障害で止める(変更したファイルに依存したページが他にあることに気づかず...)など、毎日少しずつジャブを打ったのは重要だったように思います。
(余談ですが、現在ではスナックミーのバリューに「やってみよ!」という項目があり、このようなチャレンジが広く受け入れられる土壌が整っています)
移行期間と考察
結論から書くと、弊社では 2021年8月に移行を開始し、すべての .js(x) を .ts(x) に変換完了したのが 2023/6/15 でした。
推移は以下のグラフをご覧ください。
移行戦略
単に拡張子を変えるだけであれば、 tsconfig.json で型の厳格性を弱めてしまえばすぐにでも可能でしたが、安心して TS ファイルを扱いたかったため strict: true で移行を開始することにしました。
また、 strict: true で TypeScript へ移行する場合には移行作業に時間がかかりますが、機能実装を一定期間止める(もしくは優先度を下げる)ことを許容できなかったので、対象を定めてファイル単位で少しずつ移行しています。
移行のフェーズ
機能実装を続けていたため、ファイル数は全体を通して緩やかに増加していますが、 .js(x)ファイルの減少にはいくつかの段階がありました。
機能実装のついでに期
初期は TypeScript のお試し期間でもあったので、「新規追加するファイルは TypeScript で作成して、そのファイルから参照されるファイルを時間の許す限り移行しようね」という緩めのポリシーで進めていました。グラフ中では 2021年8月から 2022年4月 のあたりです。
想定していたよりサクサクと移行が進み、半年ちょっとで TS 率50% を超過。「これは2022年中の移行完了も可能では...?」と考えていましたが、ここから長い停滞が始まります。
JS ファイル全然減らない期
2022年4月から年末まで、 JS ファイルが一切減らない時期が続きました。大きな新規機能実装が続いたことと、よく編集する JS ファイルの移行が軒並み完了したことが主な原因です。
新規開発で TS ファイルがモリモリ増えたので相対的には TS 移行率が向上しましたが、2022年の後半は実質的にまったく進捗がありませんでした。
Issue 上げてみる期
設定画面などのあまり変更されない画面がボトルネックになっていたため、 UI 改善などの名目で issue を作成し、スプリントに組み込むように。これはおよそ2022年12月から2023年3月の時期ですが、グラフからもわかるようにあまり良い方針ではありませんでした。
当時全体のデザインリニューアルが進んでおり、設定画面だけ古いレイアウトのままだったこともこの手法を選んだ要因でしたが…
- ユーザー体験としては全体の見た目を統一することが目的であるため、事業的に優先度が低い
- UI 改善にはデザイナーを巻き込む必要があるため、エンジニアのみの工数で完結しない
- 「新しい UI の適用」と「型のついていないファイルを読み込んで仕様を理解し、場合によって作り直す」を同時に行う必要があったため対応の工数が高い(よってさらに優先度が下がる)
などの理由から、一旦諦めて別の手法を取ることとしました。
いらないファイル一掃期
この時点でおよそ 250 ファイルぐらい JS ファイルが残っていましたが、まずは使われていないファイルを削除して以降対象を見極めることとしました。
- JS ファイルのファイル名から import 文で使用されそうなワードの一覧を生成
- ワード毎に git grep して1件もヒットしなかった JS ファイルを絞り込み
- 絞り込んだファイルを 1件ずつ目視で確認し、問題無ければ削除
- 削除した後もう一度 1 から実行(非 import が消えたことによって参照が消えた可能性がある)
という単純作業で、1日あたり 100ファイルほどの JS ファイルを検出し、削除することができました。
諦めてエラー踏み潰す期
ここまでで残った JS ファイルですが、
- あまり変更されない
- 難易度が高い
の二種に分類されていたため、まずは1を一掃することに。
ただし、あまり時間をかけられないため 「30分以上かかりそうなら 2 に分類して後回し」「@ts-expect-error と any を使ってもいい」という条件を付けて 1 ファイルずつ移行し、1日で 80ファイルほど移行することができました。
クロージング
この時点で残り70ファイルです!90% 以上が TypeScript なので、以前消せなかったファイルも安心して消すことができるようになり、ファイル削除だけでさらに 30ファイルほど消すことができました。
残りは前項で高難易度と判断したファイルだけだったので、スプリントに組み込んで工数を確保し、地道に型を付けて完了させることができました。機能実装の優先度を一時的に下げることになったので少し心苦しかったですが、作業対象が小さくなったため見積しやすく、他課題との優先度調整も行いやすかったです。
移行作業の反省点
ほぼ2年近くかかってしまいましたが、機能実装の優先度を大きく下げることなくやりきれたのはとてもよかったかなと思います。また、 any をほぼ使わずに移行できたので、以前より安心して機能開発できるようになりました。
しかし、実質的に移行にかけた時間は初期に想定したよりも少なく、うまく優先度を調整をすれば半年から1年程度で移行することも可能だったのでは…とも考えています。
移行を短期間で終わらせることができると、機能実装やリファクタリングをスムーズに行えるようになるため、チームやプロジェクトの規模によってはこちらの方がメリットが大きいかもしれません。
いずれにしても、「移行のために数週間から数ヶ月機能開発を止める」もしくはフルリプレイスするという選択ではなく、少し長めの期間をとって機能実装と並行するという手法は弊社にとっていい選択でした。
移行期間中、設計についてじっくりと思考を巡らせることができたため、私自身の考えを整理することができましたし、リファクタリングや設計に関する良質な記事が多く公開されので、それらのエッセンスを取り込むことができました。
今後どうするか
「 TypeScript に移行した話」とタイトルを付けましたが、TypeScript を使ったことがある方ならお気づきの通り、まだまだ「TypeScript への移行が完了した!」と言える状態とは程遠いのが現状です。
喫緊では、下記を対応しなければならないと考えています。
- @ts-expect-error, @ts-ignore, unknown, any がプロジェクト内部に多数記載されている
- noUncheckedIndexedAccess が false になっているので、配列やインデックス型へのアクセスが安全ではない
- API の型定義 (主に null, undefined ) に若干不整合がある
また、ソフトウェアの安全性を静的型解析だけで担保することは難しいため、自動テストの追加やアーキテクチャ変更、ライブラリの変更などやるべきことはまだまだ山積しています。
弊社では「永遠のβ版」として常にサービスをアップデートしているため、このような問題も永遠になくなりません。しかし、多くのお客様に使われているサービスの機能開発をしながら、技術的な課題の解決に取り組める環境はとても得がたいものだと考えています。
「多くのユーザーに使われているサービスの開発をしたい!」
「技術的な課題に立ち向かいたい!」
という欲張りな方は、一緒に snaq.me のマイページ開発にチャレンジしてみませんか?
engineers.snaq.me
募集ポジション
ソフトウェアエンジニア Backend (lead) / 株式会社スナックミー
ソフトウェアエンジニア FrontEnd (lead) / 株式会社スナックミー
ソフトウェアエンジニア Data Engineer / 株式会社スナックミー