フロントエンド領域の技術選定ふりかえり

システム技術部の野阪です。以前カブコムにおけるフロントエンド領域の技術選定について紹介しました。今回はそのふりかえりとなります。

engineering.kabu.com

プレスリリースにてお知らせしている 通り、貸株サービスのASP提供を証券会社及びそのお客様向けに行っています。お客様向け画面及び管理画面を紹介した技術スタックで構築しています。貸株サービスは2022年2月にリリースをしましたので実際に採用した技術についてふりかえりをしていきたいと思います。

TypeScript

TypeScriptで静的な型システムを導入しました。

今回の開発ではTypeScript経験者の多いチームだったため障壁なく導入、利用することができました。any型を厳密に禁止とする運用にはしませんでしたが9割以上は型がついている状態で開発でき、IDEにおける入力支援や実行時の型エラーによる手戻り防止など多くの恩恵が得られました。また、後述するスキーマファーストな開発とも非常にマッチしており品質を保ちつつ効率的に開発することができました。

Boilerplate

Boilerplateとして create-react-app を利用しました。

大きな問題は起きませんでしたが、今後はNext.jsにシフトしていく予定です。ファイルシステムベースのルーティングやCode Splittingによるバンドルサイズ削減、ページごとにSSR/SSG/CSRを選べる柔軟さなど多くの採用メリットがあると思っています。

特にルーティング周りでは今回コードの見通しの悪さが気になりました。ルーティングには React Router v5 を利用しましたが、ルーティング制御しているコードとルーティングごとにtitleタグの書き換えをしているコードが離れた場所に書かれており見通しの悪いコードとなってしまいました。Next.jsではパスの階層設計やheadタグの書き換えが直感的に行えるため、このような苦労が少ないものと考えています。

今回create-react-appを採用した背景には2018年の技術選定があり、当時のNext.jsはSSR用という印象が強く採用を見送った経緯があります。現在ではNext.jsも成熟しSSR以外の利用においても多くの採用メリットがあるため、今後はNext.jsの採用を推し進めていく予定です。(2018年のプロジェクトは様々な外的要因で凍結されました…)

CSS in JS

CSS in JSとして styled-components を利用しました。

CSSのスコープ制御やCSS in JSでありながら本来のCSS構文に対するIDEによる入力支援が効くなど多くの恩恵がありました。ただ、styled-componentsで装飾したコンポーネントが純粋なHTMLタグなのかReactで作成したコンポーネントなのかわかりづらいケースがあり、今後は Emotion によるcss Propを使う形に変えていくことも考えています。

UIコンポーネント

UIコンポーネントとして Blueprint を利用しました。

基本的なUIコンポーネントが提供されており開発工数を大きく圧縮することができました。また、IE11もサポートしているためIE11対応の工数が少なく済んだのも嬉しい点でした。

管理画面では React Hook Form と合わせて利用しました。管理画面のように入力項目が多くなりがちな画面ではUncontrolledにFormを扱えるReact Hook Formがマッチしていました。

以下はBlueprintのInputGroupをReact Hook Formで扱う例です。

import { IInputGroupProps2, InputGroup } from '@blueprintjs/core';
import { Control, FieldName, RegisterOptions, useController } from 'react-hook-form';

type TextInputProps = IInputGroupProps2 & {
  name: FieldName<Record<string, string>>;
  control: Control<Record<string, any>>;
  rules?: Exclude<RegisterOptions, 'valueAsNumber' | 'valueAsDate' | 'setValueAs'>;
};

const TextInput: React.FC<TextInputProps> = (props) => {
  const { name, control, rules, defaultValue, ...textProps } = props;
  const {
    field: { ref, ...controllerProps },
  } = useController<Record<string, any>>({ name, control, rules, defaultValue });

  return <InputGroup {...controllerProps} {...textProps} inputRef={ref} />;
};

export default TextInput;

状態管理

状態管理としてReact Contextを利用しました。技術選定時はReduxと併用としていましたが、最終的にはReact Contextのみとなりました。

認証状態の管理や検索画面の入力項目保持などに使いましたが特に不便なく扱えました。 useContext の登場によってContextが扱いやすくなったためよほど複雑なことをしない限りはReduxなしで十分な印象でした。

非同期処理

非同期処理には React Query を利用しました。

データ取得時の画面制御を少ないコード量で記述でき効率的に開発を進めることができました。

以下のように useQuery に共通的なエラーハンドリングを追加したカスタムフックを作成し利用しました。HTTPクライアントにはaxiosを利用しており、axiosのInterceptorでレスポンスに応じたErrorオブジェクトをthrowし、ここでカスタムフックを用いたログアウト処理を呼び出すなどしていました。

export function useAxiosQuery<TResult>(
  queryKey: QueryKey,
  queryFn: QueryFunction<TResult>,
  queryConfig?: UseQueryOptions<TResult, Error | AxiosError<TResult>>
) {
  if (!queryConfig) {
    queryConfig = {};
  }
  const tmpOnError = queryConfig.onError;
  queryConfig.onError = (err) => {
    if ( /** 独自に制御したいエラー条件 */ ) {
      // do something
    }
    else if (tmpOnError) {
      tmpOnError(err);
    }
  };
  return useQuery(queryKey, queryFn, queryConfig);
}

スキーマ管理

ReactアプリケーションとBFF間のスキーマ管理には openapi-generator によるコードの自動生成を利用しました。

自動生成した型を利用してReactアプリケーションとBFFを実装していくことでスキーマが乖離することなく開発を進めることができました。

BFF側では openapi-validator-middleware を用いた簡易的なValidationも行え、導入時の手間はありますが入れてよかったと思っています。

まとめ

1年半前に行った技術選定のふりかえりを行いました。概ね選定したものをベースにしつつプロジェクトに活かせたのかなと思います。今回は少人数のチームであったためフレキシブルにプロジェクトを進めることができました。大規模な開発となった際、どのように作業を分担し効率的に進めるかが課題となるため今後考えていきたいです。

私の属するアプリ基盤グループではフロントエンドのみに限らずあらゆる技術スタックの標準化に取り組んでいます。他領域の標準化活動についても別の機会に紹介したいと思います。