初めて個人開発のWebアプリをデプロイした話【MediNote】

# 個人開発でお薬管理アプリ「MediNote」をリリースするまで【体験記】

## はじめに

はじめまして！初めての投稿です。
私が個人開発でお薬管理アプリ「[MediNote](https://medicinenote.vercel.app)」を費用ゼロで作成し、デプロイまでの一連の流れを経験できたのでその体験談を投稿したいと思います。
ちなみにVSCodeにGithub Copilotを入れて大部分で補助をもらいながらコーディングしているので、独力ではなくAIの助けを借りながら開発しています。

## 私の簡単なプロフィール
- 社労士、人材紹介業を経て、現在は大学（2度目の大学生）の経済学部に所属する社会人学生です。
- 初めてプログラミング言語に触れたのは2023年4月で、その時に基本的なPythonの文法を勉強しました。
- 主には微積分・線形代数、統計学、機械学習周りの勉強を細々と続けています。エンジニアとしての実務経験はおろかIT業界での経験もないため独学のアウトプットの場として[github](https://github.com/yf591)や[Hugging Face](https://huggingface.co/yf591)につたないですが投稿させてもらってます。

## 目次


1. [開発のきっかけ](#開発のきっかけ)
2. [MediNoteの機能紹介](#medinoteの機能紹介)
3. [選んだ技術スタック](#選んだ技術スタック)
4. [開発の流れ](#開発の流れ)
5. [自分にとって特に苦労したポイントと解決方法](#自分にとって特に苦労したポイントと解決方法)
6. [セキュリティ対策](#セキュリティ対策)
7. [デプロイと自動化](#デプロイと自動化)
8. [実装して学んだこと & まとめ](#実装して学んだこと--まとめ)

## 開発のきっかけ
花粉症の季節になってきてアレルギーのお薬を処方してもらったのですが、飲み薬や目薬、それに常用しているサプリメントなどを含めると日常的に複数の服用を管理する必要が出てきました。
夜型なので週末には夜遅くまで作業をすることも多くて生活リズムが乱れるので、そんなとき「あれっ？今日飲んだっけ？」ということがあり、そんな心配を解決する方法がないかなと考えたことが開発の一つのきっかけです。
既存のアプリもいろいろあるけど、どうせなら勉強がてら「とりあえず試しに自分でほしい機能を作ってみよう」と思い開発に踏み切りました。

## MediNoteの機能紹介
機能の紹介の前にざっくりどんなアプリなのかということだけ下記に簡単にまとめました。
※仕様書からの抜粋なので若干堅苦しい文章ですがご容赦を。
```
### MediNoteの概要

お薬の服用を管理し、飲み忘れを防ぐためのシンプルなウェブアプリケーション。
ユーザーは服用している薬の情報を登録し、服用記録をつけることができます。
UI/UX は直感的で使いやすいデザインを意識しています。

### 特徴

- 直感的な操作での簡単記録: 薬名登録以外の手入力の手間を極力減らし、タップ操作中心の直観的な操作性を実現
- 時間帯別管理: 服用タイミングごとに分類・色分けされたお薬リスト表示
- 在庫管理: 残量を一目で把握し、予想切れ日を自動計算・警告表示
- 統計情報: 服用履歴の分析、服薬遵守率の可視化
- 安全な個別データ管理: ユーザーごとのデータ分離とアクセス制限
```

### 機能紹介
[MediNote](https://medicinenote.vercel.app)には以下のような機能（一部を抜粋）を実装してみました。

1. **薬の管理**
   *時間帯別に左ふちの色を変えた薬の管理画面*
   ![スクリーンショット 2025-03-29 035439.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4044731/ba0a2b45-6198-411a-bc79-f774f6149b00.png)

2. **薬の登録**
   *タップ操作中心でなるべく手入力を省略*
   ![スクリーンショット 2025-03-29 035920.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4044731/16d593a2-3b23-463a-8424-34cdedb14778.png)

2. **服用記録の登録**
   *タップ操作中心でなるべく手入力を省略*
   ![スクリーンショット 2025-03-29 040431.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4044731/51fbe84e-ced5-4912-b1b4-4e930a0bc2b8.png)


3. **在庫管理と予想切れ日の自動計算**
   *残り日数に基づいて視覚的な警告を表示*
   * まだ安心なので緑
   ![スクリーンショット 2025-03-29 040751.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4044731/b98f159f-2041-4ae8-ae3d-0f08bd89a2d9.png)
   * もうないので赤
   ![スクリーンショット 2025-03-29 041452.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4044731/5e17b757-3430-46c9-b9b7-410d6a2466dd.png)

4. **統計情報の視覚化**
   *服薬遵守率や服用パターンの分析*

   ![スクリーンショット 2025-03-29 041737.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4044731/b9984e81-5b00-479a-82b8-1bfb5d403cb0.png)

　 ![スクリーンショット 2025-03-29 041910.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4044731/2df52227-8f5a-43ea-b823-d3cd443978af.png)



## 選んだ技術スタック

ここでは私が選んだ技術スタックと選定理由を書きたいと思います。

### フロントエンド
- **Next.js (React)**: アプリケーションのUI構築
- **TypeScript**: 型安全なコード記述のため
- **TailwindCSS**: UIデザインのため
- **date-fns**: 日付操作のため

### バックエンド
- **Firebase**:
  - Authentication: ユーザー認証（メアドとGoogleアカウントでの認証を有効にしました）
  - Cloud Firestore: データベース
  - Cloud Storage: 画像の保存（プロフィール画像を変更できるようにしたため使用）
  - Cloud Functions: レート制限実装
  - Security Rules: データアクセス制御（Firestore、Storage）

### デプロイ・インフラ
- **Vercel**: フロントエンドのホスティング
- **GitHub Actions**: Firebase自動デプロイ

### 技術スタック連携図
*MediNoteの技術連携図*

```mermaid
graph LR
    subgraph ClientSide
        A["Next.js (React)"] --> B["React Context (状態管理)"]
        A --> C["TailwindCSS (UI)"]
        B --> D["Firebase SDK"]
    end

    subgraph ServerSide["ServerSide (asia-northeast1リージョン)"]
        D --> E["Firebase Authentication"]
        D --> F["Cloud Firestore"]
        D --> G["Cloud Storage"]
        D --> H["Firebase Functions"]
        
        H --> H1["checkRateLimit"]
        H --> H2["enforceWriteRateLimit"]
        H1 -.-> R{{"レート制限\n(60req/min)"}}
        H2 -.-> R
        
        E -.-> I(("Users"))
        F -.-> J(("Medicines"))
        F -.-> K(("Records"))
        F -.-> L(("RateLimits"))
        G -.-> M(("Avatars"))
    end

    subgraph Deployment
        N["GitHub"] --> O["GitHub Actions (CI/CD)"]
        O --> P["Vercel Platform"]
        O --> Q["Firebase Deployment"]
        Q --> E
        Q --> F
        Q --> G
        Q --> H
        P --> A
    end
```

## 開発の流れ
**1.要件定義と仕様書作成**

とりあえず自分がほしい機能を実装したいので、まずはセルフ要件定義をして、概要、アプリの特徴、使用メリット、対象ユーザー（自分自身がペルソナです...）、機能（認証、ユーザー情報管理、お薬管理、服用記録機能、統計情報、在庫管理）、UIなどのイメージを固めました。
それから、その要件定義をもとに以下の項目を作って仕様書のドラフトを作成しました。
```markdown
# MediNote仕様書(Next.js + Firebase, App Router 対応)

## 1. 概要

## 2. 対象ユーザー

## 3. 機能
### 3.1 ユーザー認証
### 3.2 ユーザー情報管理
### 3.3 お薬管理
### 3.4 服用記録機能
### 3.5 統計情報
### 3.6 在庫管理

## 4. UI (ユーザーインターフェース)
### 4.1 ログイン/新規登録画面
### 4.2 メイン画面 (ログイン後)
### 4.3 お薬追加/編集画面
### 4.4 在庫管理画面
### 4.5 記録追加画面
### 4.6 統計情報画面

## 5. 技術スタック
### 5.1 フロントエンド
### 5.2 バックエンド/インフラ
### 5.3 開発環境/ツール

## 6. 特徴的な実装
### 6.1 服用タイミングによる分類
### 6.2 残量と予想切れ日の自動計算
### 6.3 服用記録の統計分析
### 6.4 カレンダー表示

## 7. 今後の予定機能
### 7.1 服用リマインダー機能
### 7.2 医療機関連携（※未定）
### 7.3 高度な統計・分析（※未定）
### 7.4 家族・介護者連携（※未定）

## 8. 技術スタック連携図

## 9. ファイル構成
```

**2.環境構築**
- Next.jsプロジェクトの立ち上げ
- TailwindCSSのセットアップ
- Firebaseプロジェクトの作成と連携

**3.認証機能の実装**
- Firebase Authenticationの設定
- ログイン/サインアップページの作成

**4.薬の管理機能実装**
- 登録・編集・削除機能
- タイミングごとの分類表示

**5.服用記録機能実装**
- 記録登録フォーム
- カレンダー表示

**6.セキュリティルールの設定**
- Firestoreセキュリティルール実装
- Cloud Functionsによるレート制限の設定

**7.デプロイ自動化**
- GitHub Actionsワークフローの構築
- Vercelとの連携設定

## 自分にとって特に苦労したポイントと解決方法
開発中はエラー対応ばかりで辟易としており、何度も諦めかけました…。
そのため、挙げれば本当にキリがないのですが、3つほど以下に書きたいと思います。

### **1. TypeScriptの型定義の理解**
まず、TypeScriptの特徴を理解することに最初のつまづきがありました。
そうなんです、Pythonしか触ったことが無かったので完全に手探りで調べまくってAIに聞きまくってなんとか少しずつ進めていった感じです。
TypeScriptによる静的型付けは私にとって最初の大きな壁でした。特に以下の点が難しかったです。

- 薬のデータモデルを適切に型定義するのに苦労
- オプショナルプロパティとリクワイアドプロパティの区別
- フォーム入力と実際のデータモデルの橋渡し
- API返却値の型と内部データモデルの整合性

#### **解決方法**
具体的なインターフェースを定義し、徐々に理解を深めていきました。
```typescript
// 実際に作成したファイルsrc/types/medicines.ts から抜粋しました
export interface Medicine {
  id: string;
  userId: string;
  name: string;
  dosePerTime: number;
  dosesPerDay?: number; // オプショナルに変更（?をつけて任意項目に）
  unit: string;
  remainingAmount: number;
  usageType?: 'regular' | 'asNeeded' | 'optional';
  timings?: string[];
  startDate?: string | Date; // 複数の型を許容する共用型
  endDate?: string | Date;   
  totalAmount?: number;      
  memo?: string;             
  notes?: string;            // 互換性のために残す
  createdAt: any;
  updatedAt?: any;
}
```

さらに、フォーム用の専用型を定義して、フォーム入力と実際のデータモデルを分離するようにしました。
```typescript
// Form用の型定義
export interface MedicineFormData {
  name: string;
  dosePerTime: number;
  dosesPerDay: number;
  unit: string;
  timings: string[];
  usageType: 'regular' | 'asNeeded' | 'optional';
  startDate: string;
  endDate?: string;
  remainingAmount: number;
  totalAmount?: number;
  memo?: string;
}
```
ひとまずこれで、入力フォームと実際のデータ構造を明確に分離できたのと、型エラーを事前に検出できるようになったことでバグを減らせるようになりました。

### **2. Firestoreセキュリティルールの設計**
構文が独特だし、セキュリティということでミスのできない緊張感もあり何度も入念に確認しながら進めました。その中でも特に苦労したポイントは以下のところです。

- ユーザーごとのデータ分離を確実に行うルール設計
- バリデーションルールの実装（フィールドの型や値の範囲など）
- ルール間の依存関係を理解すること

#### **解決方法**
いっぺんにやると絶対ミスるので段階的にルールを構築して、関数化して再利用可能な設計にしました。
```bash
// firestore.rules から抜粋
service cloud.firestore {
  match /databases/{database}/documents {
    // 基本関数 - 複数のルールで再利用される共通ユーティリティ
    
    // ユーザーがログインしているかチェック
    function isSignedIn() {
      return request.auth != null;
    }
    
    // リソースの所有者かどうかをチェック
    function isOwner(resource) {
      return resource.data.userId == request.auth.uid;
    }
    
    // 作成/更新しようとしているリソースの所有者かどうかをチェック
    function willBeOwner() {
      return request.resource.data.userId == request.auth.uid;
    }

    // 薬情報に対するルール
    match /medicines/{medicineId} {
      // バリデーション関数
      function isValidMedicine() {
        let data = request.resource.data;
        return data.name is string &&
               data.name.size() > 0 &&
               data.name.size() <= 100 &&
               // 他のバリデーション条件...
               (data.remainingAmount == null || data.remainingAmount >= 0);
      }
      
      // アクセス権限設定
      allow read: if isSignedIn() && isOwner(resource);
      allow create: if isSignedIn() && willBeOwner() && isValidMedicine();
      allow update: if isSignedIn() && isOwner(resource) && noImportantFieldChanges() && isValidMedicine();
      allow delete: if isSignedIn() && isOwner(resource);
    }
    
    // 他のコレクションのルール...
  }
}
```

解決策を実施した効果として
- 共通関数を定義して再利用することでルールの一貫性を確保できた
- ルールを関数化して読みやすく、メンテナンスしやすくした
- とにかくコメントをたくさん入れて、各ルールの意図を一目でわかるようにした

### **3. GitHub Actionsによる自動デプロイの設定**
これもはじめての対応だったので、.ymlファイルの作成法を理解するのに時間がかかりました。
設定で特に苦労した点としては以下のとおりです。
- YAMLの構文エラーの頻発
- 適切なトリガー条件の設定
- 複数のFirebaseリソース（Rules, Functions, Indexes）を適切な順序でデプロイする方法
- デプロイエラー時の適切なハンドリング

```yml
# .github/workflows/firebase-deploy.yml から抜粋
name: Deploy Firebase

on:
  push:
    branches: [ main ]
    paths:
      - 'firestore.rules'
      - 'firestore.indexes.json'
      - 'storage.rules'
      - 'functions/**'
      - 'firebase.json'
  
  workflow_dispatch:  # 手動実行オプション

jobs:
  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 10  # タイムアウト設定
    
    steps:
      # Firebase CLIのインストール（最新版を使用）
      - name: Install Firebase CLI
        run: npm install -g firebase-tools@latest

      # デプロイ前にFirebaseルールの検証
      - name: Validate Firebase rules
        run: |
          echo "Validating Firestore rules..."
          firebase deploy --only firestore:rules --preview --force --project ${{ secrets.FIREBASE_PROJECT_ID }} --token "${{ secrets.FIREBASE_TOKEN }}"
        continue-on-error: true  # 検証エラーでもワークフローを続行
      
      # デプロイステップ（省略）...
      
      # デプロイ結果の確認とステータス報告
      - name: Deployment status check
        if: always()
        run: |
          deploy_rules_status="${{ steps.deploy-rules.outcome }}"
          deploy_indexes_status="${{ steps.deploy-indexes.outcome }}"
          deploy_functions_status="${{ steps.deploy-functions.outcome }}"
          
          # 実行されたステップのみ成功を確認（スキップされたステップは成功とみなす）
          if [[ "$deploy_rules_status" == "success" || "$deploy_rules_status" == "skipped" ]] && 
             [[ "$deploy_indexes_status" == "success" || "$deploy_indexes_status" == "skipped" ]] && 
             [[ "$deploy_functions_status" == "success" || "$deploy_functions_status" == "skipped" ]]; then
            echo "✅ Firebase deployment completed successfully"
          else
            echo "❌ Firebase deployment failed"
            exit 1
          fi
```
効果的だった解決策としては：
- 変更されたファイルに基づいて選択的にデプロイするよう最適化をおこなった
- デプロイ前のルール検証ステップを追加
- ステップごとの成功/失敗/スキップを適切に処理
- エラー時の診断情報出力を充実させた

そんなこんなで、なんとか悪戦苦闘しながらも、コード変更を自動的にデプロイできるようになりました。ただ、これに関しては充分に理解して進められているとは言い難いので、見直しが必要だと反省しています。

## セキュリティ対策
上記の特に苦労したところでも少し触れましたが、セキュリティ対策は本当に緊張しました。こればっかりはミスれないし設定漏れがあったら大変なのでめちゃくちゃ見直しをしました。その中でも特に注力して対応したところを挙げておきます。当たり前のことを実装するのにも自分にとっては大仕事なので温かい目で見てやってください。

### 1. Firestoreセキュリティルール
- ユーザーごとのデータ分離
- 所有権に基づくアクセス制御

### 2. Cloud Functionsによるレート制限
- DoS攻撃対策として、ユーザーごとに60リクエスト/分の制限を実装

### 3. Firebase Authentication:
- ログイン履歴の追跡

### 4. CI/CDプロセスのセキュリティ:
- GitHub Secretsによる認証情報の保護
- デプロイ前のセキュリティルール検証
- デプロイと自動化

## デプロイと自動化
環境構築からはじまり、その最後の工程としてようやくデプロイまでたどり着きました。
ここも上記の特に苦労した点で述べたことと重複しそうなので、やったことを簡単に書いて締めくくりたいと思います。
GitHub Actionsワークフローの設定をして、VercelとGitHubリポジトリの連携をしてようやくデプロイが完了しました。実際はここでも何度もエラーが出てデプロイが失敗したり、デプロイはうまくいったもののlocalhost3000では認証機能が動いたのに、本番環境ではそれが動かないとか色々大変でした。


## 実装して学んだこと & まとめ
開発を進める中で学んだことをまとめると、なんか当たり前のことばかりですが、
- 小さな機能から始めることが大切（いきなり大きなことはしない）
- エラーが出ても諦めない（実際は何度も心が折れかけました）
- 技術選定の重要性（とりあえずドキュメントが豊富かつ情報が多くメジャーなものを選択した感じです）
- セキュリティを後回しにしない（特に意識しました）
- ドキュメント作成の必要性（面倒くさがらず説明書として適宜まとめて、未来の自分を助ける）
- 他多数（あり過ぎるので省略させてください...）

は、とても大切だと学びました！

長文になりましたが、ここまで読んでいただきありがとうございます。

初投稿ではありますが、私が個人開発でWEBアプリ「[MediNote](https://medicinenote.vercel.app)」をデプロイするまでの道のりを紹介させていただきました。全技術を完全に理解し網羅するのは難しいですが、充分な知識がなくても、必要に応じて（私の場合は必要に迫られてですが）技術を少しずつ学びながら欲しい機能を実装したアプリを作ることができました。

今までごちゃごちゃ書いてきましたが、エラーにめげない忍耐力が結局は大事なんだと気づきました。