非推奨となったMoment.jsから移行を終えた話
こんにちは、スターフェスティバル株式会社 エンジニアのsoriです。
今回は1つのサービス内のアプリケーションで使われている日時操作ライブラリMoment.jsを移行した際に気をつけたことなどをお話しします。
なぜ移行したのか
Moment.jsは、2020年後半に「今後新機能の開発をせず、新規プロジェクトでの使用を推奨しない」旨アナウンス[1]がありました。
アナウンスとしては、Moment.jsがmutableなAPIを使用しており、immutableなものに変更するためには大きな破壊的な変更が必要になること、
また、バンドルサイズが大きいなどの問題があることが挙げられています。
個人的に使用していた感触としては、枯れたライブラリであり気軽に利用できるものであるのは確かなものの、仕様が緩い点[2]などが見受けられ、堅牢なコーディングを行うことにも障害があるように感じられました。
とはいえ、非推奨となってからそれなりの時間が経ち、また次の大きい開発を始める前に少し時間のとれるタイミングだったのでこの機会によりよいライブラリへと移行することに決めました。
フロントエンドアプリケーション
まず、移行範囲が少なくて済むフロントエンドのアプリケーションから着手しました。
APIサーバからはYYYY-MM-DD形式やJSON形式で日時を取得し、それらを処理するようになっており、移行も範囲を分けて安心して行うことができました。
移行先としては、先述のアナウンスのページ内で推奨されているライブラリの中から、社内での採用実績があり軽量で扱いやすいものということで date-fnsを採用しました。
元々の設計として、 下記のことを心がけていたため、こちらの移行はスムーズに行きました。
-
フロントエンドでは極力日付の演算を行わない
このサービスのビジネスでは、営業日や受付時間の計算のため日付の演算をする必要が多く発生していましたが、演算処理についてはAPIに一任しているため、フロントエンドでは受け取った値のフォーマットをするだけとなっています。そのため、今回の移行ではフォーマット処理の移行に専念することができました。 -
フロントエンドでは現在時刻を取得しない
現在時刻の取得をブラウザ側に任せると、タイムゾーンの問題やユーザーのクライアント時刻の差異による問題など解決しないといけない問題が増えます。
こちらについても、API側のサーバ時刻で処理しているため、大きなトラブルなく移行することができました。
APIサーバ
さて、フロントエンドではdate-fns
に移行しており、APIサーバの移行にも使う予定で検証を行っていました。
しかし、当アプリケーションのAPIサーバではDatabase, および実行環境をUTC時刻で取り扱っており、また主要ビジネスは国内で行うため「日本の営業時間」という概念が多分に関係しているため、date-fnsがJS Dateオブジェクトを拡張したライブラリである関係上、時間の計算や変換がうまく行かないことが多発しました。
特に、localの開発環境がJST(日本)時間の環境で動いているにも関わらず、production環境がUTC時間のため、ユニットテストを書くにも環境ごとにテストを分けなければいけないところが致命傷でした。
時刻の概念が複雑になりすぎて脳内が終了しかけたので、APIサーバではタイムゾーン操作がやりやすい別のライブラリを採用することにしました。
APIサーバで採用したライブラリは Luxonです。こちらもMoment.jsの移行アナウンス内でおすすめされている選択肢の1つで、Moment.jsとの開発の関わりもあるようです。
Luxonは独自のDateTimeオブジェクトを取り回す形になっており、その恩恵でタイムゾーンの操作がかなり直感的になっています。
date-fns等と比べると若干fatな印象は受けましたが、背に腹は変えられなかったです。
下記、APIサーバの移行で気をつけた点です。
-
モジュールを用意して記述を極力統一化する
移行にあたって既存コードを調べた結果、moment()
を使って初期化している箇所、moment-timezone
のmoment.tz()
を使って初期化している箇所、
タイムゾーンを指定してあったりしなかったりなど差異があちこちにみられました。
今回移行にあたっては日時操作のためのヘルパーモジュールを整理し、DBから日時の値を取得する際は必ずtimezoneを指定するように変更しました。
これにより、想定外の時刻計算ミスに翻弄されなくなりました。 -
現在時刻の取得を局所化する
先述しましたが、API内には現在時刻を元にした処理が必要な箇所が多くありました。
しかし、現在時刻を取得するのがService層、repository層、database層などあちこちに分散しており、収拾がつかない状況になっていました。
こちらを、ほとんどの箇所でhandler, バッチの実行, job層など、リクエストを処理する最上位の層に統一しました。
このことにより、unit testのあちこちの箇所で現在時刻のmockを記述していたのが、最上位層以外については直接テスト用の時刻を指定してテストが書けるようになりました。
また、最上位層のテストにおいても、Dateオブジェクトを直接mockしていた箇所がありましたが、現在時刻取得のヘルパーモジュールをmockすることで対応可能になりました。 -
Databaseの日時型の変換を修正した
当初、Databaseの日時の型を取得するさい、テストで2022-03-01 00:00:00
のように指定した場合は正しく動くものの、実際に動かしてみるとエラーで日時がparseできない、といったことが発生しました。
ここは本筋とずれるのですが、もともとDatabaseの日時は文字列で返ってくると思っていたのですが、既存のmysqlのライブラリの仕様上、js Dateオブジェクトで返ってきてしまっていたようでした。
Moment.jsのおおらかすぎる仕様のおかげで文字列でもjs Dateでもよしなに処理していてくれたようで、移行に伴い表面化したトラブルでした。
こちらについてはDatabaseのinitオプションを指定することにより解決しました。
移行してよかったこと
-
immutable化の恩恵を受けられるようになった
変数が不変であることが保証されるようになったので、日付の計算などの処理が理解しやすいコードになりました。
既存ではいちいちmoment.clone()
等を使って気をつけなくてはいけなかったので、だいぶ楽になりました。 -
localeをその場で指定できる
Moment.jsを使っていたときは、日時を日本語や英語等でフォーマットしたい場合、language moduleをimportし、ページのglobal methodでmoment.locale()
を呼ぶことにより、それ以降のインスタンスで言語が変更されるという状態でしたが、date-fnsもluxonでもフォーマットメソッドのオプションでロケールを直接指定できるため、意図せず違う言語のフォーマットがされてしまうことが避けられるようになりました。 -
日時関連のテストが書きやすくなった
APIサーバの気をつけた点と重複するのですが、移行に伴い日付を極力mockしない形にしたので、冗長なmock処理を書かなくて済むようになり、テストの書きやすさが増しました。
その結果、既存バグも改修することができたので結果よかったです。
おわりに
特にAPIサーバ移行時には、扱いに慣れておらず所々トラブルがありましたが、上記の各々の項目で解決することができました。
今後移行を考えられている方へ、同じトラブルに遭遇する前に参考になれば幸いです。
-
moment()
にjsDate objectでも適当なフォーマットでも突っ込むと割と大味に解釈してくれるところが逆に怖い ↩︎
Discussion