
はじめに
こんにちは!株式会社エブリーで約1か月間インターンシップに参加している山本です。配属チームはリテールハブ小売アプリチームで、主に小売店やそのお客さんに向けたサービスを開発しているチームになります。具体的には、スーパーなどの小売店がお客さんにお知らせをアプリ経由で配信するなどのサービスを手掛けています。本記事では、小売店向けのアプリの運用効率を向上させるために導入した管理機能と開発していく中で困ったことなどについてご紹介します。
背景と目的
現在、小売アプリにはお客さん向けアプリと小売店向けの管理画面の2つが存在します。小売店向けの管理画面では、お客さんに向けてお知らせやチラシなどを配布することができ、お客さん向けアプリではそれらを受け取り、利用することができます。
これらのアプリに関しては、マルチテナント化を進めており、単一コードで管理を行っています。しかし、現時点では小売店向けの管理画面を管理する管理機能のようなものが存在しません。そのため、各テナントの機能やカスタマイズを一元管理するような画面や利用状況などを監視、分析するような機能がありません。また、運営からメンテナンス等のお知らせを伝えることもできないため、各小売店に個別に連絡をする必要があります。対象の小売店が数店舗であれば運用可能ですが、これからさらに大規模になっていくことを考えると、小売店の管理機能を開発する必要があります。
このような背景のもと、本インターンでは運営効率の向上や顧客体験の統一化のために小売向けの管理機能の開発に取り組みました。
構成と技術スタック
今回、取り組んだタスクは一からのスタートだったため、技術選定から行う必要がありました。個人で開発を行う際は、特に何も考えず自分の好きな技術や触ってみたい技術を使っていたため、実際に必要な機能の実現可能性など様々なことを考慮しながら技術選定を行うのはとても難しかったです。
選定にあたっては、機能要件の実現可能性や開発効率などを多角的に検討した結果、以下の理由によりNext.jsによるフルスタック開発を採用しました。
- 開発効率の向上:フロントエンドとバックエンドが同じ言語(TypeScript)であることで初期段階の開発をスムーズに進めることができる
- コードの型安全性:フロントエンドとバックエンドで型定義を共有できるため、データの整合性を保ちやすく安全な開発が可能になる
- ライブラリの充実:必要な機能である認証機能をはじめとしたライブラリが充実しており、複雑な機能も実装できる
また、インフラ構成に関しては、以下のような構成にしました。ALBやセキュリティグループでIP制限をかけることで、社外からのアクセスを制限しています。デプロイに関しては、ECRへのpushとECSのデプロイはecspressoで管理をして、それ以外のコンポーネントはTerraformで管理をしています。
インフラ構成図

技術スタック一覧
- Next.js
- AWS
- Terraform
- ecspresso
- Github Actions
- MySQL
実装した機能
本インターンは1か月という短い期間ということもあり、優先順位の高い以下の機能を実装しました。
- 認証機能
- ログイン/ログアウト
- ユーザー管理
- アカウント作成/削除
- 管理者権限/閲覧権限
- お知らせ管理
- お知らせ作成/編集/削除
- 操作ログ
- 誰がいつ何を行ったかを記録
ログイン画面とお知らせ管理画面は現在以下のようになっています。
ログイン画面

お知らせ管理画面

困ったこと
認証機能について
認証機能に関しては、NextAuth.jsの最新バージョンであるAuth.js (v5から名称が変更) を採用しました。Auth.jsは様々な認証機能を提供しており、これらを少ないコード量で簡単に実装できるため、このライブラリを用いてEmailとパスワードでの認証機能を実装しました。
しかし、インターン期間中にXである記事が流れてきました。この記事ではAuth.jsはBetter Authに統合されることが発表され、今後はフレームワーク非依存のBetter Authに移行することが推奨されています。そのため、Auth.jsで書いたコードをBetter Authに移行する必要が発生しました。
当初実装していたAuth.jsの認証ではJWTを用いて、アプリケーション側でセッション情報を持たないステートレスな認証を行っていましたが、Better Authはステートレス認証をサポートしていませんでした。そのため、DB設計なども変更になり、完全にBetter Authで書き換えるという作業になりました。
予期せぬライブラリの移行作業は大変でしたが、結果的に数日間で複数の認証技術に触れることができ、非常に学びの多い経験となりました。また、Web技術の進化の速さをリアルタイムで体感すると同時に、実務開発のリアルな一面も経験することができました。
API呼び出しについて
Next.js App Routerでサーバーサイドの処理を行う方法として、Route Handlersを用いた実装方法とServer Functionsを用いた実装方法があります。
Route Handlers
Route HandlersはAPIエンドポイントをサーバーサイドで作り、それを呼び出します。app/api配下にroute.tsファイルを配置することで、ファイル構造がそのままAPIエンドポイントのURLとなり、フォルダとファイル名を見るだけでどのURLに対応するのかが直感的にわかるようになっています。
以下のコードをapp/api/hello/route.tsに配置した場合、クライアント側からfetch("/api/hello")で呼び出すことができます。
export async function GET() {
return Response.json({ message: "Hello World" })
}
Server Functions
Server FunctionsはクライアントサイドからRPCスタイルで簡単にサーバサイドの関数を呼び出せる機能です。"use server"ディレクティブを加えることで、以下のようにサーバーサイドの関数を定義することができます。
"use server"
export async function createPost(formData: FormData) {
// update logic
}
そして、クライアントサイドではフォームなどに以下のように記述することで処理を行うことができます。stateを保持したり、handlerを定義する必要がなく、簡潔に書くことができるというメリットがあります。
"use client"
import { createPost } from "@/app/actions"
export function Button() {
return <button formAction={createPost}>Create</button>
}
Server Functionsの簡潔な記述は魅力的でしたが、Next.jsのAPIを外部から呼び出す場合や、今後バックエンドをNext.jsから切り離すことも想定して、今回はRoute Handlersを用いて実装を行いました。
インフラ構成について
小売向けの管理機能はあまり使用頻度が高くない想定ということで、当初はLambdaを用いてデプロイを行う方針でした。LambdaはAPI Gatewayなどの何らかのイベントがトリガーとなりhandler関数が呼び出されるため、Lambda特有のインターフェースに沿った書き方を行う必要があります。しかし、Lambda Web Adapterを用いることで、元々サーバーレス環境のために作られたわけではないNext.jsなどのフレームワークをそのままLambda上で動かすことができるようになります。
当初は、このLambda Web Adapterを用いて、TerraformとLambrollでインフラ構築を行っていました。しかし、実際にデプロイ作業を行っていく中で、DBのパスワードなどの外部公開しない環境変数の渡し方で困ってしまいました。外部公開したくないためECRにpushはせず、Secrets Managerを参照して取得したいですが、調べた限りではLambdaではそのためのコードを書いて環境変数の取得を行う必要がありました。(参考)
環境変数はSecrets Managerで管理して、それを直接参照して使えるようにしたかったため、Lambdaの使用はやめ、ECS (Fargate) を用いるように変更しました。ECSではコンテナの定義にSecrets Managerのパスを書くことで直接参照することができます。
以下はTerraformで定義したSecret Managerをecspressoで参照してデプロイを行う例です。
{
"name": "DATABASE_URL",
"valueFrom": "{{ tfstate `module.secret_manager.aws_secretsmanager_secret.control_db.arn` }}:database_url::"
}
さいごに
1ヶ月という短い間でしたが、技術選定からフロントエンド、バックエンド、インフラ構築、CI/CDと様々な技術領域に触れることができ、非常に貴重な経験となりました。特に、実際に業務を進めていく中で、当初の想定通りに進まない事態に直面し、その都度相談しながら解決策を探るという実務のリアルな側面を体験することで大きな学びを得ることができました。また、この経験を通じて、実務における技術選定や計画の難しさと、状況に応じて柔軟に対応していく重要性を実感することができました。
今回のインターンシップで得た学びと経験を元にこれからも成長していき、ユーザーに価値を届けられるようなエンジニアになっていきたいです。