こんにちは、クレイの阪本です。
もともと外部委託パートナーとしてクレイ案件のお手伝いをしていましたが、気づけば中の人となっていました。
よろしくお願いいたします。
先日、DocBaseはフロントエンド構成を Backbone.js+Coffeescript から React.js+TypeScript へ移行しました。大改修です。
どれくらい大きな変更だったかというと、10万行(2500ファイル)もの変更が行われ、それまでRubyだったはずのリポジトリ代表言語がTypeScriptに替わってしまったほどでした。
なお、2021/3/31のリリースでリニューアルすべてが終了したわけではありません。今後、機能拡張やUI改善をしやすくするための足がかりという位置づけです。
安全にリリースするためテスト期間を多めに取ったこともあり、期間としては1年ほどかかってしまいました。
今回はどのようにリニューアル作業の計画や進行をおこなったかをまとめてみました。
リニューアルの動機
DocBaseは2014年にサービスを開始してから7年ほど運営を続けています。
フロントエンドの構成は Backbone.js v1.2.3 + Marionette v2.4.x と、最新のバージョンを追えていませんでした。しかもMarionetteはメジャーバージョンアップがおこなわれ、追従するには大きな変更をしなければならない状況となっていました。
- 古いバージョンのBackboneをメンテナンスしていくためには技術者の必要要件が高まってしまう
- Backbone.jsでは難しい要件が増えてきた
- TypeScriptによる型システムを取り入れてより壊れづらいサービスにしたい
- RailsのSprocketsだけでは難しい開発環境の整備をWebpackへ移行したい
などの動機から、フロントエンド構成のリニューアルをおこなうことを決めました。
バックエンドも含めて構成を考慮する必要があったので、Backboneでも取り入れていたSPAは継続しています。
移行の計画を立てる
ブランチコントロール
まずリニューアル作業中のブランチ運用をどうするか決める必要がありました。
- リニューアルしたコードをmasterブランチにマージして開発を進行する
- 開発用ブランチを作り、リニューアル後のコードはそちらにマージして開発していく
1.を実現するには、Backbone/Reactを跨ぐリンクをクリックしたときにスムーズに遷移できるよう工夫する必要があり、また開発途中のデグレをカバーするテストも追加する必要がありそうでした。なので早々にBackbone/Reactが本番に同時共棲するリリース方針は諦めました。
そのため2.のBackboneが生きているブランチはmasterブランチ、Reactへの移行ブランチは新たに作ったdevブランチを使い、リリースするタイミングでdev->masterへマージ・リリースする方針を採用しました。
はじめは特定チームのみに新実装を適用して安全なリリースを心がける
巨大な変更となるため、リニューアルを全チームに一気に適用するのはリスクが大きいと判断しました。
そのため、まずは特定のチームにだけ新実装を適用できる仕組みを作成しました。
リニューアル作業初期はクレイで実際に使用している1チームにだけ適用し、徐々にクレイのクライアントやヘルプセンターで募集したβテスト参加チームにも適用を拡大しました。βテスト利用チームから問題の報告をいただくこともあり、大変助かりました。
ご協力いただいたチームの方々、まことにありがとうございました
新実装移行に際しての変更領域について
長期の計画だったため、バックエンドは以下の理由でリファクタリングや構造変更を極力行わないようにしていました。
- 問題なく動作するマイグレーションを作成する必要がでてくる
- バックエンドのアプリケーションロジックの違いを長期に渡って把握し続けなくてはいけない
- 切り戻しに際して適用コードを変更するだけでなく、想定外のデータ変更があった場合にデータも修復する必要がでてくる
また、UIもリニューアルをリリースするまで極力大きな変更は行わないようにしました。
使い慣れたシステムのUIや挙動が突然変わることは、ユーザーに想定以上の違和感を与えてしまうことがあるため、UIに関わる変更は一度リリースを迎えてから手を入れることにしました。
バックエンド・フロントエンドに関わらずより良いシステムにするための意欲をグッとこらえ、まずは新実装をリリースすることに注力しました。
逆にいうと「え、いつの間にそんな大きな変更が入っていたの?」 と思われるくらいに自然なリリースをおこなうことが、リニューアルの成功指標の一つでもありました。
開発作業のタスク化
フロントエンドのReact化を進めるにあたり、私含め3名のメンバーがDocBaseの開発に新たに参画していました。つまりその3名はDocBaseのシステム構造を何も知らないままリニューアル作業に入ることになります。
DocBaseの開発ではタスク管理にPivotalTrackerを利用していますが、すべてのタスクを一度に登録するのはあまりに時間がかかります。新メンバーがDocBaseの機能を把握する作業も兼ね、まずはSpreadSheetでDocBaseでできる操作・機能をページごとにリスト化することから始めました。そこからストーリー(タスクの最小単位)を登録していきました。
週に1度、認識のすり合わせをするためミーティングを実施
既存のメンバーもすべての機能を実装したわけではなかったので、1週間に1度時間を取り、各ストーリーで達成するべきより詳細な要件を話し合いました。
- 正常・異常パターンの洗い出しを補足
- 画面上でリクエストするべきAPIのエンドポイントやIN・OUTパラメータ例を補足
- 権限など条件別に表示されるものやUI挙動の違いを補足
- master実装から仕様が異なるが明らかに改善が見込める処理を補足
- 前述した内容と乖離はありますが、影響がほぼない箇所は改善してただの作り変え感だけにならないようにモチベーションを保つことも大事でした
- 隠れ機能(誰も知らなかったショートカットがコード上から発掘されることなどがあります……)
またミーティングの際には音声だけではなく、画面操作やコードも画面共有するようにしていました。音声以上に理解や説明が捗るため、意識しておこなっていました。
オンライン上でコードを追いながら、気づいた箇所は即ストーリーに補完しました。話し合いの途中で判明した仕様や認識のズレを共有でき、ミーティング自体に時間はかかったものの大きな効果がありました。
開発サイクルを確立
Backbone.js+CoffeeScriptの構成では型の概念がないので、エディタでのコードジャンプなどの恩恵が得られません。そのため既存の実装を追いかけるのが困難なうえ、各人ごとに捉え方も異なる状況でした。
React+TypeScriptの構成に移行していく中で、型が定義されたことによりコードジャンプが容易になりました。各人が実装を深く理解できたことも加わり、実装漏れの危険性が大幅に減りました。作業の引き継ぎもしやすい構成になったと思います。
- 1週間のストーリーのプランニング、登録済みストーリーの詳細要件追記
- ストーリーの内容を実装
- 検証環境への自動デプロイ
- POによる受け入れ
1週間ごとにこれらのサイクルを回すことで、機能実装がスムーズに進むようになりました。
各ページの機能実装については工程を分けた
当初はすべてのページがAtomicDesignの分類+styled-componentsで実装される想定でした。
しかしながら、実装中には構造作業をブロッキングするようなページも現れてきます。理想としてはAtoms->Molecules->Organismsの順に実装を進めるべきでしたが、作業の並列度を上げるため、先にOrganismsとして正しく動作する状態を作る->Atoms,Moleculesに分割、といった流れで進めていました。
旧グローバルCSSによる問題
React化の実装とstyled-components化を進めるにあたってひとつ問題がありました。旧来のグローバルCSSの中にはstyled-componentsに好ましくない影響を与えるスタイルが含まれていたのです。しかしリニューアル作業中に旧CSSを完全にオフにしてしまうと、開発に支障が出てしまいます……
とはいえ旧CSSが残り続けるのも作業に支障が出るので、環境変数によって新CSSと旧CSSを切り替える機能を実装しました。必要に応じて旧CSSと新CSSを切り替えながら、慎重に作業を進めました(某開発パートナーさんの提案)。
styled-components化をほとんど終えた時点でこちらの機能は削除されました。
既存のe2eテストをそのまま使い、品質担保を継続させる
Backbone時代にCapybara+RSpecによるSystemTestを作成しており、Reactへの移行後も同じものをそのまま使うことにしました。旧システムと同じテストを使うことにより、テストの品質を担保できるうえ、モックが必要なテストを記述し直す工数を削減できると考えたためです。
一部Rspecのコードを抜粋すると、要素を特定するためにjs-プリフィックスのセレクタを多用していることが分かります。
it 'メモ詳細から検索できる' do
visit post_path(post2)
wait_for_ajax
find('.js-search-icon').click
find('.js-search-form .test-search-box').click
wait_for_ajax
find('.js-search-form .test-search-box').send_keys('tag:魚', :enter)
within '.js-content-main' do
expect(page).to have_content 'かつお'
expect(page).to_not have_content 'わかめ'
expect(page).to_not have_content 'せわし'
end
end
Rspecのコードは開発初期にitをxit, describeをxdescribeなど一括でpendingさせておき、実装が完了した機能から再度テストを実行する方針にしました。
js-プリフィックスはDOM要素とBackbone.js上のプロパティを紐付ける役割で利用されていましたが、Reactで実装することにより.js-プリフィックスがなくても動作するようになった箇所が多数ありました。
テストにのみ使用されている.js-プリフィックスは.test-プリフィックスへと置き換えるようにしました。テスト以外の.js-プリフィックスは不要になるのでReact実装では削除してOKになります。
合わせてbabel-plugin-react-remove-classnameも利用し、productionビルドには必要ない.test-プリフィックスをビルド時にshrinkするようにもしました。
もとのDOM構造をなるべく保ちながらReact化
DocBaseでは機能実装とスタイリングを分業していたため、エンジニアがReact化->マークアップエンジニアがstyled-components化 の流れになっていました。
そのため、エンジニアがBackboneをReact化する時点では旧SASSのスタイルをそのまま引き継ぎました。
例えば「タグから探す」のタグ未設定のリストアイテムは
最初は旧SASSのクラス名と同じDOM構造を維持してReact実装をおこないました(旧SASSはFLOCSSの命名パターンを採用していました)。いったんは表示の担保ができ、前述したテストに必要なセレクタも残すことが可能です。
- before
<div class="-type-undefined js-untagged sub-menu__result-list-item is-active">
<a href="/untagged">
<span class="js-name sub-menu__result-list-item-name">タグ未設定</span>
<span class="js-count sub-menu__result-list-item-count">336</span>
</a>
</div>
ここまでテスト・デザインが出来あがったあとに、理想のコンポーネント粒度に分類するタスクを切り出しました。
テスト・デザインが出来上がったら理想のコンポーネント粒度に分類する
AtomicDesignに則ったコンポーネント分類をおこなったあと、マークアップエンジニアがstyled-componentsで実装し直します。
- after
<ul>
<li class="tags_sub_menu__SubMenuItemUnTagged-sc-12a3ysf-0 MffkJ">
<a class="_sub_menu_item__ItemLink-sc-1m3q5a-0 fVxgko" href="/untagged">
<span class="_sub_menu_item__NameWrapper-sc-1m3q5a-1 Mbtei">
<span class="_sub_menu_item__Name-sc-1m3q5a-2 gSOJsc">タグ未設定</span>
</span>
<span class="_sub_menu_item__Count-sc-1m3q5a-4 ksXYOQ">336</span>
</a>
</li>
</ul>
実際のReact実装としてはコンポーネントをぽんと置くだけでこれらが展開されるようになっています。
<>
<SubMenuItemUnTagged
link="/untagged"
name={ unTagged.name }
isActive={ activeTag === unTagged }
count={ unTagged.postsCount }
/>
</>
PC用、モバイル用でテンプレートを分けていたのをレスポンシブに変更
Backbone.js時にはPC表示とモバイル表示で完全にテンプレートを分けており、ユーザーエージェントで判定してテンプレートを切り替えていました。この方法では1つ変更が入るとPC用とモバイル用2つのテンプレートを変更する必要があり、工数が増えたり実装漏れが起きる原因となっていました。
新実装ではPCとモバイルでテンプレートを分けるのを廃止し、1つのテンプレートをレスポンシブ化しました。
旧SASSからstyled-componentsに変更する作業は引き続きおこなっていますが、設定画面以外の主要なページはリリースのタイミングで上記の工程が終了した状態になっています。
機能実装以外の環境整備
これ以外にも機能実装に際してデグレを防ぐ/レビューを容易にする/品質を統制する施策として以下のものを適宜整備していきました。
- Sprocketsを捨てWebpackerに移行
- AtomicDesignにおけるコーディングルールの策定
- JSメインでコーディングを行うエンジニア向けのスタイリングルールの策定
- StyleLint/EsLint/CI環境の整備
- コードベースの肥大化に伴う開発環境
- 分類したコンポーネントをStorybookでカタログ化
- StorybookをPRごとに確認できる仕組みの作成
- Storybookのコンポーネントの変更をキャプチャ/コードベースでリグレッションテストをおこなう環境作り
- キャプチャベース: reg-suit+storycapによるgithub actionの作成
- コードベース: jest snapshotによるもの
- JSレイヤの例外通知を取り入れていなかったのでSentryを導入
書き上げるとキリがなく、どれも各々に工夫が必要な箇所があったので、こちらも機会があればお伝えしたいと思います。
本番環境での検証
2020年3月〜主要ページの開発が終了するまでの12月になったタイミングで前述した特定チームへの新実装のデプロイを行いました。
DocBaseではチームのURLが https://${チームドメイン}.docbase.io/ のようになっています。特定チームのみに反映するためReact実装用のインスタンスを立て、AWSのALBでホストベースのリスナールールを設定しました。
バックエンドを極力変更しない方針が生き、想定通り特定チームのみに新実装が適用できました。Sentryの例外通知や、βテストにご協力いただいたチームからのFBに対応しつつ、遅れ気味だったstyled-components化も合わせて進めていきました。
全チームへリリース
そして2021年3月31日、すべてのチームへのリリースを実施することができました。
https://help.docbase.io/posts/1827699
しばらく経過観察を行いましたが、異常な高アクセスなども認められず、想定通りのリリースとなりました。
まとめ
長期間改修をしていたので、昨年は目に見える改善がなかなか行えませんでしたが、今後は皆さまに新たな機能やユーザー体験を提供できるよう計画中です。
今後ともDocBaseをよろしくお願いいたします!
DocBaseについて
10万行のコードを変更して改修した DocBase は社内・社外の垣根を超えて情報共有ができる情報共有サービスです。
細かい権限設定ができるので、オープンな情報もクローズドな情報も DocBase に集約できます。そして大きな企業さまでも全員で使えるよう、セキュリティには最も力を入れています。
Reactに改修後、高速化にも取り組んでいるので情報共有サービスの中でもサクサク使えるほうだと思います。
チームを作成して30日間は無料ですべての機能が使えるので、ぜひチームを作成して試してみてください。
このエントリーに対するコメント
日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)
- トラックバック
「いいね!」で応援よろしくお願いします!