React17から18へバージョンアップの奮闘記
こんにちは。スペースマーケットででフロントエンドエンジニアをしていますwado63です。
スペースマーケットではゲスト向けのサービスサイトである https://www.spacemarket.com/ を社内フレームワークから、Next.jsへと移行を進めております。
Next.js製の新しいリポジトリの1commit目が2021年3月1日だったので、もう少しで2年ですね。
主要ページの移行作業が進んで日々の開発体験がよくなってきており、移行を作業を進めて本当に良かったなと思います。
今回のお話は今年の7月に行ったReactを17から18へバージョンアップした時のお話です。
詳細な内容まで書き切れないとは思いますが、こういうことがあるんだなという参考事例として読んでいただけると幸いです。
1.準備
ReactをアップデートするPRを作成する前に、以下の作業をそれぞれ個別PRで行いました。
jest実行時のconsole.error, warningのお掃除
Reactをアップデートする際にtesting-library/reactも合わせてバージョンをv12からv13に上げる必要があります。
React Testing Library versions 13+ require React v18. If your project uses an older version of React, be sure to install version 12:
https://github.com/testing-library/react-testing-library
既存のconsole.error, console.warningがあると、バージョン上げた際に発生したエラーの原因の切り分けがしづらいので予めお掃除しました。
当たり前のことでしたが、常に綺麗にしておくことが大事ですね。
この時は
[MSW] Warning: captured a request without a matching request handler …
→ requestのmockし忘れ
Warning: An update to GlobalModalProvider inside a test was not wrapped in act(...) …
→ 意図しないタイミングでのstateの更新が発生
などを修正しました。
不必要なライブラリがないか見直し
Reactアップデート起因のバグの可能性を少しでも減らすため、package.jsonのdependenciesに不要なライブラリが入っていないか見ていきます。
ここも普段から整理されていれば何もしなくてすみますね。
スペースマーケットではstyled-componentsからChakra-UIに乗り変えたのですが、styled-components時代に使用していたアニメーション用のライブラリreact-transition-groupの更新をしておらず、React18に上げた際にエラーが出ていました。
styled-componentsを外す際にEmotionのstyledに置き換えるという対応をしたので、react-transition-groupが残っていたんですね。
幸い使われている量が少なかったのでChakra-UIとセットになっているframer-motionに置き換えたり、CSSのtransitionに書き換えたりしてライブラリをお掃除しました。
framer-motionはbundleサイズが大きくなってしまうのでLazyMotionの機能を使用して、アニメーション機能を分けて読み込むようにしています。
型の修正
React17でVFCを使用していたのでFCに予め置換しておきました。
VFCという文字列が分かりやすく簡単に置換できましたし、正しく置き換え出来ているかもtscを叩くだけなのですぐに終わります。
StrictModeの変更
ReactのStrictModeが有効だとuseEffectのcleanUpが2回行われてしまうという話がありますが、開発限定の機能なのと、お試しにバージョン上げた際に大して気にならなかったので、予め設定を変更しておきました。
module.exports = {
reactStrictMode: true,
}
『開発時に挙動が変だったら一時的に設定をfalseにして確認すればいいよね』という話を開発メンバーとして、えいやで切り替えました。
2.Reactアップデート
Reactをアップデートした際のPRについてです。
本番稼働するコードは予め修正しておいたので、ここではpackageのアップデートと@testing-library/reactがバージョンアップしたことに合わせテストを修正していきます。
packageのアップデート
React18へのアップデート、
React18に対応した@testing-library/reactへのアップデート、
そして@testing-library/reactがrenderhookを用意してくれたので、@testing-library/react-hooksを削除します。
yarn add react react-dom @types/react @types/react-dom
yarn add -D @testing-library/react
yarn remove @testing-library/react-hooks
テストの修正
@testing-library/react-hooksを@testing-library/reactに置き換えます。
- import { renderHook } from '@testing-library/react-hooks'
- const { result, waitFor } = renderHook(() => useSomeHooks())
+ import { renderHook, waitFor } from '@testing-library/react'
+ const { result } = renderHook(() => useSomeHooks())
hooksのRenderResultの型はRenderHookResultという名前に変わりますのでお気をつけください。
- import { renderHook, RenderResult } from '@testing-library/react-hooks'
+ import { renderHook, RenderHookResult } from '@testing-library/react'
@testing-library/react-hooksにはテスト用のUtilitiesとしてwaitForNextUpdate
,waitForValueToChange
という関数が用意されているのですが、
@testing-library/reactには用意されていないので、予めwaitFor
で書き換えておいた方が修正が簡単になります。
(Reactアップデートの時は事前に気づいておらず時間取られました。)
謎のuserEventのactエラー
React18にアップデートした際に、なぜかuserEventを使用している箇所で
以下のようなactのエラーが出るようになりました。
Warning: An update to GlobalModalProvider inside a test was not wrapped in act
アップデート当時は原因が分からず、諦めてひたらすらuserEventを使っている箇所にactをつけて回ったのですが、最近になってやっと原因が分かりました。
testing-libraryのissueを漁っていたところ@testing-library/domが怪しいということが判明。
yarn why @testing-library/dom
で調べたところ、
- @testing-library/react
- @storybook/testing-library
で別々の@testing-library/domを使っていることが発覚しました。
別な@testing-library/domが存在するだけでこのactの問題が発生するようです。
package.jsonのresolutionsでバージョンを揃えたところ解決しました。
再現するかの確認のため、新しくプロジェクト作って、@testing-library/react、@storybook/testing-libraryを入れてためしてのですが、@testing-library/domが二つになることはありませんでした。どちらかの入れ方が間違ってたのかなと予想してます。
3.リリース
実装が終わったので、どのようにリリースするかについてです。
スペースマーケットでは主に以下の環境を用意しているので、これらの環境を使用して最終的な動作確認をしリリースします。
- sandbox環境: 開発者向けのテスト環境。データもテスト向けのもの。
- staging環境: 本番環境とデータを持つ、リリース前の確認用の環境。
- production環境: ユーザーが閲覧できる環境。
マニュアルテスト
jestでのカバレッジが9割あったのと、Next.jsに移行しているページが、検索ページや詳細ページのような表示を中心としたページだったので、細かいテストケースを作ってテストするよりも開発メンバーで数時間ブラックボックステストを行いました。
ここでは特にバグを見つけられなかったので次へ進みます。
E2Eテスト
スペースマーケットではAutifyというSaaSを使用しており、staging環境で規定のシナリオに沿ってE2Eテストを行っています。
ここではスペースマーケットの基本的な機能である、検索できることや詳細ページが閲覧できることなどをテストしてサービス影響の大きい障害を起こさないようにチェックしています。
E2Eテストでもエラーはありませんでしたのでリリースへ進みます。
リリース作業
リリースに関しては課題感があります。
出来ることならばカナリアリリースをし、少しづつ対象ユーザーを増やしながらちょっとでも問題あればすぐに切りもどすということをしたかったのですが、スペースマーケットではカナリアリリースをするためにちょっと準備が必要になります。
マニュアルテストでエラー件数が0だったので問題ないと判断し、本番と同じデータを使用しているstaging環境でもポチポチバグが無いか探索した後、普段の開発と同じようにリリースしました。
結果として致命的なエラーは発生しなかったのですが、後述のエラーが発生したので一度リバートを行いました。
4.リリース後バグ対応
リリース後に発覚したバグについてです。
Next.jsのHydrationエラー
Hydration failed because the initial UI does not match what was rendered on the server.
リリース後、Sentryのエラー通知で発見しました。
Next.jsで生成したHTMLをクライアントサイドでHydrationする際HTMLが一致しないというエラーです。
このエラーはReactをアップデートしたから発生したのではなく、Reactをアップデートしたことによってproduction buildであってもconsole.errorを出力されるようになったものでした。
エラー出た際、React上げたからか!?と焦ったのですが、そんなことはなく既存のコードのお作法が悪かっただけでした。ただし、このことを知らないとかなり焦ることになります。
実際に起こった事象としては、検索で利用日時を表示する箇所ところのタイムゾーンの取り扱いが間違っておりました。SSR時には間違った時間を出力するのですがHydration後は正しい日時に変わるという挙動をしていています。
UTCとJSTで日付が異なるときのみ発生するときのみという気付きづらいバグです。
日付の部分がクライアントサイドでJSが動くと正しい日付に変わります
ぱっと見おかしいところがないので特定に時間がかかりましたが、devtoolsでJSの実行を止めた表示とJSを実行した際のHTMLを比較して突き止めました。
修正:Dayjsのタイムゾーン指定
SSRとCSRでタイムゾーンを合わせるためにtz()を変換するメソッドを挟んで修正しました。
- date.format("MM月DD日")
+ date.tz().format("MM月DD日")
再発防止策1:開発時のタイムゾーン設定不足
スペースマーケットではローカル環境でNext.jsのdevサーバーを立ち上げて開発しています。
ローカル環境ではタイムゾーンがJSTになっており、本番環境のUTCと異なりました。
なので、ローカル環境でも本番のタイムゾーンに合わせる設定を加えました。
"dev": "TZ=utc next dev"
再発防止策2:Sandbox環境でのSentryへのエラー通知
いままではsandbox環境で起きてたエラーをハンドリングしていなかったのですが、テスト環境であってもSentryに通知するようにし、画面に表示されないようなエラーも早期に発見しやすいように改善しました。
振り返ってみて
Reactのアップデート作業に着手してからリリース完了まで2週間程度かかりました。
普段からリファクタリングをして綺麗な状態を保ち、Hydrationエラーあらかじめ消しておけば工数的には半分で済んだろうなと思っております。
とはいえトラブルがありながらもこの程度で済んだのは助かりました。
カナリアリリースは必須だなと思うのと、jestによるテストやe2eテストを充実させておくと安心感が違いますね。
もしこれからReactをアップデートする方がいらっしゃいましたら、この記事を踏み台にしていただけると幸いです。
最後に
スペースマーケットでは、一緒にサービスを成長させていく仲間を探しています。
開発者だけでなくビジネスサイドとも距離感が近くて、風通しがとてもよい会社ですのでとても仕事がしやすいです。
とりあえずどんなことしているのか聞いてみたいという方も大歓迎です!
ご興味ありましたら是非ご覧ください!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion