0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発振り返り】「NaN日」表示バグを防ぐ!日付計算の落とし穴と対策

Posted at

はじめに

私が作成している「IT技術学習アプリ」のユーザープロフィール画面を開発していた際、以下のような表示不具合に遭遇しました。

  • 学習期間が「NaN日」と表示される
  • 学習開始日が過去の固定日付「2025/6/8」になってしまう

スクリーンショット 2025-06-29 134844.png
実際のバグ画面:学習期間がNaN日と表示されている

この記事では、なぜこのバグが発生したのかどう修正したのか、そして今後同じ問題を防ぐための教訓をまとめます。

🐛 発生していた問題

問題1:学習期間が「NaN日」表示

ユーザーの学習開始日から現在日までの日数を計算する処理で、「NaN日」と表示されてしまう。

問題2:学習開始日が固定値表示

データベースから正しいデータが取得できない場合に、ハードコードされた過去日付が表示される。

🔍 原因分析

問題の根本原因

データベースからfirstLoginDateが正しく取得できていないことが根本原因でした。

具体的には:

  • userData.stats.firstLoginDateundefined
  • または無効な日付形式のデータが入っている

コードレベルでの問題箇所

問題箇所1:日付計算処理

// 🚨 修正前:危険なコード
const studyDays = userData?.stats ? 
  Math.ceil((new Date() - new Date(userData.stats.firstLoginDate)) / (1000 * 60 * 60 * 24)) : 0;

何が問題?

  • userData.stats.firstLoginDateundefinedの場合
  • new Date(undefined)Invalid Dateになる
  • Invalid Dateを使った計算 → NaNになる

根本的には、userData.stats.firstLoginDateSupabaseの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;
})();

修正のポイント

  1. 存在チェック: データがあるかまず確認
  2. 有効性チェック: isNaN(date.getTime())で日付の妥当性を検証
  3. 異常値チェック: 負の値などを除外
  4. 早期リターン: 問題があった段階で安全な値を返す

修正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()}

修正のポイント

  1. 意味のあるメッセージ: 「未設定」「日付エラー」など状況がわかる表示
  2. 関数化: 処理を分離して可読性向上
  3. デバッグしやすさ: エラー状態が一目でわかる

🔄 修正前後の比較

項目 修正前 修正後
学習期間 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())) { /* 何らかの処理 */ }

まとめ

フロントエンド開発では、データが期待通りに取得できない状況は日常茶飯事です。

今回の事例から学んだ最も重要なポイントは:

  1. 外部データを過信しない
  2. 適切なフォールバック処理を用意する
  3. ユーザーにわかりやすいエラー表示を心がける
  4. 段階的なバリデーションで問題を早期発見する

これらを意識することで、ユーザーにとって使いやすく、開発者にとってもデバッグしやすいアプリケーションを作ることができます。

IT技術学習アプリ

私が作成に取り組んでいる「IT技術学習アプリ」です。React等の技術について学習できるアプリになっています。
現在は開発中のベータ版ですが、周辺技術も学習しながら継続して改善中です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?