💰

Server Actions で経費削減できた話

2024/08/13に公開

はじめに

個人開発にて、App Routerのフロントエンドとkotlin / Spring Boot / REST APIのサーバーサイドで構築されるシステムを2022年くらいから運用していました。機能的には問題なく使えていたのですが、如何せんAWS / EC2 上にNginx, Tomcat, RDBを立てているため、初年度無料枠で使用できても2年目からは月々2000~3000円ほど利用料が発生していました。
どうにかいい方法はないかと模索していたら、Server Actionsが使えるのでは?ということで思い切ってサーバーサイドを刷新することにしました。

以下は、この刷新を受けて得られたメリット・デメリットです。

メリット

  • 今回採用した環境ではすべて無料なので、サーバー使用量がまるッと浮いた。(2024/08/13時点)
  • kotlin のお守りがなくなり、TypeScriptのみの改修が可能になった。
  • REST API / サーバーサイドを立ち上げる手間がなくなった。

デメリット

  • App Routerがサーバーサイドのコードを管理しないといけなくなり、ディレクトリ設計どうしようか考え中(デメリットというほどではない)
  • 無料のRDBを使用しているので、若干遅く感じる(ちゃんと課金すれば、改善はされるはず)
  • がんばって作ったkotlin / Spring Bootの息の根を止めることになる😇

準備

サンプルコードはToDoアプリを題材にしたいと思います。
本質でないので、以下はサンプルコードに表現していないので、ご了承くださいませ

  • スタイルの適用
  • React.* などの自明なimport
  • UIコンポーネントの実装(名称からなんとなく察してください)
src/todo/components/RegisterTodo.tsx
'use client';

import { registerTodo } from '../server/registerTodo';

export const RegisterTodo = () => {
    const [title, setTitle] = useState<string>('');
    const [contents, setContents] = useState<string>('');

    const registerHandler = async () => {
        try {
            await registerTodo({title, contents});
            alert('登録しました。');
        } catch(error) {
            alert('失敗しました。');
        }
    }

    return (
        <div>
            <TextInput
                label={'タイトル'}
                value={title}
                setValue={setTitle}
            />
            <TextInput
                label={'内容'}
                value={contents}
                setValue={setContents}
                multiple
            />
            <Button
                label={'登録'}
                onClick={registerHandler}
            />
        </div>
    )
}

改修前の基本情報

当時私は、Reactを始めたばかりで、技術選定などしっかりできるほど知識はなかったので、なんとなくでaxios使って、REST APIを叩きに行くって構成にしたんだと思います。axiosはREST APIを呼び出すことができるお手軽なライブラリです。

https://qiita.com/ksh-fthr/items/2daaaf3a15c4c11956e9

以下は、axiosを使った改修前のサンプルコードです。

src/todo/server/registerTodo.ts
import axios from "axios";

export const registerTodo = async (args: {
    title: string, 
    contents: string,
}) => {
    await axios.post(
        'https://server/side/endpoint/api/v1/todo/register', 
        args
    );
}

kotlin側のコードですが、よくあるMVCモデルで、Controllerで受けっとってRDB: CRUDするって感じのものです。
RDBとの疎通には、MyBatisを使用しており、テーブルにCUDするものについては、Mybatis Generatorによる自動生成コードを使用しておりましたが、Rについては、自作のSQLを書く必要がありました。この時点で単体テストの必要性があったり、手間がかかります。
https://mybatis.org/generator/index.html

サンプルコードを書いても消すことになるので、省略します。

余談ですが、REST APIの一式のファイル数は、自動生成含めてですが750以上ありました。一人で実装したわけですが、Server Actionsの登場により不要となりました😇 「あの時の対応時間、知見はなんだったんだ」という気持ちがありつつも、メンテコストが下がるというのはやはり気持ちが楽になりました。

改修後の期待

以下の要素技術を使います。

  • Server Actions on Vercel(個人の範囲では無料)
  • Apollo / hasura with GraphQL(個人の範囲では無料)
  • PostgreSQL on NeonDB(個人の範囲では無料)

前後で対応付けすると次のようになります。厳密には完全に一致するものではないので、なんとなくのイメージを掴んでもらえればと思います。

要素 改修前 改修後
サーバーサイド REST API / Spring Boot Server Actions on App Router
RDSの実行環境 MySQL on EC2 PostgreSQL on NeonDB
フロントエンドの実行環境 Nginx on EC2 Vercel
サーバーサイドの実行環境 Tomcat on EC2 Vercel
データ疎通するライブラリ Mybatis with SQL Apollo / hasura with GraphQL

改修のアプローチ

Server Actionsを利用するまでに必要な環境の準備をしていきます。

hasura の準備

GraphQL APIを提供してくれるサービスです。以下のサイトから簡単に利用を開始することが可能です。

https://hasura.io/docs/latest/index/

プロジェクトを立ち上げたら、GraphQLの元となるRDBを指定します。自前のRDSをお持ちであれば、そちらを指定することもできます。NeonDBであれば、無料で新規作成・使用することが可能です。

ここから、テーブルの追加もできるので、todoテーブルを作成しておきましょう。

create table todo (
    id uuid primary key,
    title text not null,
    contents text not null
);

hasuraの外からGraphQLを実行する場合は、権限が必要になるため、権限設定も忘れずに行っておきましょう

https://hasura.io/docs/latest/auth/authorization/permissions/common-roles-auth-examples/

Apollo / codegen の準備

ちょうど、目的の記事があったので、引用させていただきます 🙏

https://zenn.dev/hisamitsu/articles/01b0f0cbf5ab1d

今回は、ToDoを登録するものなので、以下のようなmutationになると思います。

mutation insertTodo(
  $id: String!
  $title: String!
  $contents: String!
) {
  insertTodoOne(
    object: {
      id: $id
      title: $title
      contents: $contents
    }
  ) {
    __typename
    id
  }
}

これをcodegenするのですが、その前に

https://qiita.com/shinnoki/items/576277184de0e89cab97

codegen-client.yml のpluginsにtyped-document-nodeを適用するようにしておくと、冗長なコードがなくなったり、将来的な可用性が上がります。
codegenすると、InsertTodoDocumentというファイルが生成されるかと思います。これはあとで使用します。

Vercel

Next.jsなどのデプロイ・実行環境を提供するサービスです。GitHubとの連携もしているので、マージされたらVercelにデプロイってことも可能でなにかと便利です。

https://vercel.com/docs/getting-started-with-vercel

Server Actions

さて、本題です。

https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations

公式に倣って、実装すると

src/todo/components/RegisterTodo.tsx
// 修正なし
src/todo/server/registerTodo.ts
+ 'use server';

- import axios from "axios";
+ import { getClient } from 'graphql/apollo';
+ import { InsertTodoDocument } from 'graphql/generate';

export const registerTodo = async (args: {
    title: string, 
    contents: string,
}) => {
-    await axios.post(
-        'https://server/side/endpoint/api/v1/todo/register', 
-        args
-    );
+    await getClient().mutate({
+        mutation: InsertTodoDocument,
+        variables: {
+            id: // uuidを発行する
+            ...args,
+        }
+    })
}

REST APIのときと比較すると、革命的なコード量の減少です。一見増えているようですが、REST APIだとこの先、Controllerを実装して、ビジネスロジックを実装して、登録用のSQL書いてなどまだやることあります。どれだけ頑張ってもSpring Bootで実装すると、100行は超えると思います。一方で、Server ActionsGraphqQLを使うことでこんなにも実装量を減らすことができました。

この例だと、Apolloの関数を呼び出すだけでしたが、もう少し込み入った実装が必要な場面も出てくると思いますので、その際はファイルを分離したり、ディレクトリ設計をしっかりするがいいと思います。

まとめ

Server Actionsのサンプルコードを紹介しましたが、この登場は界隈でも批判的な意見出ているようですが、個人的にはありがたき産物なのかと感じています。

また、争点となっているクライアントサイドとサーバーサイドのコードがだいぶ近い場所に共存することになったので、それはそれでいい感じのアーキテクチャを考えるのもいいと思います。(気が向けば記事書きます)

GitHubで編集を提案

Discussion