新規サービス「FAANS」における、立ち上げからReact+TypeScriptのSPA開発を2年間運用した際に取り組んだ組織的・技術的な課題

OGP

こんにちは、ブランドソリューション開発本部フロントエンド部の田中です。
普段はFAANSのWebフロントエンドの開発を行なっています。

FAANSとは「Fashion Advisors are Neighbors」がサービス名の由来で、ショップスタッフの効率的な販売をサポートするショップスタッフ専用ツールです。

ショップスタッフ向けにコーデ投稿・成果確認などの機能が存在し、2022年8月に正式ローンチしました。詳しくは以下のプレスリリースをご覧ください。

corp.zozo.com

現在FAANSは立ち上げから2年経過し、Webフロントエンドの開発現場において様々な組織的・技術的課題がありました。
今回はその課題と取り組みについて紹介したいと思います。

目次

前提

まず前提としてFAANSの組織とWebのプロダクトの特徴について紹介したいと思います。

FAANSの組織の特徴

FAANSはWeb、iOS、Androidのプラットフォームが存在します。立ち上がって2年ほどで、スタートアップのような小規模なチーム(チーム全体で約15名)で開発をしています。

FAANSのWebのプロダクトの特徴

FAANSは導入が決まった企業のみが利用でき、検索エンジンには載らないログイン必須な業務ツールです。

したがって、SEOは意識せず、ページ遷移やユーザーによる操作などのインタラクションが多いのが特徴です。その特徴のもとユーザービリティの高い体験を提供するために、以下のようにクライアントサイドレンダリングをベースにして開発しています。

FAANSのフロントエンドの全体像

アプリケーションのコードをViteを使ってビルドし、生成されたHTML,CSS,JSなどのファイルをFirebase Hostingからブラウザへ配信する構成になっています。

配信後クライアントサイドレンダリングでページ遷移し、データが必要な場合はバックエンドのAPIを叩いてデータを取得するシングルページアプリケーションです。

初回表示はFAANSの全体のフロントエンドのファイルをブラウザに配信するため時間がかかりますが、その後のページ遷移に関してはクライアント側でレンダリングするため速くなります。FAANSはSEOを意識しない業務ツールで頻繁にページ遷移が発生することから、初回表示よりもその後のページ遷移の速度を重視してこのような構成になっています。

また現時点(2023年10月時点)で採用している主な技術スタックは以下の通りです。

  • TypeScript
  • React
  • Vite
  • Chakra UI
  • React Hook Form
  • Storybook

変化の多い環境下で遭遇し続ける課題

慎重にプロダクトに合わせたチームを形成・技術選定がされていたものの、運用していくにつれていくつか課題に遭遇しました。

後に紹介しますが、Webフロントエンドの開発チームが1人になり案件をさばける体制でない、立ち上げ当初は推奨されていた技術がメンテナンスに不安があるといった組織的・技術的にも取り組まないといけない課題がありました。

そのような変化がある環境下で、企業様が必要とする機能開発とバランスをとりながら、プロダクトチームと話し合い、優先度をつけてそれらの課題に取り組む必要がありました。

組織的・技術的課題とその取り組み

以降遭遇した組織的・技術的な課題とその取り組みについて時系順に紹介したいと思います。同じような課題感を持つ方の参考になれば幸いです。

課題1: UIコンポーネントの作成に時間がかかっていた

開発当初のFAANSのWebフロントエンドチームは2名体制で、styled-componentsを採用して1からCSSやJSを書いてUIコンポーネントを作成していました。

UIコンポーネントライブラリをFAANS用のUIにカスタマイズする案もありましたが、そのUIに引っ張られてカスタマイズに時間がかかる懸念から、当時は1から書く手法をとっていました。

しかし、モーダルのような複雑なUIコンポーネントを作成するときに時間がかかって、思うようにスピード感が出せてないと感じていました。少ないチームメンバーでスケジュール通りに機能を届けるためにも、そのスピード感を上げる必要がありました。

取り組み1: UIコンポーネントライブラリのChakra UIを導入した

そこでCSSライブラリの変更の舵を切るのに不安がありましたが、プロダクトやチームの特性を考えて途中でChakra UIを導入することにしました。主な理由は以下の通りです。

  • 機能豊富なコンポーネントが備わっていて、かつ、アクセシビリティが考慮されており、その土台がある状態からUIコンポーネントを作成できる。
  • UIの個性が強くなく簡単にスタイルを上書きできるのでFAANSのUIにカスタマイズしやすかった。
  • 自作していたstyled-componentsのUIコンポーネントに対して、marginTopやfontSizeなどのスタイリングをするために、Styled Systemを使って拡張していた。Chakra UIもStyled Systemを参考にしているため、スタイリングの際のpropsのインタフェースがほとんど同じで移行しやすかった。

実際に運用してみるとstyled-componentsからの移行もスムーズで、UIコンポーネントの作成にかかる時間を短縮できました。これは少ないチームで開発をする上で大きなメリットとなりました。

また選定時は意識していなかったのですが、Inputのようなフォームのコンポーネントを非制御コンポーネントとして扱えるのもFAANSのWebにとってメリットがありました。

FAANSのWebでは店舗登録やスタッフ登録画面などのフォームが多く、フォームの管理のためReact Hook Formを採用しています。主な採用理由は非制御コンポーネントに対しても扱うことができ、その場合にフォームのデータをDOM自身が管理するため、再レンダリングの回数を減らせるからです。

以下のようにReact Hook Formのregisterを使って、フォームのコンポーネントに対してrefを渡すことで、データが更新されたとしても再レンダリングされないようにできます。

制御コンポーネントを扱うControllerを使うのに比べてregisterでの登録は記載が短くすみ、可読性の向上に繋がりました。

import { forwardRef, Input, InputProps } from '@chakra-ui/react';

export const StyledInput = forwardRef<InputProps, 'input'>((props, ref) => (
  <Input
    height="42px"
    fontSize="13px"
    borderColor="gray.CCCCCC"
    borderRadius="4px"
    _placeholder={{
      color: 'gray.999999',
    }}
    ref={ref}
    {...props}
  />
))
<StyledInput
  {...register('externalUrl')}
  placeholder="例:https://faans.jp"
/>

課題2: FAANSのWebを開発しているメンバーが1人となり案件をさばけるような体制ではなかった

FAANSのWebフロントエンドの開発は2人で開発していたのですが、一時的に1人のメンバーが開発から離れ、自分1人になった時期がありました。

その時期にとある企業様のFAANSの導入を進めるにあたって、スケジュール優先で案件を着地させる必要がありました。

メンバーが自分1人になる前、FAANSのWebフロントエンドの採用へ繋げるための土台づくり(テックブログを書く・登壇する)をしたものの、エンジニアの採用は難しく採用へ繋げることができませんでした。
そこで機能のスコープを優先度が高いものに絞ったり、開発効率を向上するために自動化などの施策を試みたものの、メンバー1人では案件をさばける状況ではありませんでした。

余裕をもった上で案件を着地させるためにも、この状況下で適切な手段を考える必要がありました。

取り組み2: FAANS内の他職種の人に協力してもらう

そこでチームで解決策について話し合った結果、FAANSチームの他の職種からもWebフロントエンドの開発に協力してもらう手段を選択しました。というのも、Webフロントエンドの経験があったり、Webの最近の動向を知って自分の開発に活かしたいと興味のあるメンバーがいたからです。

実際にそのとき余力があったバックエンドのエンジニア2名とiOSのエンジニア1名に協力してもらい、自分を含む計4人体制で開発を進めました。

お願いする際にこの協力は評価されるかという点を気にしていましたが、会社の評価制度も柔軟で評価対象だと分かり安心してお願いできました。

協力の際にはJIRAでタスクを細かく切って、ガントチャートで進捗を可視化しつつ、その方のフロントエンドの経験度に合わせてタスクをお願いしました。

また新しく開発に関わる人にはどのディレクトリにどのファイルを置けば良いか、どういった作法でコードを書けば良いのか分からないという問題がありました。そこはHygenを使って指定したディレクトリにテンプレートのファイルを自動生成することで、開発時の迷いを軽減させるようにしました。

この協力体制のもと案件を切り抜けられました。この手法のメリットとして、再度Webのフロントエンドの開発の人手が足りない時に一度協力してもらった方にお願いできる余地ができました。

課題3: 開発ドキュメントが少なく属人化していた

取り組み2でFAANSの他職種の人にWebフロントエンドの開発の協力をしてもらい4人体制で開発を進めましたが、開発ドキュメントが充実していませんでした。質問の度に口頭で設計や運用ルールなどのWebフロントエンドの開発に必要な説明をしていて時間がかかっていました。

取り組み3: 開発ドキュメントを作成し、フロードキュメントとストックドキュメントを分けて運用

取り組み2の案件が終わった後、メンバーが1人仲間に加わり、FAANSのWebフロントエンドの開発メンバーが2人になりました。

そのタイミングで開発ドキュメントを作成し、それを見ればFAANSのWebフロントエンドの開発に必要な情報が分かるようにしました。

この結果、属人化が抑えられ開発の説明にかかる時間が短縮されました。

ただ、次の懸念としてそのドキュメントがメンテナンスされ続けるかという点を気にしていました。開発ドキュメントは一度作成して終わるというわけではなく、プロダクトの成長に伴ってドキュメントを追加したり更新する必要があります。

各々がバラバラにドキュメントを配置してしまったり、どのドキュメントを信頼すべきか分からなくなる可能性があり、それを避ける必要がありました。

そこでドキュメントを以下のように分けて運用しました。

  • ストックドキュメント: アーキテクチャーなど定期的にメンテナンスする必要があるドキュメント
  • フロードキュメント: メンテナンスする必要がないメモのような一時的なドキュメント

フォルダの階層構造

メモ書きでも良いのでチームとしてドキュメントは残す方針にし、一度フロードキュメントに入れてその中で良い内容はストックドキュメントにも記載、適切なタイミングで階層構造を整備する運用にしました。こうすることで、ドキュメントを残す文化が醸成し、ストックドキュメントにはメンテナンスされていて信頼できる情報が残るようになりました。

課題4: FAANSのWebにおいて何の課題を優先して取り組むべきか分からなかった

取り組み3で開発ドキュメントを作成したことによって、開発の説明にかかる時間は短縮されました。そのタイミングで一時的に離れていたメンバーも戻ってきて、FAANSのWebチームも3人になりました。ただ中途で入ったばかりの人や久しくFAANSの開発に携わる人にとって、案件とは別に現状どの課題が存在しているのか、何の課題を優先して解決すべきか分からないという声がありました。その結果優先度が低い課題に着手してしまう可能性がありました。

取り組み4: FAANSのWebチームで抱えている課題をJIRAで管理するようにした

FAANSのWebチームの抱えている課題をJIRAのチケットとして作成して、管理するようにしました。その課題に対しては「FAANS_WEB_IMPROVEMENT」のラベルを貼るようにし、抱えている課題のチケットに絞って一覧で表示できるようにしました。それを元にチーム内で話し合い、課題に対してHigh、Medium、Lowなどの優先度付けをしました。これによって、メンバー全員に課題感の共有ができ、優先度が高い課題に取り組む体制となりました。

JIRAのチケットを優先度順にソートして表示

課題5: 権限やフラグによってUIの表示や機能が異なり把握しづらかった

FAANSはショップスタッフ、ショップを管理する人、企業を管理する人などの権限や企業が持つ自社のECとの連携状況などのフラグが存在しています。そして、その権限やフラグによって表示されるUIや機能が異なるのもFAANSの特徴の1つです。

そのため各権限やフラグにおいて表示されるUIや機能が正しいか確認するために、アカウントを切り替えながら開発していて手間だと感じていました。

また、ある権限やフラグのアカウントで修正したときに他の条件のアカウントに影響が出ていないか自動的に担保する仕組みがありませんでした。

取り組み5: Storybookを使って多様なUIを管理した

そこで解決策としてStorybookを導入し、権限やフラグの状態をMock Service Workerなどを使ってモックして、それぞれのページにおけるStoryを一覧で見られるようにしました。これでアカウントを都度切り替えずとも各UIのパターンを把握しながら開発できるようになりました。一例を挙げると以下のように自社EC連携の有無によって自社ECのカードが表示されるか確認できます。

Storybookの状態ごとの表示

また、StorybookではInteraction testsの機能が備わっており、表示されたUIに対して期待値と一致しているかのテストができます。以下では自社ECを連携している場合に自社ECのカードが表示されるかをテストしています。そのテストをCIに組み込むことができ、リグレッションが起きていないか自動的にテストできました。

Storybookのテスト

export const IsLinkedOwnedEc: Story = {
  name: '自社EC連携をしている場合',
  parameters: {
    msw: {
      handlers: {
        mockGetStaffMember: rest.get(
          `${MSW_ORIGIN}/v1/staff_member`,
          (req, res, ctx) => {
            return res(
              ctx.json(
                produce(mockGetStaffMemberBaseResponse, (draftState) => {
                  draftState.company.is_linked_with_owned_ec = true;
                }),
              ),
            );
          },
        ),
      },
    },
  },
  play: async ({ canvasElement, step }) => {
    await step('自社ECのカードが表示されていること', async () => {
      const title = '自社EC';
      await within(canvasElement).findByRole('region', {
        name: title,
      });
    });
  },
};

また、FAANSは業務ツールということもありユーザーによる操作などインタラクションが多いのも特徴です。例えばフォームの入力、ボタンのクリックなどの操作です。インタラクション後のUIも期待値通りか自動的に担保したく、それが可能で見やすい形でデバッグができるInteraction Testsの機能はFAANSのプロダクトの特性に合った選択肢でした。

Storybookのテスト

Storybookの運用にあたって、Storybookの作成が目的化して、メンテナンスするためのモチベーションの低下を懸念していました。
しかしUIコンポーネントの管理の他に、以下のようにStorybookをメンテナンスするための目的を持たせたり、開発フローに取り込む事でモチベーション低下を防ぎました。

  • 多様なUIパターンを把握しながら開発する
  • その多様なUIやインタラクション後のUIも含めて自動テストをし、不具合を検知できるようにする

次の課題として、表示されたUIに対して担保したい項目が多く、その分テストコードも増えてメンテナンスコストが高くなっています。カバー率の高いビジュアルテストと組み合わせて、コストを下げることを検討しています。(参考)

課題6: フロントエンドとバックエンドの差異を吸収する層が存在していなかった

フロントエンドとバックエンドの差異を吸収する層が存在しないため、APIの変更の影響を受けやすいViewの実装がありました。

一例をあげると以下のフォームのように、APIのリクエストのあるパラメーターの型がbooleanなので、onChangeの際にbooleanへ変換するAPIを意識したViewの実装です。

この実装をするとAPIのリクエストのパラメーターの変更によって、Viewのコードの変更が必要になります。またこの変換や加工といった吸収処理をどこに書くかルールが決まっておらず、開発者によって書く場所が異なり、レビューコストが上がってしまいました。

  // フォームのViewの実装例。ここではReact Hook FormのControllerでフォームのデータを管理。
  <Controller
    control={control}
    name="parameter_name"
    render={({ field }) => {
      return (
        <RadioGroup
          onChange={(e) => {
            field.onChange(e === 'true'); //APIのリクエストのパラメーターの型に合わせるために、stringをbooleanに変換している。このようにViewで書く開発者もいれば、onSubmit時に書く開発者もいて統一されていなかった。
          }}
          value={field.value ? 'true' : 'false'}
        >
          <Radio
            value="false"
          >
            false
          </Radio>
          <Radio
            value="true"
          >
            true
          </Radio>
        </RadioGroup>
      );
    }}
  />

取り組み6: フロントエンドとバックエンドの差異を吸収するPresenters層を設けた

フロントエンドとバックエンドの差異を吸収するPresenters層を設けてそこで吸収させることにしました。

これによって、APIのインタフェースの変更の影響範囲はそのPresenters層に限定でき、Viewにその影響を与えないようにしました。

またViewに書かれていた変換や加工といった吸収処理がPresenters層に統一されたことで、開発者がどこに書くか迷うことがなくなったり、Viewのコードもシンプルになりました。結果としてレビューコストも下がるようになりました。

// フォーム送信時に呼び出すPresenters層の関数。
export const xxxPresenter = async (
  formData: FormData
): Promise<Response> => {
  // formDataをAPIのインターフェースに合わせて変換・加工する。(例、stringをbooleanへと変換。)
  // 変換後にAPIを呼ぶ。
};
  // フォームのViewの実装例。ここではReact Hook FormのControllerでフォームのデータを管理。
  <Controller
    control={control}
    name="name"
    render={({ field }) => {
      return (
        <RadioGroup
          //stringのまま管理。APIは意識せずにViewのコードを書ける。
          onChange={field.onChange}
          value={field.value} 
        >
          <Radio
            value="false"
          >
            false
          </Radio>
          <Radio
            value="true"
          >
            true
          </Radio>
        </RadioGroup>
      );
    }}
  />

課題7: FAANSで使っていたCreate React Appにおいてメンテナンス状況に不安があった

JIRAで管理している課題チケットの中にDependabotから通知されるセキュリティーアラートの数が多いという問題がありました。

そこでそのセキュリティアラートで指摘されているパッケージを見ると利用していたv4のCreate React App(以下CRA)が依存しているパッケージ起因であることが分かりました。

それを機にCRAの現在について調べてみると最近のv5へのアップデートが1年前だったり、メンテナンス状況に不安が残るissueがいくつかありました。またチーム内から開発サーバーの立ち上がりに時間がかかるといった声を聞くようになりました。

旧Reactの公式ドキュメントではSPA開発にCRAがお勧めされていて、その選定には違和感なく開発していたのですが、最近ではこのような状況になりフロントエンドの変化のスピードに驚きました。

取り組み7: Viteへ移行

そこで月1回開催されるフロントエンド勉強会で技術顧問やZOZOTOWNやWEARなどの他のチームの方に相談して、CRAに感謝しつつViteへの移行を決めました。

Next.jsでSPAを作るという候補も上がっていましたが、以下の理由でViteへ移行しました。

  • FAANSのWebがReact Routerに依存しておりファイルシステムベースのルーティングへの移行コストがかかりそう。
  • サーバーサイドレンダリングの予定がない。

移行作業もスムーズに行き、規模が大きいところでいくと環境変数の参照をprocess.envからimport.meta.envへ変更することでした。なので環境変数を利用しているページに影響するとみて、そのページを中心にQAして頂いてリリースしました。

結果として抱えていた課題は解消され、以下のように開発サーバーの立ち上げや本番ビルドなどの速度が上がりました。

比較内容 CRA4 Vite
dev cold start 約3分15秒 約13秒
dev warm start 約27秒 約2秒
hot reload 約2秒 保存直後
production build 約10分 約3分

課題8: OpenAPIのymlを手動でコピーして運用していた

FAANSのWebではOpenAPI Generatorを使って、バックエンドのリポジトリに保存されているOpenAPIのymlファイルをフロントエンドのリポジトリに手動でコピーして、API Clientを生成していました。

定期的にこの作業は発生し、手動のためオペレーションミスを引き起こす可能性がありました。

手動でswaggerをコピーしていたときの図

取り組み8: submoduleを使ってOpenAPIのymlを参照し、自動でAPI Clientを生成するようにした

そこでフロントエンドのリポジトリにsubmoduleを登録し、そこからバックエンドのリポジトリに保存されているOpenAPIのymlファイルを参照するようにしました。また、OpenAPIのymlの更新を検知して自動でAPI Clientを生成し、プルリクエストを作成するワークフローを組みました。この作業を自動化することで、オペレーションミスを防いだり、他のクライアントのチームにも展開できるようになりました。

submoduleを使ってOpenAPIを参照し、自動でAPI Clientを生成した図

終わりに

以上が組織的・技術的な課題とそれに対する取り組みになります。

全体的にまず課題の共有から始めて、WebフロントエンドのチームのみならずFAANSの他職種の方と会話を重ねるのも大事でした。
広い視点で、そのときの状況やプロダクトの特性に適した解決策が見つかったり、今何をやるべきかといった優先度も洗練されていくからです。

課題2のようにWebのフロントエンドの開発者が1人になり案件がさばける体制でない状況下で、他の職種の方に協力してもらう解決策はその1つの例でした。
当時は自動化して開発効率を上げる技術的なアプローチで解決することを考えていましたが、このように組織的なアプローチで解決に繋がるとは思わなかったです。

そうして話し合いながら、優先度が高い課題に対処して改善されていく日々を目の当たりにすることは、その課題の解消の効用以上にチームの雰囲気に良い影響を与えました。チームの雰囲気が良くなれば、チーム内で意見が言いやすくなったり、次なる課題に対して取り組みやすくなると思います。

この「課題の共有」→「プロダクトチームと会話」→「優先度づけして取り組む」→「チームの雰囲気が良くなる」→「次なる課題の共有」→(以降繰り返し)というポジティブなサイクルは、この変化の多い環境下で課題を解決し続けていく上で大事だと感じました。

現状FAANSのWebに関して以下のような課題を抱えており、これからも案件とバランスをとりながら取り組み続けていきたいと思っています。

  • Findyで開発生産性の可視化した上で施策が打ち続けられるチームづくり
  • 開発効率を上げることを目的としたデザインシステムの作成

ZOZOではこのように課題を前向きに改善してくれるエンジニアを募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

hrmos.co

corp.zozo.com

カテゴリー