Amethyst のフロントエンド構成

アドベントカレンダー13日目は、弊社のSEO分析ツール「Amethyst」(アメジスト)のフロントエンドエンジニア淺津によるシステム構成のご紹介。こんな感じになってます。

こんにちは。Amethystのフロントエンドエンジニアをしている淺津です。

この記事は JADE Advent Calendar 2023 の13日目です。

この記事では、Amethystフロントエンドの技術選定や開発インフラ等についてご紹介します。

前提条件

プロダクト

  • Amethystは、Search Console API を利用した、SEOのための分析 SaaS(BtoB)です。
    • インハウスのSEOマーケターの方やエージェンシー向けに、大規模サイトの分析に便利な機能を提供しています。
    • 現在の主な機能は、サイトのクロール率・インデックス率を監視するIndex Workerと、サイトへの流入検索キーワードを従来ツールよりも多面的な視点で分析できるSearch Analyitcsです。(詳細はこちら
  • バックエンドはGraphQLサーバー(Go製)で抽象化したFirestore、BigQuery、Bigtableです。

チームの規模

フルタイムでフロントエンドをメインに作業しているのは筆者ひとりです。その他に、プロダクトオーナーであるファウンダーの長山さん、デザイナーのMasayさんとアルバイトのメンバーがコミットしてくれることが多いです。

ベースの技術

  • TypeScript
  • React
  • Next.js
    • Cloud Run上で動いています。
    • 基本的には薄いルーターライブラリのつもりで使用しています。
    • SSR関連の機能は、ほぼ使用していません。
    • Pages Routerで動いています。
      • Next.js 13でもPages Routerのサポートが継続されていること、また、Vercelを利用していないので恩恵を最大限受けられるか微妙なことから、急いで移行する必要はないと考えています。

アーキテクチャ

状態管理・API連携

Apollo Client、Jotaiを導入しています。

  • Server Cache:Apollo Client

    • Apollo Client を使用して、GraphQL サーバーとやりとりをしています。
    • 現状は、 /graphql のようなディレクトリにFragmentやQuery、Mutationを書いて、GraphQL Code Generator で生成したCustom Hooksを用いています。
      • 型安全の恩恵は受けられていますが、それ以外(Overfetchingの防止、ページ単位でのリクエストなど)の恩恵は受けられていないのが現状です。
      • Fragment Colocationをしていく必要があります。
  • Form State & Global State:Jotai

    • フォームにJotaiを応用することについては、私の以前の発表スライドや、@mrsekut さんの記事が参考になるかもしれません。
      • メリットは、柔軟な状態の表現、バリデーションの条件設定の小回りがきくことです。
      • デメリットは、ボイラープレート的な記述が多くなることと、どうしてもGlobal Stateなのでリセットのためのコードを書く必要があることです。
    
    // Jotaiを使用したフォームの例
    
    const shouldValidateAtom = atom(false);
    
    // 例:入力画面と確認画面を切り替える際に、バリデーションが通っていることを確認
    const modeAtom = atom<InputConfirmMode, [InputConfirmMode], void>(
      'input',
      (get, set, update) => {
        if (update === 'confirm') {
          set(shouldValidateAtom, true);
          if (get(hasErrorAtom)) return;
          set(modeAtom, update);
        } else {
          set(shouldValidateAtom, false);
          set(modeAtom, update);
        }
      }
    );
    
    // 例:あるStateを変更する際に、別のStateも一緒に更新したい
    const serviceAccountIdAtom = atom('', (_, set, update) => {
      set(selectedPropertiesAtom, []); // serviceAccountを切り替える時は、propertyもリセット
      set(serviceAccountIdAtom, update);
    });
    
    // Jotaiそのものは露出させない。Custom Hooksとしてコンポーネントから利用する
    export const useServiceAccountId = () => useAtom(serviceAccountIdAtom);
    

i18n

next-i18nextを使用しています。

  • 当初は翻訳JSONファイルを単体で管理していました。
    • 当初、Figma上でJSONファイルをもとに日・英を切り替えできるようにするプラグイン(デザイナーMasayさん謹製。便利!)を運用していました。
      • このJSONと実装用のJSONを共通化していたため、1ファイルの翻訳JSONに優位性がありました。
    • プロジェクトが大きくなるにつれて、管理のしやすさ、パフォーマンス等の理由で、ページ単位への分割作業を段階的に進めています。
  • JSONファイル名を名前空間として登録する必要があるため、 /public/locales 内のファイルをリストアップして d.tsを生成するスクリプトなどが動いています。

スタイリング

Chakra UIを使用しています。

  • 動くものを素早く作りたい初期段階では、デザイン付きのUIライブラリが有効でした。
  • Theming機能を使ってビジュアル面はほぼ改造しています。
  • ヘッドレスUIライブラリ+テーマ機能をもったCSS in JSライブラリの組み合わせへの移行を検討しています。

その他

  • チャートライブラリとして、recharts を使用しています。
  • ドラッグ&ドロップには、dnd kit を使用しています。

開発補助

テスト、Storybook

Vitestを使用して、重要なロジックを中心にテストしています。

また、Storybookも合わせて作成しています。VSCode のスニペットを活用してStorybookファイルを簡単に作れるようにはしています。

{
  "Component": {
    "prefix": "comp",
    "body": [
      "type Props = {}",
      "",
      "export const ${1:ComponentName} = ({}: Props) => {",
      "  return <></>",
      "}"
    ],
    "description": "React component snippet",
    "scope": "typescriptreact"
  },
  "Story": {
    "prefix": "story",
    "body": [
      "import {${1:ComponentName}} from './${1:ComponentName}';",
      "import {Meta, StoryObj} from '@storybook/react';",
      "",
      "const meta: Meta = {",
      "  component: ${1:ComponentName},",
      "};",
      "export default meta;",
      "",
      "type Story = StoryObj<typeof ${1:ComponentName}>;",
      "",
      "export const Default: Story = {",
      "  args: {},",
      "};"
    ],
    "description": "Storybook component snippet",
    "scope": "typescriptreact"
  }
}

テスト周りについては正直なところまだまだ未整備な部分が多く、

  • Storybookを活用したコンポーネントの振る舞いのテスト
  • Playwright等によるブラウザ自動化テスト

は、現時点では存在しません。サービスにとってクリティカルな機能に対して優先的に、今後対応していくことを検討しています。

Lint、Format

GTS(Google Typescript Style)を使用しています。内部的にはESLintとPrettierのプリセットです。

GTS側でのTypeScriptバージョンへの追従があまり早くないなどの問題もあり、Configを引き継いだうえで素のESLint・Prettier、あるいはBiome等への切り替えも検討していきたいです。

おわりに

Amethystは現在、Public βとして公開されています。

Amethystサービスサイトはこちら

ご興味のあるSEO担当者様は、お問い合わせください。

明日は長山さんの記事です。ぜひお楽しみに!

アドベントカレンダーはこちら↓ https://adventar.org/calendars/9386