💬

Next.jsアプリのバックグラウンドジョブをTrigger.devで実装してみた

2024/03/03に公開

バニッシュ・スタンダード深田です。

業務ではGoをメインとしたサービス開発に携わっておりますが、個人開発ではT3 Stackベースの効率的な小規模開発にも関心があり、その可能性を探求しています。

バックグラウンドジョブ機能

Webアプリケーションの開発過程では、しばしば時間を要する処理が必要となります。これらのプロセスをメインのリクエスト処理から分離し、バックグラウンドで実行することで、アプリケーションのパフォーマンスを向上させ、ユーザーエクスペリエンスを改善できます。

T3 Stackをベースにしたプロジェクトを開発していて、時間を要するデータ処理をユーザーのリクエストに基づいて実施する必要が生じました。これを同期的に処理するとユーザーエクスペリエンスが著しく低下するため、バックグラウンドジョブ機能の導入を決定し、その実装方法を模索中にTrigger.devに出会いました。公式ドキュメントの他にはあまり情報がなかったため、独自に実装を試みることにしました。

Trigger.devとは

https://trigger.dev/
Trigger.devはTypeScriptで書かれたオープンソースのバックグラウンドジョブフレームワークで、簡単にバックグラウンドジョブを定義し、管理することができます。特に、Next.jsなどを使用したプロジェクトにおいて、Trigger.devを導入することで、以下のようなメリットがあります。

  • イベント駆動のジョブ実行:特定のイベントが発生した際にジョブを自動的にトリガーすることができます。これにより、アプリケーション内で発生するユーザーのアクションやスケジュールされたイベントに基づいて、バックグラウンドでの処理を簡単に実行できます。

  • ダッシュボードによる管理:ジョブの実行状況をリアルタイムで確認できるダッシュボードが提供されています。このダッシュボードを通じて、ジョブの実行状況やエラーログを簡単に確認でき、デバッグや監視が容易になります。

  • スケーラビリティ:サーバーレス環境での実行をサポートしており、負荷に応じて自動的にスケールアップ・ダウンします。

実装例

外部APIを連続して呼び出し、そのレスポンスをデータベースに保存するシナリオを考えます。このプロセスが重い場合、リクエストのタイムアウトリスクがあります。

この問題を解決するためにバックグラウンドジョブを導入すると、APIのレスポンス待ちやデータ処理をバックグラウンドで実施できます。これにより、メインのアプリケーションプロセスはユーザーリクエストに即座に応答でき、ユーザーは重たい処理の完了を待たずに他の作業を行うことが可能となります。

Trigger.devプロジェクトの新規作成

既存Next.jsプロジェクトへのセットアップ

$ npx @trigger.dev/cli@latest init -k xxxxxx -t https://cloud.trigger.dev
$ npm run dev
$ npx @trigger.dev/cli@latest dev 

問題がなければ画像のようになるはずです。

フロントエンドからのトリガー

src/components/trigger/Test.tsxでサーバーへの非同期リクエストの送信を行います。

"use client";
import { Button } from "@/components/ui/button";
import { useForm } from "react-hook-form";
import { Textarea } from "../ui/textarea";

const TriggerTest = () => {
  const { register, handleSubmit } = useForm();
  const onSubmit = async (data: any) => {
    const urls = data.urls.split("\n");
    const ids = urls
      .map((url: string) => {
        const match = url.match(/(\d+)$/); // URLの末尾の数字(ID)を抽出
        return match ? match[1] : null;
      })
      .filter((id: any) => id !== null);

    const response = await fetch("/api/triggerJob", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ ids: ids }),
    });
    window.alert("Triggered");
  };

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)} className="mt-16 m-8 w-96">
        <Textarea className="h-36" {...register("urls")} />
        <div className="flex justify-end mt-4">
          <Button type="submit">Trigger</Button>
        </div>
      </form>
    </div>
  );
};

export default TriggerTest;

APIルートの設定

src/app/api/triggerJob/route.ts では、フロントエンドからのリクエストに応じて特定のジョブをトリガーするためのAPIルートを設定します。このファイルでは、POSTリクエストを受け取り、その内容に基づいてバックエンドプロセスを実行します。

import { NextResponse } from 'next/server';
import { client } from '@/trigger';

export async function POST(req: Request) {
  if (req.method === 'POST') {
    try {
      const body = await req.json();
      const { ids } = body;
      client.sendEvent({name: "test-backgroud-job", payload: {
        ids: ids
      }});
      return NextResponse.json({ message: '成功' });
    } catch (error) {
      return NextResponse.json('エラーが発生しました', { status: 500});
    }
  } else {
    return NextResponse.json(`Method ${req.method} Not Allowed`, { status: 405});
  }
}

ジョブの定義

src/jobs/testBackGroundJob.ts では、特定のイベントに応じて実行されるバックグラウンドジョブを定義します。この例では、外部APIを呼び出し、その結果をデータベースに保存するというプロセスを模擬しています。

import { client } from "@/trigger";
import { eventTrigger } from "@trigger.dev/sdk";
import prisma from "@/lib/prisma"

client.defineJob({
  id: "test-backgroud-job",
  name: "Test Backgroud Job",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "test-backgroud-job",
  }),
  run: async (payload, io, ctx) => {
    const ids = payload.ids as number[];
    for (const id of ids) {
      // 外部APIに依存した時間がかかる処理を想定
      await new Promise(resolve => setTimeout(resolve, 1000));
      const statuses = [200, 404, 500];
      const status = statuses[Math.floor(Math.random() * statuses.length)];
      const stock = await prisma.backGroundTest.create({
        data: {
          randomID: id,
          status: status,
        }
      })
    }
    await io.logger.info('完了');
  },
});

デモ

トリガーとなるボタンを押下する

ジョブが実行リスト上に表示、ジョブが完了する

DBにデータが保存される

まとめ

Trigger.devの利用により、バックグラウンドジョブの管理が容易になり、ダッシュボードを通じてジョブの実行状況やエラーログを簡単に確認できます。また、サーバーレス環境での実行をサポートしており、負荷に応じて自動的にスケールアップ・ダウンするため、スケーラビリティが向上します。

Trigger.devを採用しない場合、これらのバックグラウンドジョブの管理やスケーリングを自分たちで実装する必要があり、大幅に開発コストが増加します。Trigger.devの導入により、これらの課題を簡単に解決し、開発に集中できるようになります。

個人的には、セットアップの容易さに驚かされました。参考資料が少ない中での実装は困難でしたが、この経験を通じて得られた知識とスキルは、今後の開発において大いに役立つと思います。

最後に

弊社「株式会社バニッシュ・スタンダード」では、現在エンジニアおよびデザイナーを積極的に募集しています。

私も入社してから半年しか経っていませんが、Go、React、LangChain、Amazon Redshift、Amazon QuickSightなど、幅広い技術領域に携わることができ、日々充実した仕事ができています。技術レベルの高いエンジニアが多く、成長意欲のある方には特におすすめです。また、みんなが仕事に対して責任感を持ち、親切な人ばかりなので、非常に働きやすい環境です。

少しでも興味を持たれた方は、ご応募をお待ちしています。
https://v-standard.notion.site/Engineering-at-VANISH-STANDARD-929c17c0252c4d4a92fefc9a90b04d1a

Discussion