🍇

Ruby on RailsのサービスをNext.js+GraphQLに置き換えした話

2024/05/07に公開

はじめに

こんにちは!マイベストで入社してから2年目になるtkeita1024 です。現在は、Frontendのエンジニアをしています!
今回は、新卒で入社してから1年かけてRuby on Railsで表示されているページをNext.js + GraphQLへ置き換えした話について書きたいと思います。

背景

マイベストでは、2022年~2023年にグローバル化対応というプロジェクトでRuby on Railsで表示されているページをNext.js + React + TypeScriptにし、APIをGraphQLを使用するように一部置き換えました。入社した当時は、主要なページのみしか置き換えが完了しておらず、管理画面(Admin)や新規事業(Favlist)の画面の置き換えは完了していない状況でした。そこで、新卒のタスクとして、置き換えが完了していない画面を全て置き換えるというのを取り組むことになりました。
https://speakerdeck.com/isaka1022/maibesutogurobaruhua-nosubete

どのように進めたか

また、GraphQLの実装とNext.js + React + TypeScriptの実装をそれぞれバックエンドエンジニアとフロントエンドエンジニアで分担しながら進めていきました。
置き換えを行うために、以下の手順で取り組んでいきました。

  1. 置き換えられていないページ・APIの調査
  2. GraphQLのスキーマを定義
  3. GraphQLのQuery・Mutationの実装
  4. SlimファイルをReact + TypeScriptで実装
  5. GraphQLとの繋ぎ込み
  6. 不要なコードの削除

1.置き換えられていないページ・APIの調査

はじめに、置き換えられているものと置き換えられていないものが混在しているため、置き換えるべき対象を明確にするため調査を行いました。調査では、Railsのルーティングやコントローラーなどのファイル、実装されている画面のコードなどを全て手作業で確認して、置き換えすべきかどうかを調べました。
また、進捗の可視化を行うために、調べたものを以下のようなNotionにまとめて取り組みました。

2.GraphQLのスキーマを定義

マイベストでは、Client Firstの設計思想のもとスキーマを決めています。
この思想は主にフロントエンドで使用したいFieldを元に、スキーマを設計するということを意味しています。
そのため、フロントエンドで必要なFieldを相談して、ObjectTypesに定義していました。

module Admin
  module ObjectTypes
    class UserType < Types::BaseObject
      field :id, ID, null: true
      field :name, String, null: true
      field :email, String, null: true
    end
  end
end

また、フロントエンドで日付や価格表示の情報を表示したい場合は、基本的にバックエンド側で整形したものをFieldとしてフロンエンドに渡すようにしています。この理由としては、フロントエンド内のコードを複雑にしないようにするためです。

module Admin
  module ObjectTypes
    class ItemType < Types::BaseObject
      field :id, ID, null: true
      field :name, String, null: true
      field :price, Int, null: true
      field :formatted_price, String, null: true

      def formatted_price
        ... # 変換する処理
      end
    end
  end
end

3.GraphQLのQuery・Mutationの実装

スキーマを定義したらQuery・Mutationの実装を行いました。
基本的には、Controllerのindex・show・new・editアクションはQueryで実装するように、create・update・destroyアクションはMutationで実装するようにしていました。

class UsersController < ApplicationController
  # Queryで実装
  def index
    @users = User.all
  end

  # Mutationで実装
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: 'ユーザーが作成されました。'
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password)
  end
end

上記のようなControllerを置き換える場合は、indexアクションをQueryで、createアクションをMutationで実装し、以下のように書きます。

# Queryの実装
module Resolvers
  class UsersResolvers < BaseResolver
    type Types::UserType.connection_type, null: false

    def resolve
      User
    end
  end
end

# Mutationの実装
module Mutations
  class CreateUserMutation < BaseMutation
    argument :id, ID, required: true
    argument :email, String, required: true
    argument :password, String, required: true

    field :user, Types::UserType, null: true
    field :errors, [String], null: true

    def resolve(name:, email:)
      user = User.new(name: name, email: email
      if user.save
        { user: user }
      else
        { errors: user.errors.full_messages }
      end
    end
  end
end

4.SlimファイルをReact + TypeScriptで実装

マイベストでは、RailsのviewにRubyのテンプレートエンジンであるslimを使用しています。
そのため、slimファイルで書かれている内容をReact + TypeScriptに置き換える必要があります。
置き換える際には、slimファイルの実装を元に、React + TypeScriptに置き換えても影響が出ないように実装していきました。

https://github.com/slim-template/slim/tree/main

ルーティングに関しては、Next.jsのルーティングを使用しました。
マイベストでは、以下のようにpagesを親ディレクトリとしてルーティングを設定しています。
また、*.page.tsxをルーティングとして表示するように設定し、呼び出すGraphQLのQueryやFieldの設定は*.graphqlで定義するようにしています。

pages/
└── admin/
    └── user/
        ├── index.page.tsx
        ├── index.graphql
        └── [id]/
            └── edit/
                ├── index.page.tsx
                └── index.graphql

5.GraphQLとの繋ぎ込み

マイベストでは、繋ぎ込みにApollo Clientを使用しています。
以下のようなクエリを呼ぶためにgraphqlファイルでクエリや呼び出したいFieldを定義するようにしています。

# UserQuery
query user {
 user {
  id
  name
  email
 }
}

# CreateUserMutation
mutation user($id: ID, $name: string, $email: string) {
 createUser(id: $id, name: $name, email: $email) {
  errors
 }
}

これらを定義したものをGraphQL Code GeneratorでuseQueryuseMutationのhook
・型を自動生成させます。その後使用したいファイルで使用したいクエリをimportして、以下のように使用することでGraphQLとの繋ぎ込みが可能です。

const { data, error, loading, refetch } = useUserQuery();
const [createUser] = useCreateUserMutation({
  variables: {
    id
    name
    email
  }
});

6.不要なコードの削除

置き換えた後は、Railsにある、ControllerやView、ルーティングのファイル・コードの削除を行います。
削除を行う際には、削除した後もNext.jsに置き換えた画面に影響が出ないことを確認してから削除します。

取り組んで学んだこと

バックエンドエンジニアとフロントエンドエンジニアがそれぞれ1人ずつで、1年かけて置き換えを行なっていきました。それぞれがバックエンドの実装とフロントエンドの実装を分担しながら取り組んできましたが、スキーマの定義は上手くいかないことがありました。例えば、Client Firstをもとにスキーマを定義してそれを分担して実装した際に、バックエンド側の構造的に良くないことがわかり、スキーマを再設計することになりました。そのため、大きな手戻りが発生してしまいました。

その後は、フロントエンド・バックエンドでそれぞれ実装した場合のProsConsをDesignDocにまとめました。それらの中から一番最適なものはどれか決めて実装しました。以下のようにNotionでDesignDocを作成して決めました。

今後は、バックエンドエンジニアとフロントエンドエンジニアが互いにこのスキーマなら実装できそうかを相談しながらできると良さそうかと思いました。

まとめ

今回は、新卒1年目でNext.js+GraphQLに置き換えした話について紹介しました。今回の置き換えで多くの学びなどがあったので、新卒2年目ではこの学びを活かしていきたいと思います。

Discussion