⚙️

Refineでココナラ法律相談管理画面を作り直した話

2023/01/16に公開

こんにちは。

ココナラ法律相談という弁護士の先生方と相談したい悩みのあるユーザーのマッチングサービスを担当しているエンジニアの高崎と申します。

法律相談開発チームでは、一般に公開されているユーザーの利便性向上はもちろんのこと、社内のメンバーが利用する管理画面への継続的な改善を行っています。

ココナラ法律相談開発チームでは、RefineというReactベースの管理画面構築用フレームワークを利用して、ココナラ法律相談の管理画面を効率よく再構築しました。

その結果リプレイス作業を通して様々なメリットがあったので、共有したいと思います。

リプレイスの背景

ココナラ法律相談は、2016年にリリースされ、サービス開始から早7年が経過しました。お陰様で順調にユーザー数を増やし、現在では全社の約10%の利益を生み出すサービスに成長することが出来ました。

accounting
引用: 2022年8月期通期決算説明資料

ビジネス面では順調に成長を続ける法律相談ですが、エンジニアリング面では主に運用面での負債が顕著化し、日常業務や改善作業を圧迫することが多くなってきました。

具体的には以下のような原因から、ビジネスサイド、開発サイド両面から生産性が低下していました。

業務に対して管理画面の機能が合わない

サービスリリース時の仕様に合わせて、管理画面が設計されているため、すでに使っていない機能や、現在の業務に合わない画面が存在していました。

本来であれば、機能が業務に合わなくなった時点で、改修や不要機能の削除を行うべきですが、事業の初期フェーズでは優先度の関係から、着手出来ない状況でした。

売上に直結するような一般ユーザー向けの新規機能開発が優先された結果、それに伴う管理機能開発へのリソース確保が難しくなりました。結果として、CSチームなど運用者側でカスタマイズしたスプレッドシートやKintoneなど別のSaaSを活用し、業務を遂行するケースが多くなり、 データの管理が難しくなったり、業務フローが複雑化してしまったりと、属人化が加速していました。

管理画面の改修コストが高い

上記の問題を解決するために、改修を行う際にもスムーズに開発を進めるのが難しい状況にありました。

旧管理画面は、Ruby on Railsで構築されたココナラ法律相談サービスの中に管理画面向けや、特定のユーザー向けの名前空間を切り、その中で専用の画面やサーバサイドの処理を実装していく、といういわゆるモノリシックなアプリケーションとして実装されていました。

before

Ruby on RailsはMVCアーキテクチャでバックエンドの処理とフロントエンドの実装を密結合させることで、初期の開発速度を担保することが出来ますが、長期的にスケールするアプリケーションにするためには、設計思想の一貫性が必要不可欠になってきます。しかし現状は秩序があるとは言い難い状況でした。

特に悩ましかったのは、フロントエンドで複数(Vue.js, jQuery + erb)のフロントエンドライブラリの共存によるコードの複雑化です。erbのテンプレートにjQueryでのDOM操作とVue.jsの仮想DOMの処理が共存しており、 やりたいことに対して、既存コードが必要以上に複雑化していました。

そこで、DBスキーマやバックエンド(RoRのコントローラ層まで)、フロントエンド(RoRのビュー層からJSの仮想DOMまで)までのどのレイヤーにおいてもRuby on Railsに強依存している状況から脱却し、その依存度及び結合度を下げることが今後の成長を見据える上で必須という結論に至りました。

Railsへ強く依存した状況を剥がすための、フロントエンド分割

分割するためのファーストステップとして、以下のような管理画面を独立したフロントエンドアプリケーションに分割することにしました。

after

その上で、既存のRailsアプリケーション上にフロントエンドアプリケーション用のエンドポイントを設定し、フロントエンドはそのエンドポイントのみを参照することで、必要なデータにアクセス出来るようにしました。

また後述しますが、フロントエンドとバックエンドの通信はGraphQLを用いており、ホスティング環境にはFirebase Hostingを採用しました。

Refineの導入

今回の管理画面実装に際し、Refineという管理画面構築用のフレームワークを導入しました。

Refineは2021年にリリースされた比較的新しいライブラリで、認証、ルーティング、状態管理など、昨今のWebアプリケーションに必須となる機能を統合し、ReactベースのCRUDアプリケーションを迅速に実装することが出来ます。

use_case
https://refine.dev/docs/getting-started/overview/

Refineのコンセプトは、認証やフォーム管理、UIコンポーネントなど、複数のサードパーティライブラリを実装者のニーズに応じて統合することです。

つまりUIコンポーネント部分はChakra UIを利用し、フォーム管理はReact Hook Form、認証部分はGoogle Authを利用するなど、実装としてどのライブラリを採用するかは、Refineがサポートしている範囲で自由に選定することが出来ます。

自分たちがRefineを選定した当初はAnt Designのみのサポートでしたが、現在はMUI、Mantine UI、Chakra UIなど様々なコンポーネントをサポートしています。

このフレームワークを選定した背景としては以下の点があります。

管理画面はCRUDベースの画面がほとんどなので、それを共通化しつつ、ビジネスロジックの実装に集中出来る

管理画面のリプレイスは、中長期的にはコスト削減や利益へ繋がりますが、短期的には利益に繋がりにくく売上に直結するようなユーザー向け機能へリソースを割くことになります。

そのため、なるべく他のタスクへの影響を抑えるべく、短期間でリプレイスする必要がありました。そこで出来るだけUIの構築や認証など、ビジネスロジック以外の部分に関しては極力自前実装を避ける方針で開発を進めました。 その点において、Refineを利用することで実装を簡略化することが出来ます。

例えば、リソースへのアクセス部分に関しては、RefineはDataProviderという仕組みを提供し、これを使うことでリソースの取得や作成、更新といった処理をアプリケーション全体で抽象化しています。

DataProvider自体は以下の関数を持つシンプルなオブジェクトであり、これらの関数を利用して、バックエンドAPIとRefineアプリケーションを連携します。

dataProvider.ts
const dataProvider = {
    create: ({ resource, variables, metaData }) => Promise,
    createMany: ({ resource, variables, metaData }) => Promise,
    deleteOne: ({ resource, id, variables, metaData }) => Promise,
    deleteMany: ({ resource, ids, variables, metaData }) => Promise,
    getList: ({
        resource,
        pagination,
        hasPagination,
        sort,
        filters,
        metaData,
    }) => Promise,
    getMany: ({ resource, ids, metaData }) => Promise,
    getOne: ({ resource, id, metaData }) => Promise,
    update: ({ resource, id, variables, metaData }) => Promise,
    updateMany: ({ resource, ids, variables, metaData }) => Promise,
    custom: ({
        url,
        method,
        sort,
        filters,
        payload,
        query,
        headers,
        metaData,
    }) => Promise,
    getApiUrl: () => "",
};

https://refine.dev/docs/api-reference/core/providers/data-provider/

具体的な使い方としては、各コンポーネントの中で、Data Hooksと呼ばれる、React Query(Refineで利用している状態管理ライブラリ)のHooksをラップした関数を呼び出します。

それらを利用することで、コンポーネントの中から簡単にAPIの呼び出しなどを行うことが出来ます。

refine_api_consuming_flow

以下は新規作成をする場合に利用するuseCreateフックの例です。

type CategoryMutationResult = {
    id: number;
    title: string;
};

import { useCreate } from "@pankod/refine-core";

// Refineが提供するuseCreateフックを呼び出し、mutate関数を取得する
// mutate関数の実態は、React QueryのuseMutationの戻り値
const { mutate } = useCreate<CategoryMutationResult>();

// mutate関数を呼び出すことで、DataProviderのcreateメソッドが呼び出される
mutate({
    resource: "categories",
    values: {
        title: "New Category",
    },
});

https://refine.dev/docs/api-reference/core/hooks/data/useCreate/

DataProvider自体は、getListやcreateなど、必須のインターフェイスとなる関数を持つオブジェクトなので、簡単に自作することも出来ます。

例えば今回の管理画面では認証にFirebase Authenticationを利用したので、ログイン認証チェックのために、HTTPリクエストにトークンをセットしてリクエストするようにしました。

以下は、ページネーション情報を含むリソースの一覧を取得するgetList関数の実装例です。

graphqlDataProvider.ts
import { GraphQLClient } from 'graphql-request'
import * as gql from 'gql-query-builder'

const graphqlDataProvider = (
  client: GraphQLClient,
  firebaseAuth: FirebaseAuth,
): Required<DataProvider> => {
  return {
    getList: async ({
      resource,
      hasPagination = true,
      pagination = { current: 1, pageSize: 10 },
      sort,
      filters,
      metaData,
    }) => {
      // 検索条件のパラメータ
      const { current = 1, pageSize = 10 } = pagination ?? {}
      const sortBy = genereteSort(sort)
      const filterBy = generateFilter(filters)
      const camelResource = camelCase(resource)
      const operation = metaData?.operation ?? camelResource

      // GraphQL Queryの組み立て
      const { query, variables } = gql.query({
        operation,
        variables: {
          ...metaData?.variables,
          sort: sortBy,
          where: { value: filterBy, type: 'JSON' },
          ...(hasPagination
            ? {
                start: (current - 1) * pageSize,
                limit: pageSize,
              }
            : {}),
        },
        fields: metaData?.fields,
      })

      // Firebase認証トークンをHTTPヘッダにセットしてリクエスト
      const idToken = await firebaseAuth.auth.currentUser?.getIdToken()
      client.setHeader('Authorization', `Bearer ${idToken}`)
      const response = await client.request(query, variables)
      
      // レスポンスをパースして画面側に渡す
      return {
        data: response['examples']['examples'],
        total: response['examples']['count'],
      }
    },
    
    // getMany, create...
  }
}

CSSの習熟度に依存せず、迅速に画面をつくりたい

法律相談開発チームは少人数であり、全員がCSSに習熟している訳ではないので、UIコンポーネントを導入して、自前でCSSを書くコストを減らす必要がありました。

その点で、簡単にUIコンポーネントを導入出来て、かつ統合用の複数のAPIを予め提供している点は魅力的でした。

例えば以下のフックを使えば、APIからカテゴリー一覧を取得し、簡単にAnt Designのセレクトボックスとして表示することが出来ます。

https://refine.dev/docs/api-reference/antd/hooks/field/useSelect/

create.tsx
import { Form, Select, useSelect } from "@pankod/refine-antd";

export const PostCreate = () => {
    const { selectProps } = useSelect<ICategory>({
        resource: "categories",
    });

    return (
        <Form>
            <Form.Item label="Categories" name="categories">
                <Select {...selectProps} />
            </Form.Item>
        </Form>
    );
};

interface ICategory {
    id: number;
    title: string;
}

select_box

このように予めRefine側で用意されているフックを利用することで、HTMLマークアップやReactやGraphQLが初めてのメンバーでも、比較的容易に導入が出来ると考えました。

Reactベースであり, TypeScript, GraphQLをデフォルトでサポートしている

ココナラは新しいことへの挑戦も積極的に応援する文化のため、単なる既存技術を用いた画面のリプレイスではなく、今後の発展を見据えて、新技術導入の場にしたいという思いがありました。 そのため、管理画面の刷新に際してこれまでメインで使っていたVue.jsとJavaScriptではなく、React、TypeScriptベースで実装することにしました。

また、バックエンドとの会話にはGraphQLを利用することにしました。 GraphQLはMeta社が開発した主にWebAPIとの連携で利用されるクエリ言語であり、その特性から以下のようなプロダクトで特にフィットすると考えたからです。

  • UIの変更が頻繁に行われている or 変更される可能性が高いプロダクト
    • 一度GraphQL APIを整備すれば、フロントエンド側の修正だけで済むため、生産性が高い
    • フロントエンドチーム単体での改善を主導しリリースすることも可能になる
    • UI要件をアジリティ高く柔軟に変更したいチーム / プロダクトと親和性高い
    • クライアントからの扱いやすさという観点でスキーマを整理するので、DB設計やモデルに依存しないドメインモデルの再設計が可能(副次的効用)
  • フロントエンド側のUIがリッチで複雑なプロダクト
    • 近年のWebアプリケーションでは典型的な1URL(画面) = 1リソースだけでは要件が満たせないケースが多い
    • 画面に関係するリソースが増えるほどREST API利用時のコストは高くなるため、GraphQLが有利
  • TypeScriptで画面が構築されているプロダクト
    • GraphQL Code Generator を利用することでAPIサーバとのリクエスト / レスポンスの型整合性をシームレスに担保出来る

法律相談のフロントエンドは、ビジネス要件の変更などから、UIの改修要望も頻繁にあり迅速にその要望に対応する必要があります。そのため、GraphQL APIを整備することで、フロントエンド主導のリリースができるようにしました。

上記の3点から、今回の要件との親和性が高いと考え、Refineの採用に至りました。

なぜReactAdminではないのか?

https://marmelab.com/react-admin/

Refineと同じ用途のフレームワークにReactAdminというフレームワークもあり、こちらも検討対象に上がりましたが、以下の理由から今回は選びませんでした。

UIコンポーネントがMaterial UI(V4)に依存する

選定当時のReactAdminバージョン(V3系)では、利用出来るUIコンポーネントが、Material UIに固定されていました。当時はすでにMaterial UIの後継であるMUI(V5)がリリースされており、ReactAdminは最新バージョンに追従出来ていない状況でした。

新規開発において、依存するライブラリが既に最新ではないのは抵抗がありますし、当時はいつ対応されるかも不明な状況でした。また、自分たちで使いたいUIコンポーネントを適宜使用するRefineのアプローチの方が、「特定ライブラリへの依存度を抑える」という今回のリプレイスの大方針にも合っており、長期的なメンテナンス性に優れると判断しました。

GraphQLのサポートがRefineに比べ見劣りする

ReactAdminもGraphQLのサポートは既にされていましたが、そのアプローチの手法に違和感がありました。

例えばReactAdminは、GraphQLを利用する場合、公式ライブラリを提供しています。

https://github.com/marmelab/react-admin/tree/master/packages/ra-data-graphql-simple

このライブラリは、対象となるエンドポイントに対して、イントロスペクションクエリを実行し、取得したスキーマ情報からデフォルトで、取得出来るフィールドすべてを取得しようとします。

すべてのフィールドを取得してしまうのは、GraphQLの利点(フロントエンドが必要なフィールドを宣言できる)に反しますし、もし必要データのみに限定したい場合は、以下のように特定のクエリ部分を明示的に上書きする必要があり、メンテナンス性に乏しいと言わざるを得ません。

https://github.com/marmelab/react-admin/blob/master/packages/ra-data-graphql-simple/README.md#L134-L170

導入の結果

上記の検討を経て、管理画面のリプレイスを行った結果、以下のようなメリットがありました。

比較的短期間(約3ヶ月)での新管理画面のリリース

React, GraphQL, TypeScriptのキャッチアップ含め、約3ヶ月程度で新管理画面のV1リリース(アプリケーションのベース部分と特に優先度の高い機能の移行)が達成出来ました。

主に自分1人でフロントエンド、バックエンドの実装を行い、着手当初は特にフロントエンドに苦手意識がありましたが、フロントエンドのアプリケーションを1から実装する経験が出来て自信に繋がりました。

その要因としては、RefineのAPIとUIコンポーネント(今回はAnt Designを採用)を利用することで、Reactのプラクティスや、TypeScriptの書き方を学べたことが大きいと感じています。

特にRefineはサンプルコードやドキュメントが充実しているので、実装に困ったときは大いに参考になりました。

https://github.com/refinedev/refine/tree/next/examples

デプロイ時間の短縮

これまではモノリシックなRailsアプリケーションだったこともあり、フロントエンドの修正だけでもデプロイに10分以上かかっており、課題になっていました。

今回のリプレイスで管理画面を独立したアプリケーションに分割し、Firebase Hostingにデプロイすることで、ビルドを含めたCI/CD全体で2分程度、ホスティング環境へのデプロイ自体は数秒で終わる程度に短縮出来ました。

これにより障害からの復旧も迅速に行えますし、問題の切り分けも容易であることから今後の運用コストも軽減出来ると考えています。

新規事業への技術横展開による迅速なサービス立ち上げ

副次的な結果ではありますが、今回の技術導入を通じて、生産性の向上やチームにとってのメリットを確認できたため、他サービスへの転用も可能になりました。

法律相談開発チームは、新規事業開発も担当しており、直近でリリースした別サービスであるココナラエージェントでは、今回得た知見を横展開し、比較的短納期でサービスリリースまでこぎつけることが出来ました。

ココナラエージェントで主に利用した技術・フレームワークなど

  • Next.js(React)
  • Ruby on Rails
  • GraphQL Ruby
  • TanStack Query(React Query)
  • Radix UI
  • Stitches
  • React Hook Form

まとめ

今回はRefineという管理画面構築用フレームワークを利用して、効率的に管理画面を移行した流れを整理しました。

こういったフレームワークは往々にして、簡単に使える方に傾倒しがちであり、実装が暗黙的でカスタマイズ性に乏しく、実際の業務アプリケーションの活用は難しいのでは?と最初は感じていました。

ただ検討を進めていく中でRefineは他のライブラリとの統合ツールとして機能することで、ある程度の自由度を担保しつつ、実装を簡略化したい部分はレールに乗ることも出来るという点で面白いと感じ、実際に実務で利用してみました。

ココナラ法律相談開発チームでは、このように技術的負債の解消を進めながら、新しい技術スタックへの挑戦を今後も続けていく方針です。

  • エンジニアリングだけでなく、ビジネスサイドとも連携しながら開発を進めていきたい方
  • レガシーな部分の改修だけでなく、そこで得た知見を新規事業のプロダクトに活かしていく経験をしたい方
  • 小さなチームで大きな仕事をしたい方

ぜひ一緒に働きましょう!

https://coconala.co.jp/recruit/engineer/

Discussion