こんにちは。Backlog開発チームの砂川です。
現在BacklogではフロントエンドのReact/TypeScriptへのリプレイスを進めています。既に一部の画面(課題検索画面、課題詳細画面)がリプレイスされ、本番環境で稼働しています。
この記事では、Reactへのリプレイスによって素早い機能開発ができる状態を実現しようとしていること、リプレイスしたコードを安心して触れる様にするために導入したものをご紹介します。
目次
本題の前に: 表示速度も大幅に高速化
本題の前に今回のリプレイスによる副産物を紹介しておきます。今回のリプレイスは機能開発を楽にすることが第一目的ですが、リプレイスの過程で一部SPA化もしているため、ユーザーの体験も向上させることができました。
課題検索画面と課題詳細画面をSPA化したことによって、この二画面間では、移動の際にバックエンド側でHTMLを生成する必要がなくなり、事前に取得していたデータを他の画面で利用することが容易になりました。その結果、課題検索画面で課題をクリックしてから課題詳細画面が表示されるまでの時間が、900ms程度から50ms程度まで短縮されました。大体18倍速です。
今後他の画面にもReactへのリプレイスを横展開していくことで、同様の速度改善が見込めると考えています。
リプレイスの背景/なぜReactに置き換えたのか?
さて本題です。実はBacklogのフロントエンドの開発環境は、やりたいことに対して時間がかかり過ぎ、バグを生むリスクも高い状態になっていました。このプロジェクトは、その状態を解決し、フロントエンド開発のスピードを高めることを目的として立ち上げられました。
既存の開発環境では開発中に不安が付きまとっていた
- コードが追いにくい
- 機能の影響範囲がわかりにくい
- どこで上書きされているかわからないグローバル変数
- ホットリロード機能を利用できない
- Webに情報が少ない
- フロントエンドの自動テストが整備されていない
既存のフロントエンドはKnockout.jsとHaxe、一部jQueryと素のJavaScriptで構築されていますが、十年以上の開発を経て様々な開発者が手を入れ続けたことにより、開発のしにくい典型例のようなコードになってしまっていました。
既存のコードは、この機能に手を入れたいとなってから変更すべき場所を特定するのに時間がかかり、変更をした場合の影響範囲も都度調べる必要があり、変数にきちんと値が入ってくるかもコードを追って確認しなければなりませんでした。そして、ブラウザで動作確認をするためには都度トランスパイルの長い待ち時間が必要です。(長い、とにかく長い)このような開発環境では安心して楽しく機能を開発することなどできません。
このような状態になってしまったのはHaxeという言語のせいではありませんが、フロントエンドの開発をしやすい状態にするためには今あるものを使い続けるよりも作り直した方が良いと判断し、リプレイスに着手しました。
ちなみに、リプレイスの前段階として、既存コードの整理も兼ねてリファクタリングは実施していて、可能な範囲でのグローバルな変数の整理やスコープを小さくするという改善はしています。
React/TypeScriptで開発に安心と楽しさを
- 情報が多い
- 静的型付け言語
- コードが追いやすい
- スコープが自然に狭くなる
- 単体テストが書きやすい
- ホットリロードあり
リプレイスするにあたりReactを選択した理由の一つは、まずはなんといっても2022年現在最もメジャーなライブラリの一つであり、情報やプラクティスが豊富という点です。
次に、Reactの流儀に則りコンポーネント単位で記述していくだけでコードのスコープと影響範囲を制限できるため、既存のフロントエンドのように無秩序な状態になるのをある程度防げるのも嬉しい点です。また、ロジックとDOMを同じファイルに書くので、対応するコードを把握しやすいところも個人的にはメリットだと感じました。
モダンなフロントエンドライブラリには大抵用意されていますが、ホットリロード機能を利用するのが簡単な点も、開発者の体験として大きなメリットです。実際にホットリロード機能を利用して開発すると背負っていた重りを外したような気分になり、開発を楽しく感じられました。
開発者が安心して書けるフロントエンドにしたい
特に気を配っているのは、開発者が安心して書けるフロントエンドにしたいということです。コードの影響範囲が明確で、追いやすく、変数や型のことを信頼できる状態です。
現在リプレイスされているのは課題検索/詳細画面のみですが、他の画面へ横展開していくことも考えると、様々な開発者が触っても散らからないように、継続的に気を配り続けなければなりません。
導入したライブラリ/設定/ルール
今回の記事では、TypeScriptにおける型を信用しやすくするために導入したものを中心に紹介します。
myzod
APIから取得したデータが正しい型であることを保証するために導入しました。
const issueSchema: Type<Issue> = z.object({ id: idSchema, projectId: idSchema, issueKey: z.string().min(1), keyId: idSchema, summary: z.string().min(1), description: z.string(), formattedDescription: z.string().nullable(), status: statusSchema, }) issueSchema.parse(data)
上記のようにオブジェクトのschemaを定義し、parseすることで型付けと検証をしてくれます。APIからデータを取得した時に必ずparseを通すことで想定通りのデータが取得できているかを検証して、その時点でエラーを検知できます。これによって想定外のデータが来た場合の調査が楽になります。
またこの時点で型付けが完了しているので、実際にデータを扱うロジックの中で考えることを減らせます。
no-non-null-assertion
Backlogのフロントエンドは30人以上の開発者が触る可能性があり、フロントエンド開発に関する技術力もまちまちです。TypeScriptでは基本的にnullableな型はそれを明記する必要があるので、例えばstring型の変数には「基本的には」null/undefinedになることはありません。しかし書き方によっては、null/undefinedが想定されていない型の変数にnull/undefinedが入るということが起こり得ます。その一つが非nullアサーション演算子(non-null-assertion)で、例えば hoge! の末尾についている ! がそうです。
非nullアサーション演算子は T | null | undefined な変数の型を T とみなせるようにする演算子で、null/undefinedの可能性があるTのプロパティを参照したい場合に hoge!.name と簡潔に描けるようになります。
非nullアサーション演算子は、nullでないことが明らかである場合にnullチェックの記述を省けるなど有用なケースもあるのですが、後からコードを読む人にとっては、その変数に本当にnullが入ってこないのかを確認する必要があり、考えることが増えてしまいます。また、想定外のケースによりnullが入ってきた場合、後のコードはnullを想定せずに書かれるため、ほとんどの場合実行時にどこかでエラーが発生してしまいます。
このようなケースで発生するエラーは実際に動かしてみるまで発見されませんし、原因特定に時間がかかるパターンに陥りやすいので、事前に何かしらのエラーとして教えて欲しいところです。
no-non-null-assertionは非nullアサーション演算子が使われるのを禁止するeslintの設定で、コードを書いている時点でエラーにしてくれるので、想定外のnull/undefinedによるエラーを実行する前に防げます。明示的に値をチェックをする必要があり、コードが冗長になるというデメリットはありますが、メリットの方が大きいと判断しました。
noUncheckedIndexedAccess
想定外のnull/undefinedに悩まされるケースはまだあります。
const issueIds: number[] = [0,1,2] issueIds[2] // 2 issueIds[3] // undefined(ただしnumber型として扱われる)
TypeScriptでは、上記のようなnumber型の配列がある場合、issueIds[3]はコンパイルエラーにも実行時エラーにもなりません。コンパイル時にはnumber型の値として解釈され、更に実行時にはundefinedとして評価されます。
noUncheckedIndexedAccessは、tsconfigで有効にすると、。配列アクセスの返り値の型を T | undefined として扱ってくれる設定です。
undefinedのチェックが必要になる分、コードは多少冗長になってしまいます。が、しかし、仮にこの設定を有効にしていないとしても、結局開発者がコードを書くときには、その値がundefinedかどうかを気にすることになるのです。言語レベルでルールを守らせることで、長期的には可読性を高めてバグを減らすことに繋がると考えています。
ReactQuery
非同期なAPIとの通信まわりの状態管理とキャッシュ管理をまとめられるのが便利なライブラリです。Reactへのリプレイスの第一の目的は「素早い機能開発ができるようにする」ことなのですが、冒頭に紹介したように、リプレイス後は動作の体感速度も大きく上がっています。
冒頭のパフォーマンス改善には、それほど意識しなくても良い感じにキャッシュを取り扱ってくれるReactQueryが大きく貢献しています。
まとめ
Backlogは、フロントエンドの開発環境改善を通して、今後はより素早く、ユーザーの皆さんに使いやすい改善をお届けできるようにしていきたいと考えています。これからもよろしくお願いします。