はじめに
こんにちは。アメリカ在住で独学エンジニアを目指している Taira です。
ついに私の個人開発アプリ「Juku Cloud」のMVP版が完成しましたので、紹介させてください。
GitHub リポジトリ
- プロジェクト全体レポジトリ
- フロントエンド
- バックエンド
Juku Cloudとは?
Juku Cloudは、私が3年間運営していた個別指導塾の現場で感じた課題をもとに開発したアプリです。
個別指導塾を運営するうえで、特に「生徒の特性を把握し、それに合わせた指導を行うこと」はとても重要です。
とはいえ、「生徒の特性」と言われても少しイメージしづらいですよね。学校と塾の違いで考えてみると、わかりやすいかもしれません。
学校の授業では、クラス全体のカリキュラムに合わせて進むため、学力の高い子・低い子、積極的な子・静かな子など、個々の特性を細かく考慮することは難しいです。
一方で個別指導塾では、一人ひとりに合わせた柔軟な対応が求められます。宿題のレベルを調整したり、内気な生徒には質問しやすい雰囲気をつくったりなど、「その子に合った教え方」が常に必要になります。
ただ、こうした特性は講師たちがなんとなく意識していても、言葉として共有されることはほとんどありません。
そのため、別の講師が授業を担当すると、生徒から「なんか今日の先生、いつもの先生と違う感じがする」と言われることもあります。
Juku Cloudは、こうした課題を解決するために生徒の特性を「良い特性」と「注意が必要な特性」に分類し、クラウド上で一元管理できるようにしました。
講師間での情報共有をスムーズにすることで、生徒一人ひとりに最適な指導を提供できるようになります。
開発背景
私はソフトウェアエンジニアを目指す前、3年間にわたり個別指導塾を経営していました。
学習塾では受験対策や成績向上が最重要ですが、それ以上に、生徒一人ひとりの特性を理解し、最適な指導を行うことが欠かせません。
私が教えていた生徒の中には、理解が不十分でも「わかった」と言ってしまう子がいました。
そのような生徒には、段階的に理解度を確認しながら進める必要があります。
一方で、理解力が高く、基礎でつまずかない生徒もいて、その場合は応用的な内容まで進めることで、さらなる成長を促せました。
つまり、生徒を指導するうえで、さまざまな特性を把握し、どう対応すべきかを判断する力が求められます。しかしこれらの特性は言語化されづらく、日々の授業準備や業務の中に埋もれてしまうことが多いのです。
ある日、私が担当していた生徒のAちゃんから、「先生の授業、なんか楽しくないんだよね」と言われたことがありました。
Aちゃんは「楽しい雰囲気の中でこそ集中できる」という特性を持っていたのですが、当時の私は「まずは成績を上げなければ」という思いが強く、その配慮ができていませんでした。
それ以降、授業中に学校や日常の話を交えながら、少しでも楽しい雰囲気づくりを意識したところ、Aちゃんは以前よりも積極的に勉強に取り組むようになりました。
この経験を通して、「生徒の特性を言語化して共有できれば、もっと良い指導ができるのに」と感じたことが、Juku Cloudを開発しようと思った最初のきっかけでした。
そして現在、エンジニアとして学び続けた結果、あのとき抱いた課題を解決できるアプリを自分の手で作れるようになりました。
生徒一人ひとりの特性をクラウドで可視化・共有し、指導を標準化する。そんなビジョンのもとに開発したのが「Juku Cloud」です。
Juku Cloudは、生徒の特性を「良い特性」と「注意が必要な特性」に分類し、クラウド上で一元管理します。
これにより講師間の情報共有が容易になり、生徒一人ひとりに最適な指導を提供できます。
さらに授業の引き継ぎ事項を科目ごとに管理できるため、引き継ぎにかかる時間を大幅に削減し、より教育そのものに集中できる環境を実現することができます。
技術スタック
| カテゴリ 技術 | |
|---|---|
| フロントエンド | React 19.2.0 / Vite 7.0.4 / TypeScript 5.8.3 |
| バックエンド | Ruby 3.4.4 / Ruby on Rails 8.0.3(APIモード) |
| データベース | PostgreSQL 15.14 |
| 認証 | devise token auth 1.2 |
| 環境構築 | Docker,Devcontainer |
| CI/CD | GitHub Actions(CloudFront/S3, ECR/ECSデプロイ, OIDC認証) |
| インフラ | AWS(ECS / ECR / RDS / CloudFront / S3 / Route53 / ACM / ALB / Secrets Manager / CloudWatch) |
| テスト | Vitest / Testing Library / MSW / RSpec / SimpleCov |
| 静的解析 | ESLint / Prettier / RuboCop / Bullet / Bundler Audit |
| その他(フロントエンド) | Tailwind CSS / Shadcn UI / Zustand / TanStack Query / Zod |
| その他(バックエンド) | Kaminari / Alba / Letter Opener Web |
フロントエンド
React
- 既存プロジェクトに導入するわけでもなく、これから事業として作ってくアプリで長期視点で安定版ではなく、最新のバージョンを採用しました。
- 高速なビルド・開発環境を提供するViteを採用し、ReactでコンポーネントベースのUIを構築。
- SPAを導入するうえでVueも検討しましたが、以下の点でReactを選択しました。
- Tailwind CSSとの親和性が高く、UIライブラリ系(shadcn/uiなど)が充実している。
- TypeScriptとの親和性が高く、型安全な開発がしやすい。
Vite
- React Create Appは公式から非推奨となり、Viteが主流となっていることから採用の検討を開始しました。
- React 公式に書いてあるほかのビルドツール(Parcel,Rsbuildなど)と比較してもViteが最も高速であり、
Vitestとの相性もいいため採用しました。
TypeScript
- 型安全なコードを書くことで、バグの早期発見・保守性向上を図るために採用しました。
- Zodと組み合わせて、APIレスポンスのバリデーションと型定義を一元管理できます
バックエンド
Ruby
- 長期運用を見据え、安定性よりも将来性を重視して最新バージョンを採用しました。
- 日本で開発された言語ということもあり、ほかの言語と比べて学習コストが低く、個人開発に適していると判断しました。
- オブジェクト指向言語であり、責任分割や設計パターンを適用しやすい点も評価しました。
Ruby on Rails (APIモード)
- RailsはRubyの代表的なWebフレームワークで、豊富なライブラリ・コミュニティが存在するため採用しました。
- APIモードを採用することで、フロントエンドとバックエンドを分離し、将来的にモバイルアプリなど他クライアントからもAPIを再利用しやすい構成としました。
データベース
PostgreSQL
MySQLも検討しましたが、以下の理由でPostgreSQLを採用しました。
- 今後の拡張性を考えて、PostgreSQLの方が高度な機能が多く、柔軟なデータ操作が可能。
- 生徒、講師、引継ぎ事項、科目、生徒の特性など、複数テーブルにまたがる整合性が非常に重要であり、PostgreSQLの堅牢なトランザクション管理が有利。
- MySQLの素早い動作よりも、PostgreSQLの正確で壊れないデータ構造の方が重要と判断。
認証
devise token auth
-
devise_token_authを採用し、access-token/client/uidをLocalStorageに保持しています。 - Axios インターセプタで各リクエスト時にヘッダへ付与し、レスポンスヘッダに含まれる新しいトークンを用いてローテーションを行います。
- サーバ側にセッションを保持しないためRails APIモードに最適であり、Cookieベースの認証と比較して CSRF攻撃のリスクを低減 できます。
- 一方で LocalStorageはXSSに弱いため、CloudFrontでCSP(Content Security Policy)を設定して外部スクリプト実行を制限しています。
詳細は こちら を参照。
環境構築
Docker
- 開発環境と本番環境の差異を最小化し、一貫した動作環境を提供するために採用しました。
- Docker Compose により、Rails・PostgreSQL・React の各サービスをコンテナで統合管理。
- チーム開発時も
docker compose upだけで環境を再現可能です。
Devcontainer
- VS Code 上で自動的に開発環境を構築できるよう、Devcontainer を導入しました。
- Ruby や Node.js、AWS CLI などをあらかじめイメージに含め、セットアップの手間を最小化しています。
- コンテナ起動時もコマンドが不要で、「コンテナを再度開く」だけで開発環境にアクセスできます。
CI/CD
GitHub Actions
- GitHub Actions を採用し、コードの変更を自動的にビルド・テスト・デプロイするワークフローを構築しました。
- バックエンドは ECR への Docker イメージのビルド・プッシュ、ECS へのデプロイ、DBのmigrateを自動化。
- フロントエンドは S3 へのビルド済みアセットのデプロイと CloudFront キャッシュの最適化を自動化。
インフラ
AWS
- 無料のRenderも検討しましたが、豊富なサービスと高い拡張性を持つAWSを採用しました。
- それぞれのサービスの選定理由については こちら を参照してください。
テスト
Vitest, Testing Library, MSW
- Jestも検討しましたが、Viteとの親和性が高く、高速なVitestを採用しました。
- React Testing Library、MSWを組み合わせて、単体テストだけではなく、APIモックを利用した結合テストも実施しています。
RSpec / SimpleCov
- 以前作成したTodo アプリではMinitestを採用しましたが、テストの網羅性を高めるためにRSpecを採用しました。
- SimpleCovを導入し、コードカバレッジを測定。目標カバレッジ80%以上を設定しています。
静的解析
ESLint / Prettier
- ESLint と Prettier を組み合わせて、コードの一貫性と可読性を確保しています。
RuboCop / Bullet / Bundler Audit
- RuboCopを導入し、コードスタイルと品質を自動的にチェックしています。
- Bullet を導入し、N+1クエリなどのパフォーマンス問題を検出。
- Bundler Audit を使用して、依存関係の脆弱性を定期的にチェックしています。
その他(フロントエンド)
Tailwind CSS
- Shadcn UI を利用するために Tailwind CSS を採用しました。
Shadcn UI
- Tailwind CSS をベースにしたコンポーネントライブラリで、今回のような業務アプリに適したUIコンポーネントが豊富に揃っているため採用しました。
Zustand
- Context API よりもシンプルなグローバルステート管理が可能で、
useStateライクな書き方で直感的に扱えて学習コストが低いため採用しました。
TanStack Query
- サーバー状態の管理が容易で、データフェッチング・キャッシュ・同期化・更新を効率的に行えるため採用しました。
Zod
- バックエンドだけでなく、フロントエンドでもバリデーションを一元管理できるため、一部のスキーマ定義に採用しました。
その他(バックエンド)
Kaminari
- ページネーションを非常に簡単に実装できることがとても魅力的であったため採用しました。
# page = params[:page]
# per_page = params[:per_page]
students.preload(ASSOCS).order(:id).page(page).per(per_page)
Alba
- 最初は自作のシリアライザを使用していましたが、Albaを採用することでAPIレスポンスのスキーマを統一的に管理できるようになりました。
Letter Opener Web
- 開発環境でメール送信をブラウザ上で確認できるため、採用しました。本番ではSESを使用しています。
こだわった点
技術面
フロントエンド
Zod を導入してフロントでもバリデーション
上記は授業引継ぎを作成する際の有効期限のバリデーションスキーマです。
有効期限って本来は今日以降の日付じゃないとダメですよね?
フロントにはこういったバリデーション機能が備わっていないので、もしZodを使わないとすると自作バリデーションやバックエンド側でのバリデーション実装が必要になります。
ただし、自作バリデーションを実装するとコードが煩雑になるうえに保守性も下がりますし、バックエンド側でバリデーションを実装するとユーザー体験が悪くなります。
そこで、一例ですが以下のようなZodスキーマを定義して、フロントエンド側でも有効期限のバリデーションを実装しました。もちろんバックエンド側でも同様にバリデーションを実装しているので、二重の安全策にもなっています。
// 有効期限のバリデーションスキーマ
export const ExpireDateSchema = z.string().refine(
(val) => {
// startOfDay を使用し時間を切り捨てる
const today = startOfDay(new Date());
const inputDate = startOfDay(parseISO(val));
return inputDate >= today;
},
{
message: "有効期限は今日以降の日付を入力してください",
}
);
生徒の新規作成・編集
生徒作成をする際に大変だったポイントは以下の通りです
- 新規作成と編集でフォームの挙動を共通化したい
- 生徒の基本情報に加えて、担当科目(subject_ids)、指導可能日(available_day_ids)、指導の関係性(assignments)など複数の関連情報を一括で管理する必要があった
- 曜日、科目に該当する講師を動的に取得して、担当関係(assignments)を管理する必要があった
新規作成と編集でフォームの挙動の共通化
Mode という型を用意して、Conditional Typesを使用して型を切り替えました。
export type Mode = "create" | "edit";
export type SchemaByMode = {
create: typeof createStudentSchema;
edit: typeof editStudentSchema;
};
// モードに応じて submit の型を切り替える
export type PayloadByMode<M extends Mode> = z.infer<SchemaByMode[M]>;
// modeに応じてDraftの型を切り替える
export type DraftByMode<T extends Mode> = T extends "edit" ? EditDraft : Draft;
呼び出すときは、create か edit を指定します。
// features/students/components/dialog/CreateStudentDialog.tsx
<StudentForm
mode="create" // ここでモードを指定
value={value}
onChange={setValue}
onSubmit={() => {
submit(
// zod のバリデーションを通ったら mutate を呼ぶ
(valid) => mutate(valid),
// zod のバリデーションに落ちたらエラーメッセージをトースト表示
(msgs) => msgs.forEach((m) => toast.error(m))
);
}}
loading={isPending}
teachers={teachers}
/>
基本情報以外の関連情報の一括管理
まずは、以下が新規作成する際のuseStateの型です。編集にはこれにidが追加されます。
export type Assignment = {
teacher_id: number;
subject_id: number;
day_id: number;
};
export type Draft = {
name: string;
status: string;
school_stage: string;
grade: number | null;
desired_school: string | null;
joined_on: string; // YYYY-MM-DD形式の文字列
subject_ids: number[];
available_day_ids: number[];
assignments: Assignment[];
};
上記のように、生徒の基本情報に加えて、担当科目(subject_ids)、指導可能日(available_day_ids)、指導の関係性(assignments)など複数の関連情報を一括で管理する必要があります。
そのため、これらをuseStateに入れるために成形するユーティリティ関数を作成しました。
export const createStudentFormHandlers = <M extends Mode>(
onChange: OnChange<M>
) => {
// Draftの型をmodeに応じて切り替え
type Draft = DraftByMode<M>;
const handleInputChange =
(field: keyof Draft) => (e: ChangeEvent<HTMLInputElement>) => {
onChange((prev) => ({ ...prev, [field]: e.target.value }));
};
// 入塾日と通塾状況の際に使用
const handleSelectChange =
<T extends keyof Draft>(field: T) =>
(value: Draft[T]) => {
onChange((prev) => ({ ...prev, [field]: value }));
};
// 学年セレクトの際に使用
const handleStudentOptionChange = (value: string) => {
onChange((prev) => {
if (value === "") return { ...prev, school_stage: "", grade: null };
const [stage, grade] = value.split("-");
return { ...prev, school_stage: stage, grade: Number(grade) };
});
};
// 科目・曜日に使用、追加や削除ができる
const toggleInArray = (key: ToggleableKeys, id: number) => {
onChange((prev) => ({
...prev,
[key]: toggleValueById(prev[key] ?? [], id),
}));
};
const toggleAssignmentInForm = (a: Assignment) => {
onChange((prev) => ({
...prev,
assignments: toggleAssignment(prev.assignments ?? [], a),
}));
};
return {
handleInputChange,
handleSelectChange,
handleStudentOptionChange,
toggleInArray,
toggleAssignmentInForm,
};
};
上記の関数を使用することで、onChangeを使用して簡単に各フィールドの更新が可能になります。
講師の動的取得
これも、講師と選択した科目・曜日に応じて動的に取得するユーティリティ関数を作成しました。
export const buildTeachersByTab = (
teachers: Teacher[],
selectedSubjectIds: number[],
allDayIds: readonly number[]
) => {
// 高速処理できるように Set に変換
const subjectSet = new Set(selectedSubjectIds);
// 選択した曜日・科目に応じて、曜日ごとに講師を分類
const byDay: Record<number, Teacher[]> = {};
for (const dayId of allDayIds) {
byDay[dayId] = teachers.filter((t) => {
// その曜日に勤務していて、かつ選択された科目を指導可能な講師
const worksThatDay = t.workable_days?.some((d) => d.id === dayId);
const subjectOK = t.teachable_subjects?.some((s) => subjectSet.has(s.id));
return worksThatDay && subjectOK;
});
}
return { byDay };
};
上記の関数を使用することで、選択した科目・曜日に応じて講師を動的に取得できます。
エラーハンドリングの統一
毎回エラーハンドリングを書くのは冗長なので、共通化しました。
まずは、エラーの型定義です
type Errors = {
code: string;
field?: string;
message: string;
};
type CommonServerError = {
errors?: Errors[];
};
Devise Token Auth は独自のエラーフォーマットを返しますが、それを上記の型に変換して扱うようにしています。
# app/controllers/api/v1/auth/registrations_controller.rb
def render_update_error_user_not_found
render_error!(
code: "USER_NOT_FOUND",
message: I18n.t("devise_token_auth.registrations.user_not_found"),
status: :not_found
)
end
# render_error! メソッドはエラー表示をするためのメソッドです
次に、フロントでのエラーハンドリング共通化を以下のように実装しました。
// src/lib/getErrorMessage.ts
import { isAxiosError } from "axios";
import { ZodError } from "zod";
type Errors = {
code: string;
field?: string;
message: string;
};
type CommonServerError = {
errors?: Errors[];
};
export const getErrorMessage = (error: unknown): string[] => {
if (!error) return ["無効なエラー"];
// axiosError
if (isAxiosError<CommonServerError>(error)) {
const response = error.response?.data;
const status = error.response?.status;
if (typeof status === "number") {
if ([401, 403, 404, 500].includes(status)) {
return [
response?.errors?.[0].message || "予期せぬエラーが発生しました。",
];
} else if ([400, 422].includes(status)) {
return (
response?.errors?.map((error) => error.message) || [
"予期せぬエラーが発生しました。",
]
);
}
}
}
// ZodError
if (error instanceof ZodError) {
return ["データ形式が不正です"];
}
// 通信エラーなど
return ["通信エラーが発生しました。"];
};
このメソッドを導入するだけで、毎回書いていたエラー処理のわずらわしさを解消できました。
カバレッジ80%以上
npm run test:coverage 実行結果は以下の通りです。
- stmts(実行文の網羅率): 97.49%
- branch(分岐の網羅率): 91.99%
- func(関数の網羅率): 91.6%
- lines(行の網羅率): 97.49%
バックエンド
招待トークンの実装
招待トークンを使用して講師を登録するフローを実装しましたが、その際にトークンの改ざん防止と検索可能性を両立させる必要がありました。
| 方法 | 強み | 弱み | 招待トークン用途での向き |
|---|---|---|---|
| HMAC-SHA256(今回採用) | 改ざん防止、速い、検索可 | 秘密鍵漏洩で終わり | ◎ 最適 |
| 単純 SHA256 | 実装簡単 | 辞書攻撃されやすい | × |
| bcrypt/scrypt/argon2 | 総当たり超耐性 | 非決定的 → 検索不可、遅い | △(パスワード向き) |
Rails MessageVerifier
|
改ざん検出+署名 | トークン文字列が長くなる | ○(署名つきリンク向き) |
| AES 暗号化 | 復号できる | 招待トークンは復号不要 | × |
bycryptなどの非決定的ハッシュは検索できないため不向きであり、MessageVerifierはトークンが長くなるためUX的に不向きと判断し、HMAC-SHA256を採用しました。
# app/services/invites/token_generator.rb
module Invites
class TokenGenerate
def self.call(school, role: :teacher, expires_at: nil, max_uses: 1)
raw_token = SecureRandom.urlsafe_base64(32)
digest = Invite.digest(raw_token)
Invite.create!(
token_digest: digest,
school: school,
role: role,
expires_at: expires_at || 7.days.from_now,
max_uses: max_uses
)
raw_token
end
end
end
# app/models/invite.rb
class Invite < ApplicationRecord
.
.
.
# raw_tokenをdigest に変換
def self.digest(raw_token)
secret = Rails.application.secret_key_base
OpenSSL::HMAC.hexdigest("SHA256", secret, raw_token)
end
end
Studentの新規作成・編集の最適化
Studentの新規作成・編集に関して、関連情報の設定やトランザクション管理をサービスオブジェクトで整理しました。
-
create_service.rb(Student作成サービス) orupdater.rb(Student更新サービス)-
save_transaction(Studentをトランザクション内で保存)-
relation_setter(Studentの関連情報を設定)
-
-
そして、relation_setterではクエリの最適化を意識してupsert_allやdelete_allを活用しました。
# app/services/students/relation_setter.rb
def update_student_subject_links!(student_id, subject_ids, now:)
rows =
subject_ids.map do |sid|
{
student_id: student_id,
class_subject_id: sid,
created_at: now,
updated_at: now
}
end
Subjects::StudentLink.upsert_all(
rows,
unique_by: %i[student_id class_subject_id]
)
# 残すscs_idsを取得
keep_link_ids =
Subjects::StudentLink.where(
student_id: student_id,
class_subject_id: subject_ids
).pluck(:id)
# 削除するscs_idsを取得
to_remove_scs =
Subjects::StudentLink
.where(student_id: student_id)
.where.not(id: keep_link_ids)
# 関連するlesson_notes を削除
LessonNote.where(student_class_subject_id: to_remove_scs).delete_all
to_remove_scs.delete_all
end
これ以外にも、class_subjects_idsを更新したい場合はstudent.class_subjects = subject_idsを実行することで、関連情報を一括で更新できますが、ActiveRecordオブジェクトを大量に生成してしまうため、パフォーマンス面で不利です。
エラーハンドリングの統一
バックエンドで毎回エラーハンドリングを書くのは冗長で、ばらつきが出やすいのでconcernで共通化しました。
module ErrorHandlers
extend ActiveSupport::Concern
included do
# 500
rescue_from StandardError do |e|
raise e if Rails.env.development?
Rails.logger.error("[500] #{e.class}: #{e.message}")
Rails.logger.error(Array(e.backtrace).join("\n"))
render_error!(
code: "INTERNAL_SERVER_ERROR",
field: "base",
message: I18n.t("application.errors.internal_server_error"),
status: :internal_server_error
)
end
# 404
rescue_from ActiveRecord::RecordNotFound do |e|
render_error!(
code: "NOT_FOUND",
field: "base",
# 空文字列の場合にデフォルトメッセージを使う
message: e.message.presence || I18n.t("application.errors.not_found"),
status: :not_found
)
end
# 403
rescue_from ForbiddenError do |e|
render_error!(
code: "FORBIDDEN",
field: "base",
message: (e.message.presence || I18n.t("application.errors.forbidden")),
status: :forbidden
)
end
# destroy! 失敗時の例外
rescue_from ActiveRecord::RecordNotDestroyed do |e|
render_error!(
code: "RECORD_NOT_DESTROYED",
field: "base",
message: (I18n.t("application.errors.record_not_destroyed")),
status: :unprocessable_content
)
end
# 422 バリデーションエラー
rescue_from ActiveRecord::RecordInvalid do |e|
render_model_errors!(
e.record,
status: :unprocessable_content,
default_code: "VALIDATION_FAILED"
)
end
.
.
.
end
end
# エラーレスポンス例
# HTTPステータス: 422
"error": {
"code": "VALIDATION_FAILED",
"field": "email",
"message": "メールアドレスを入力してください"
}
このErrorHandlersをApplicationControllerにincludeするだけで、共通のエラーハンドリングが可能になります。
機能を開発する場合も、エラーを投げることのみを意識すればよくなり、コードの簡潔化と品質向上に寄与しました。
カバレッジ80%以上
bundle exec rspec 実行結果は以下の通りです。
Line Coverage: 98.19% (759 / 773)
Branch Coverage: 89.52% (94 / 105)
ここまで高いカバレッジを達成できたのは、責務分離を意識して設計したことが大きいです。
UI/UX
Student の作成・編集UI
- 科目はバッジ形式で表示し、選択を解除する際はバッジをクリックするだけでOK
- 科目と曜日を選択するまで講師が表示されないようにして、無駄な情報を排除
- 曜日ごとにタブ切り替えできるようにして、講師の情報量を削減
- 選択された講師は、名前、科目、曜日がわかるようになっていて、これもバッジ形式で表示。
色合いに意図を持たせて直観的に操作できるように
- 生徒の特性は、良い特性は緑、注意すべき特性は黄色で表示
- 授業引継ぎはテーブルの色で直観的に科目がわかるように工夫
苦労した点
個性を共有化する学習塾管理アプリが特殊で評価を受けるか心配だった
ほかのエンジニア志望の型のポートフォリオはBtoCが多くフィードバックをもらいやすい一方で、私のアプリは学習塾を経営していて「あったらいいな。」と思った機能を盛り込んだアプリになります。
なので、エンジニア以外の方にとってはニッチすぎて評価されないのではないか?と心配でしたし、今も評価フェーズではないので心配しています。
エンジニア像って多様なので一概には言えませんが、私が目指すエンジニアは「ユーザーの課題を技術で解決できるエンジニア」なので、ニッチな分野でもユーザーの課題を解決できるアプリを作ることに価値があると信じて開発を進めました。
React + TypeScript の実装が初めてで常に壁にぶつかった
基礎は学んでいたので、今回の実装に入る際にはどのような技術をつかって実装するかはイメージできていました。
しかし、実際にコードを書き始めると、TypeScriptの型定義やReactのコンポーネント設計で多くの壁にぶつかりました。
そして、TanStack Query を知る前まではZustand でserver state を管理していましたが、非効率的であることに気づき、途中からTanStack Query に切り替えました。なので最初に実装したTeacher 関連のコードはZustand を使ったままになっています。
AWS の ECS / Fargate 構成、CDを自力で構築するのが大変だった
学習でAWSの基礎は学んでいましたが、ECS / Fargate構成での本格的なアプリ構築は初めてでした。
特にこういったインフラはどこの何をしているのかがブラックボックスになりやすく、トラブルシューティングに苦労しました。
また、CD構成も初めてで、権限の付与(今回はOIDC認証)やECSタスク定義の更新、デプロイの自動化など、試行錯誤しながら構築しました。
今後の展望
本番運用するために、最低限以下の機能追加・改善を予定しています。また、その後も継続的に改善を進めていく予定です。
機能追加
- マスター権限の実装をして、塾、管理者の管理機能を実装する
- 管理者作成も講師同様に招待トークンで登録できるようにする
- メールアドレス変更・パスワードリセット機能の実装
- 機能ごとに権限を管理者が付与できるように実装する
改善
- teachers の queries や mutations を tanstack query に移行して効率的なステート管理を実現する
追記
私自身、未経験から独学でエンジニアを目指し、試行錯誤しながら学習・転職活動を進めてきました。
その過程で「何に時間を使い、何を捨てたのか」「うまくいかなかった時にどう立て直したのか」を、
1本の記事にまとめています。
・これから独学でエンジニアを目指そうとしている方
・独学で進めているものの、手応えを感じられていない方
もし当てはまるものがあれば、参考になる部分もあると思うので、よかったらご覧ください。


