学生が知人のサロン業務をNextjsでDX化してみた話
1.背景
この記事は、私の知人が経営する美容サロンの業務を、Next.jsを用いてWebアプリケーション化し、業務改善を試みた事例について記録します。
(サロン側には公開の許可を得ております)
1.1 自己紹介
現在、北海道のとある大学に通っています。専攻は建築で、26卒です。
普段は、自分の専攻分野の研究を進めつつ、web開発のインターンを行っています。
就活では「建築かITか」で迷った時期もありましたが、最終的にエンジニアとしての就職を決め、今はエンジニア生活に少しずつシフト中です。
1.2 知人サロンからの「HP制作依頼」──しかし本当に必要なのは?
ある日、母を通じて、美容サロンを経営している方から「ホームページを作ってほしい」という相談が届きました。私は普段からWeb開発に取り組んでいたこともあり、快く引き受けました。なにより、その方は浪人時代から私を支えてくれた恩人でもあり、何かしら恩返しがしたいという気持ちがありました。
ヒアリングを進めていくと、単なるホームページ制作では本質的な課題解決にならないということが見えてきました。
1.3 課題の本質を深掘り
このサロンでは、ヘア・ネイル・エステ・マツエク・マシンピラティスといった多様なサービスを展開しています。しかし、既存のお客様の多くが、そこが複合サロンであると気づいていない状況であるそうです。
さらに調査を進める中で、以下のような問題が浮かび上がってきました。
- HotPepperBeautyでは「ヘア」と「その他」で予約媒体が分かれていて、ユーザーが迷子になる
- SNS(X / Instagram / LINE)ごとの投稿管理が煩雑で、どこに何を投稿したのか分からなくなっている
- 顧客も「どの媒体を見ればいいか」分からず、情報が届きにくい
このような状態でさらに「HPの更新作業」が発生すると、むしろ業務負担が増えるリスクがありました。
1.4 恩返しとして、本当に役に立つものを
浪人時代から応援してくれていた大切な方だったので、どうにか「本当に役に立つもの」をつくって恩返ししたいと考えました。そこで、HP単体ではなく、業務全体を見直せるSaaS型の業務支援ツールの開発を提案しました。
具体的には、以下のような「業務効率化」+「情報整理」を目的とした構成にしました:
- スタッフごとの指名予約リンクやメニューと、予約媒体(HotPepperなど)を一元管理
- カタログ・ブログ投稿機能(メニュー紹介、実績など)
- 投稿と同時にX、LINE公式など複数SNSに通知
- 投稿内容に対して、アクセス数・予約数などを計測・分析
- 顧客側からは、ブログやカタログを閲覧 → そのまま予約ページにアクセス可能
2.本アプリを支える技術
以下は本アプリケーションを支える技術について記録します。
2.1 フロントエンド
個人的に普段からReactを触る機会が多かったことから、今回はReactをベースに選定する方針にしました。
Reactの中でもNext.jsのフレームワークは、SSRと静的生成をページ単位で選択できる柔軟なデータ取得機構や、API Routesによってバックエンド機能も統合できる点が魅力で、今回作成する小規模なサロン向けアプリケーションに適していると判断しました
また、Next.js14では安定した機能が揃っており、App RouterではなくPage Routerを選択しました。構成のシンプルさや既存ドキュメントの豊富さも、この選択を後押ししました。
UIコンポーネントライブラリについては、導入コストやカスタマイズ性を踏まえてあえて使用せず、Sassをベースに独自でスタイリングしています。状態管理ライブラリについても、ページを跨ぐ複雑な状態は想定されなかったため、ReduxやRecoilなどは導入せず、必要最低限のstateはReactのuseState/useContextで管理しています。
2.2 インフラ構成と各種サービスの役割
本プロジェクトでは、モダンなフロントエンド環境とGoogle Cloud Platformの各種バックエンドサービスを組み合わせ、シンプルでありながら拡張性の高いインフラ構成を目指しました。
2.2.1 フロントエンドのホスティング
フロントエンドのホスティングにはVercelを使用しています。サロン側のアプリケーションは、基本的にNext.jsのSSR機能を用いて構築しています。一方、顧客側のアプリケーションに含まれるブログページとカタログページは、Next.js の ISR(Incremental Static Regeneration)機能を利用しており、ビルド時ではなくリクエストに応じて静的に生成されます。生成されたHTMLはVercelのCDNにキャッシュされます。(後述しますが)これらのページについても、サロン側の要望により常に最新の情報を表示する必要があったため、SSR的な動的性と、静的サイトならではの高速表示の両立を実現できる構成を採用しました。
2.2.2 バックエンドとデータベースまわり
データの保存にはFirebase Firestoreを採用しています。ブログやカタログといった更新頻度の高いコンテンツも、スキーマレスかつスケーラブルなFirestoreの特性と相性が良かったです。
当初はMySQLなどのリレーショナルデータベースも候補に挙がりましたが、取得するデータの大半は「ブログ一覧」「スタッフ一覧」などの単純なリスト取得であり、複雑なJOIN や集計処理は必要ないと判断しました。Firestoreで提供されるwhereやorderByを組み合わせたクエリで十分に対応できるため、今回はリアルタイム性や構築のしやすさを重視してFirestoreを選定しています。
ログイン・ログアウトなどの認証処理にはFirebase Authenticationを使用し、主にサロンスタッフ向けのアカウント管理に活用しています。また、画像データ(投稿画像・ヘッダー画像など)は Firebase Storageに保存し、アップロード後の表示・ダウンロードもスムーズに行えるようにしています。
2.2.3バックエンド処理の自動化
ブログ投稿やカタログ更新に伴うバックエンド処理として、以下のような非同期タスクが発生します:
- アップロードされた画像の圧縮処理・WebP形式への変換
- X(旧Twitter)や LINE公式アカウントへの自動投稿通知
- 投稿ステータスや予約データのFirestoreドキュメント更新処理
これらの処理は、Firebase単体では表現しづらい複雑なロジックを含むため、Cloud Functions(第1世代)を使用し、Cloud Build経由でビルド後にCloud Run上で実行する構成を採用しました。
Cloud FunctionsはTypeScriptで実装し、あらかじめcloudbuild.yaml
を用意しておくことで、CI/CDによる安定的なデプロイ環境を構築しています。具体的には、npm run deploy
のようなローカル手動実行ではなく、GitHubリポジトリへのpushをトリガーとしてCloud Buildが自動でデプロイ処理を行うようになっています。これにより、コードとインフラ構成の管理を GitHub上で一元化できるだけでなく、ビルドの状態やエラーも明示的に確認できる開発体制を整えることができました。
2.3 API仕様
本プロジェクトでは、Firestoreを主なデータベースとし、フロントエンドからSDK経由で直接データ取得・更新を行う構成としています。そのため、一般的なバックエンド開発におけるRESTful API設計(例:GET /posts/:id
など)は基本的に不要でした。ただし、ISRによる静的ページの再生成(/api/revalidate.ts
)についてのみ、Firebase Functionsを用いたAPI 実装も併用しています
Firestoreからのデータ取得後の処理については、Zodを用いたスキーマベースの型定義・バリデーションを導入しました。具体的には、Nxのモノレポ内にlibs/common/types
ディレクトリを設け、以下のような形で管理しています:
- 各ドメイン(例:
Menu
,Staff
,Blog
など)ごとにZodスキーマを定義 - Zodスキーマから
TypeScript
の型をそのまま推論し、Firestoreの入出力・フォーム入力の整合性に活用
また、Zodによってバリデーションが型に強く紐づいているため、スキーマの設計がそのままAPI仕様書のような役割を果たしています。結果として、ぼくの中での認識のズレも起こりにくくなりました。
2.4 ソースコード管理とリポジトリ構成
今回は最初から、「サロン運営側の管理画面」と「顧客向けの閲覧・予約導線を担う画面」という、役割の異なる2つのNext.jsアプリケーションを構築する想定でいました。そのため、初期段階からモノレポ構成での開発を前提としました。
モノレポを選定した理由は以下の通りです:
- コンポーネントの共通化による開発効率の向上
- 型定義の共有によるエラーの削減(特に FirebaseへのCRUD処理等)
- アプリ間での状態・ロジックの再利用(サービス層の使い回し)を想定していたため
- パッケージのバージョン管理を統一し、依存のズレを防ぐため
また、過去のインターンで 複数アプリケーションのモノレポ化による同一リポジトリの管理やアプリ間共通処理の切り出しなどを経験していたこともあり、ある程度スムーズに導入・運用できる見込みがありました。モノレポの管理にはNxを採用しました。
3.システム全体の設計・開発
この章では、デザインと要件をどのように検討・決定していったかについて記録します。
3.1 要件定義とUIデザイン
本プロジェクトでは、顧客向け画面とサロンスタッフ向けの管理画面の両方を、要件定義からUI設計・実装・保守運用まで一貫して担当しました。
サロン側との打ち合わせを進める中で、「あれもやりたい、これも追加したい」と要件が増えそうになる場面も多くありました。限られた時間と開発リソースの中で、シンプルさを維持しながら本質的な価値に集中できるよう、機能の取捨選択を丁寧に交渉しました。
UIデザインの検討・提案・話し合いには、建築学科の設計課題やコンペ経験で培われたスキルが少し活きたような気がします。もちろんプロのwebデザイナーの方のレベルには程遠いのですが、illustratorを用いて画面遷移やUIモックを作成しながら、サロンの方と一緒に確認・修正を重ね、要件を具体的なUIに落とし込んでいきました。
3.2 プロジェクトの進め方
私自身北海道に住んでおり、遠方での開発だったため、対面での打ち合わせが難しい時期にはLINEを中心としたテキストベースのやり取りを行い、着実に仕様を詰めていきました。
当時、ちょうど大学の春休み期間だったため、このタイミングを活かして一気にマークアップ作業を完了。千葉に戻った際に実機でのデモンストレーションを実施し、実際の使用感やフィードバックを直接もらうことができました。
3.3 投稿と自動ビルドの流れの設計
本システムでは、サロン側がブログやカタログを投稿した際に、画像最適化から静的ページの再生成までをすべて自動化しています。ここでは、カタログ投稿を例に、処理の全体像を紹介します。
投稿〜最適化処理
- サロンスタッフが管理画面のブラウザから新しいカタログを投稿
- フロントエンドサーバがFirestoreにデータが書き込む(
status=imaging
) - GCPのFirestoreトリガー関数が発火
- アップロードされた画像に対して最適化処理を実行:
- WebP形式への変換
- サイズ圧縮
- 元画像の削除
- 最適化が完了すると、Cloud FunctionsがFirestoreの該当ドキュメント内の画像 URLを最適化済みのものに書き換え
- 同時に
status=building
に更新
静的ページの再生成(ISR)
-
status=building
への変更をトリガーとして、Cloud Functions経由で顧客画面の再生成APIの/api/revalidate.ts
が叩かれる - 顧客側アプリのサーバー上でISRのキャッシュがstale状態になる
- stale状態になったことを受け、 Cloud Functionsから顧客側アプリのサーバーにISRの再生成リクエストが送られる
- サーバー上でcurlコマンドが叩かれ、対象ページのビルドが行われる
- ビルドが完了すると、Firestoreの
status=public
に更新される - 顧客画面に初めてカタログが表示される
表示とキャッシュ戦略
この一連の流れにより、最適化された画像を含むページが静的に生成・CDNにキャッシュされ、一般顧客に対しては初期のビルド遅延をなくしつつ、高速かつ安定した閲覧体験が提供されます。再ビルドは「サロンがブログ・カタログを新規作成 or 更新した場合」のみに限定されており、余分な再生成やCDNの更新は発生しないようにしています。
3.4 そもそもISRを採用した理由
当初、顧客向け画面にはSSRを採用していました。というのも、サロンからは「頻繁に更新する予定があるので、常に最新の状態をお客さんに見てもらいたい」という要望があったため、SSR によってリアルタイムでデータを反映する構成が妥当だと判断していました。
ただし、実際の運用を進める中で問題が発生しました。
サロン側の要望で、50-100件近いメニューと複数の画像(当初は未最適化)を一覧で表示する必要があり、それを毎回SSRで描画する構成では、ページの読み込みが非常に重くなってしまいました。実際にサロンの方にお見せしたところ「ちょっと重いですね……」という率直なフィードバックを受け、描画速度(FCPやLCP)と通信コストの見直しが急務となりました。
課題となったのは以下の点です:
- SSRのように、ブログやカタログなどの更新を即時に反映したい
- しかし、ユーザーにはSSGのような高速な描画体験を提供したい
- SSG に切り替えると描画は高速になるが、コンテンツを更新するためには、全体ビルドが必要で即時反映ができない
- ISR(revalidateの秒数指定)を使うと、周期的にCloud Functionsや顧客アプリサーバへのリクエストが発生し、加えて毎回「最初にアクセスしたユーザー」が HTML再生成のコストを負担することになる
これらを踏まえ、Next.jsのISRを活用しつつ、再生成のタイミングを制御できる構成を採用しました。具体的には、Firestoreのstatusフィールドの更新をCloud Functionsで監視し、必要なときだけ、/api/revalidate
エンドポイントを呼び出して対象ページのHTMLを再生成します。結果として、SSGのような高速な描画とSSRのような即時反映が両立でき、運用負荷を抑えながらも、サロンとエンドユーザーの双方にとって快適な体験を提供することができました。
4.導入した結果
現時点では、導入直後のため詳細な数値(PV数や予約数の増減など)はまだ計測中です。
今後、一定期間運用した後に改めて成果データを取得し、本記事を更新する予定です。
予約動線の整理や投稿自動化による業務負担の軽減については、すでにスタッフの方から好意的な声をいただいています。
5.まとめ&感じたこと
以下にまとめと感じたことについて記録します。
5.1 プロジェクト全体を振り返って
今回のプロジェクトでは、実装だけでなく、ヒアリングやデザインなど、エンジニアリングの枠を超えた多くの工程を経験しました。個人開発ではありますが、サロン運営の現場課題に向き合い、実際に動く仕組みとして形にできたことは大きな学びでした。
サロンの方に2回目のデモをお見せしたとき、「前よりすごく良くなってるね」と笑顔で言ってもらえたのがとても印象的でした。その瞬間、単純に「あ、いいことできたな」と思えたのをよく覚えています。技術力だけでなく、相手の立場に立って考えること、現場のリアルな課題に寄り添うことが大事で、それこそが DXの本質なんだと、今回の経験を通じて実感しました。
5.2 UIデザインについて
UIデザインについても、試行錯誤の連続でした。
- 顧客向け画面では、サロンの雰囲気や世界観を壊さず、自然に馴染むようなビジュアルにすること
- 管理画面では、操作に迷わない・すぐ使ってもらえるような直感性を意識すること
…と、それぞれ異なる“使う人”に合わせた視点が求められました。
ただし、いざ作る段階になると難しく、自分の「やりたいデザイン」や「こう見せたい」というこだわりをどこまで捨てるべきか悩む場面が多くありました。正直、建築で培った作家性はこの場面ではむしろ邪魔になってしまい、“使いやすさに徹する”バランス感覚の難しさを痛感しました。
一方で、自分のこだわりを完全に捨ててしまうと「何も作れない」というジレンマもありました。結果として、「自分らしさ」と「使いやすさ」のちょうどよい距離感を見つけることが、今後の課題だと感じています。
6. 最後に
ここまでお読みいただき、ありがとうございました。
今後もエンジニアとして一歩ずつ成長していけるよう、精進してまいります。
Discussion
Cool