はじめに
私が作成している「IT技術学習アプリ」のユーザープロフィール画面を開発していた際、以下のような表示不具合に遭遇しました。
- 学習期間が「NaN日」と表示される
- 学習開始日が過去の固定日付「2025/6/8」になってしまう
この記事では、なぜこのバグが発生したのか、どう修正したのか、そして今後同じ問題を防ぐための教訓をまとめます。
🐛 発生していた問題
問題1:学習期間が「NaN日」表示
ユーザーの学習開始日から現在日までの日数を計算する処理で、「NaN日」と表示されてしまう。
問題2:学習開始日が固定値表示
データベースから正しいデータが取得できない場合に、ハードコードされた過去日付が表示される。
🔍 原因分析
問題の根本原因
データベースからfirstLoginDate
が正しく取得できていないことが根本原因でした。
具体的には:
-
userData.stats.firstLoginDate
がundefined
- または無効な日付形式のデータが入っている
コードレベルでの問題箇所
問題箇所1:日付計算処理
// 🚨 修正前:危険なコード
const studyDays = userData?.stats ?
Math.ceil((new Date() - new Date(userData.stats.firstLoginDate)) / (1000 * 60 * 60 * 24)) : 0;
何が問題?
-
userData.stats.firstLoginDate
がundefined
の場合 -
new Date(undefined)
→Invalid Date
になる -
Invalid Date
を使った計算 →NaN
になる
根本的には、userData.stats.firstLoginDate
がSupabaseのuser_statsテーブルのfirst_login_dateカラムからのデータを期待していたが、Supabaseのuser_statsテーブル自体が定義できておらず、結果的に表示がNaN
になってしまっていました。
問題箇所2:フォールバック値
// 🚨 修正前:ハードコードされたフォールバック
📅 学習開始日: {userData?.stats?.firstLoginDate ?
new Date(userData.stats.firstLoginDate).toLocaleDateString('ja-JP') :
'2025/6/8' // ← この固定値が表示されていた
}
何が問題?
- データが取得できない場合、過去の固定日付が表示される
- ユーザーにとって混乱を招く表示
✅ 修正方法
修正1:安全な日付計算処理
// ✨ 修正後:段階的にチェック
const studyDays = (() => {
// Step1: データの存在チェック
if (!userData?.stats?.firstLoginDate) return 0;
// Step2: 日付オブジェクトの作成
const firstDate = new Date(userData.stats.firstLoginDate);
// Step3: 日付の有効性チェック
if (isNaN(firstDate.getTime())) return 0;
// Step4: 日数計算
const today = new Date();
const diffTime = today - firstDate;
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// Step5: 異常値チェック
return diffDays > 0 ? diffDays : 0;
})();
修正のポイント
- 存在チェック: データがあるかまず確認
-
有効性チェック:
isNaN(date.getTime())
で日付の妥当性を検証 - 異常値チェック: 負の値などを除外
- 早期リターン: 問題があった段階で安全な値を返す
修正2:わかりやすいフォールバック表示
// ✨ 修正後:専用関数で安全に処理
const getFormattedStartDate = () => {
if (!userData?.stats?.firstLoginDate) {
return '未設定'; // ← わかりやすいメッセージ
}
const date = new Date(userData.stats.firstLoginDate);
if (isNaN(date.getTime())) {
return '日付エラー'; // ← デバッグにも役立つ
}
return date.toLocaleDateString('ja-JP');
};
// 表示部分
📅 学習開始日: {getFormattedStartDate()}
修正のポイント
- 意味のあるメッセージ: 「未設定」「日付エラー」など状況がわかる表示
- 関数化: 処理を分離して可読性向上
- デバッグしやすさ: エラー状態が一目でわかる
🔄 修正前後の比較
項目 | 修正前 | 修正後 |
---|---|---|
学習期間 | NaN日 |
0日 (データなし時) |
学習開始日 |
2025/6/8 (固定値) |
未設定 (データなし時) |
エラー時 | 異常表示 |
日付エラー (明確) |
可読性 | 処理が複雑 | 段階的チェックで明確 |
補足説明
Step 1: オプショナルチェーニング
特徴:複雑な存在チェックを簡略化して書ける
userData?.stats?.firstLoginDate
各段階での判定:
// 段階1: userDataの存在チェック
userData?.stats?.firstLoginDate
// ↑ userDataがnull/undefinedなら → ここで停止 → undefined
// 段階2: statsの存在チェック(userDataが存在する場合のみ)
userData?.stats?.firstLoginDate
// ↑ statsがnull/undefinedなら → ここで停止 → undefined
// 段階3: firstLoginDateの取得(userData、statsともに存在する場合のみ)
userData?.stats?.firstLoginDate
// ↑ firstLoginDateの値を取得
Step 2: 三項演算子による条件分岐
特徴:if-else文を1行で書ける演算子
[Step1の結果] ? [truthy時の処理] : [falsy時の処理]
実際のケース別動作
ケース1: userDataが存在しない
const
userData = null;
// 実行過程
userData?.stats?.firstLoginDate
// ↓ userDataがnullなので1段階目で停止
undefined
// ↓ 三項演算子の判定
undefined ? new Date(userData.stats.firstLoginDate).toLocaleDateString('ja-JP') : '2025/6/8'
// ↓ undefinedはfalsy
'2025/6/8' // 結果
ケース2: userDataはあるがstatsがない
const userData = { profile: { nickname: 'テスト' } };
// 実行過程
userData?.stats?.firstLoginDate
// ↓ userDataは存在 → 1段階目クリア
{ profile: { nickname: 'テスト' } }?.stats?.firstLoginDate
// ↓ statsがundefinedなので2段階目で停止
undefined
// ↓ 三項演算子の判定
undefined ? new Date(userData.stats.firstLoginDate).toLocaleDateString('ja-JP') : '2025/6/8'
// ↓ undefinedはfalsy
'2025/6/8' // 結果
ケース3: userData、statsはあるがfirstLoginDateがない
const userData = { stats: { totalQuestions: 10 } };
// 実行過程
userData?.stats?.firstLoginDate
// ↓ userDataは存在 → 1段階目クリア
{ stats: { totalQuestions: 10 } }?.stats?.firstLoginDate
// ↓ statsは存在 → 2段階目クリア
{ totalQuestions: 10 }?.firstLoginDate
// ↓ firstLoginDateプロパティが存在しない
undefined
// ↓ 三項演算子の判定
undefined ? new Date(userData.stats.firstLoginDate).toLocaleDateString('ja-JP') : '2025/6/8'
// ↓ undefinedはfalsy
'2025/6/8' // 結果
ケース4: 正常なデータが存在
const userData = { stats: { firstLoginDate: '2025-06-15T00:00:00Z' } };
// 実行過程
userData?.stats?.firstLoginDate
// ↓ 全段階クリア
'2025-06-15T00:00:00Z'
// ↓ 三項演算子の判定
'2025-06-15T00:00:00Z' ? new Date(userData.stats.firstLoginDate).toLocaleDateString('ja-JP') : '2025/6/8'
// ↓ 文字列はtruthy → 日付処理実行
new Date('2025-06-15T00:00:00Z').toLocaleDateString('ja-JP')
// ↓
'2025/6/15' // 結果
📚 学んだ教訓
1. 防御的プログラミングの重要性
外部データ(API、データベース)は常に「期待通りでない可能性」を考慮する。
楽観的なコード(問題あり)
// APIから返ってくると期待しているデータ
const userData = fetchUserFromAPI();
const birthDate = userData.profile.personalInfo.birthDate;
const age = calculateAge(birthDate);
// 問題:
// - APIがエラーを返した場合 → userData が null
// - ユーザーがプロフィール未設定 → profile が undefined
// - 個人情報を非公開 → personalInfo が null
// - 生年月日未入力 → birthDate が undefined
// どれか一つでも欠けていると、アプリがクラッシュ
防御的なコード(安全)
const userData = fetchUserFromAPI();
const birthDate = userData?.profile?.personalInfo?.birthDate || null;
if (!birthDate || isInvalidDate(birthDate)) {
// データがない、または無効な場合の処理
const age = null;
console.log('生年月日が設定されていません');
} else {
// データが正常な場合のみ処理実行
const age = calculateAge(birthDate);
}
2. エラーメッセージをユーザーフレンドリーに
-
NaN
→未設定
-
Invalid Date
→日付エラー
-
undefined
→データなし
3. 段階的なバリデーション
一度にすべてをチェックせず、段階的に検証することで:
- デバッグしやすい
- 処理の流れが明確
- 適切なエラーハンドリングが可能
4. テストケースの重要性
正常系だけでなく、異常系のテストも必須:
// テストすべきケース
- userData が null/undefined
- firstLoginDate が null/undefined
- firstLoginDate が無効な文字列
- firstLoginDate が未来日付
- firstLoginDate が異常に古い日付
🚀 今後更に堅牢性を高めるための改善案
データベース側の対策
-- NOT NULL制約とデフォルト値の設定
ALTER TABLE user_stats
ADD COLUMN first_login_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
バリデーション関数の共通化
1. UserProfile.js
const getFormattedStartDate = () => {
if (!userData?.stats?.firstLoginDate) {
return '未設定'; // ← 文字列を返す
}
const date = new Date(userData.stats.firstLoginDate);
if (isNaN(date.getTime())) {
return '日付エラー'; // ← 文字列を返す
}
return date.toLocaleDateString('ja-JP'); // ← フォーマット済み文字列
};
2. UserService.js
const createUser = (userData) => {
if (!userData.birthDate) {
throw new Error('生年月日が必要です'); // ← 例外を投げる
}
const date = new Date(userData.birthDate);
if (isNaN(date.getTime())) {
throw new Error('無効な日付です'); // ← 例外を投げる
}
// 処理続行...
};
3. LessonProgress.js
const validateLessonDate = (dateString) => {
if (!dateString) return false; // ← boolean を返す
const date = new Date(dateString);
return !isNaN(date.getTime()); // ← boolean を返す
};
1~3に共通している日付有効性チェックは切り出して共通関数化する
// すべてに共通する「日付の有効性チェック」ロジック
if (!dateString) { /* 何らかの処理 */ }
const date = new Date(dateString);
if (isNaN(date.getTime())) { /* 何らかの処理 */ }
まとめ
フロントエンド開発では、データが期待通りに取得できない状況は日常茶飯事です。
今回の事例から学んだ最も重要なポイントは:
- 外部データを過信しない
- 適切なフォールバック処理を用意する
- ユーザーにわかりやすいエラー表示を心がける
- 段階的なバリデーションで問題を早期発見する
これらを意識することで、ユーザーにとって使いやすく、開発者にとってもデバッグしやすいアプリケーションを作ることができます。
IT技術学習アプリ
私が作成に取り組んでいる「IT技術学習アプリ」です。React等の技術について学習できるアプリになっています。
現在は開発中のベータ版ですが、周辺技術も学習しながら継続して改善中です。