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?

React + TypeScript + Node.js で作る動的英語熟語学習アプリ

Last updated at Posted at 2025-07-26

React + TypeScript + Node.js で作る動的英語熟語学習アプリ

はじめに

このアプリ・記事はCursorで作成しました。

英語の熟語を楽しく学べるWebアプリケーションを作成しました。このアプリは、React + TypeScriptでフロントエンドを構築し、Node.jsでAPIサーバーを実装しています。特徴的なのは、外部APIとローカルデータを組み合わせた動的な熟語取得機能です。

アプリの特徴

🎯 主要機能

  • 動的熟語取得: ボタン一つで新しい10個の熟語を取得
  • 難易度フィルター: 初級・中級・上級で問題を分類
  • インタラクティブ学習: 答えを隠して考えてから確認
  • レスポンシブデザイン: モバイルでも使いやすいUI

🔄 データ取得の仕組み

  1. 外部API: api.dictionaryapi.devから実際の熟語フレーズを取得
  2. ローカル生成: 著作権フリーの熟語データから動的生成
  3. フォールバック: API失敗時はローカルデータで補完

技術スタック

フロントエンド

  • React 18 - UIライブラリ
  • TypeScript - 型安全性
  • Vite - ビルドツール
  • Tailwind CSS - スタイリング

バックエンド

  • Node.js - サーバーサイド
  • HTTP/HTTPS - 外部API通信
  • CORS - クロスオリジン対応

開発・デプロイ

  • Concurrently - 並行実行
  • Render - ホスティング
  • Git - バージョン管理

プロジェクト構造

study-english-idiom/
├── src/
│   ├── components/
│   │   ├── IdiomCard.tsx          # 熟語表示カード
│   │   ├── DifficultyFilter.tsx   # 難易度フィルター
│   │   └── DynamicIdiomLoader.tsx # 動的データ読み込み
│   ├── services/
│   │   └── api.ts                 # API通信
│   ├── types/
│   │   └── idiom.ts               # 型定義
│   └── data/
│       ├── idioms.ts              # サンプルデータ
│       └── scraped-idioms.ts      # スクレイピングデータ
├── scripts/
│   ├── api-server.js              # APIサーバー
│   └── dynamic-scraper.js         # 動的データ生成
└── package.json

実装のポイント

1. 動的データ取得の実装

// src/services/api.ts
const API_BASE_URL = process.env.NODE_ENV === 'production'
  ? 'https://study-english-idiom-api.onrender.com/api'
  : 'http://localhost:3001/api';

export const fetchRandomIdioms = async (count: number = 10, difficulty?: string) => {
  const params = new URLSearchParams();
  params.append('count', count.toString());
  if (difficulty) params.append('difficulty', difficulty);
  
  const timestamp = new Date().getTime();
  params.append('t', timestamp.toString());
  
  const response = await fetch(`${API_BASE_URL}/idioms?${params}`, {
    cache: 'no-cache'
  });
  
  if (!response.ok) {
    throw new Error(`API Error: ${response.statusText}`);
  }
  
  return response.json();
};

2. 外部APIとの連携

// scripts/dynamic-scraper.js
async function fetchIdiomsFromAPI() {
  const idiomPhrases = [
    'break the ice', 'hit the nail on the head', 'cost an arm and a leg',
    'pull yourself together', 'get over it', 'take it easy'
    // ... 60以上の熟語フレーズ
  ];
  
  const shuffledPhrases = [...idiomPhrases].sort(() => Math.random() - 0.5);
  const selectedPhrases = shuffledPhrases.slice(0, 10);
  
  const allIdioms = [];
  
  for (const phrase of selectedPhrases) {
    const firstWord = phrase.split(' ')[0];
    const apiUrl = `https://api.dictionaryapi.dev/api/v2/entries/en/${firstWord}`;
    
    try {
      const response = await makeRequest(apiUrl);
      const data = JSON.parse(response);
      
      if (data && data[0]) {
        allIdioms.push({
          id: `api-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
          english: phrase,
          japanese: `${phrase}の意味`,
          explanation: data[0].meanings?.[0]?.definitions?.[0]?.definition || '説明なし',
          example: data[0].meanings?.[0]?.definitions?.[0]?.example || '例文なし',
          difficulty: 'medium'
        });
      }
    } catch (error) {
      console.log(`API取得エラー: ${phrase}`);
    }
    
    // レート制限を避けるため待機
    await new Promise(resolve => setTimeout(resolve, 300));
  }
  
  return allIdioms;
}

3. キャッシュ制御とランダム化

// 常に10個の熟語を返すように制御
const shuffledIdioms = dynamicIdioms
  .map((idiom, index) => ({
    id: `scraped-${Date.now()}-${Math.random().toString(36).substr(2, 9)}-${index}`,
    ...idiom
  }))
  .sort(() => Math.random() - 0.5)
  .slice(0, 10); // 常に10個に制限

console.log(`取得数: ${shuffledIdioms.length}個`);
return shuffledIdioms;

4. CORS設定

// scripts/api-server.js
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Cache-Control, Pragma, Expires',
  'Content-Type': 'application/json',
  'Cache-Control': 'no-cache, no-store, must-revalidate',
  'Pragma': 'no-cache',
  'Expires': '0'
};

開発環境のセットアップ

1. 依存関係のインストール

npm install

2. 開発サーバーの起動

# フロントエンド + APIサーバーを並行実行
npm run dev:full

# または個別に起動
npm run dev    # フロントエンド (http://localhost:3000)
npm run api    # APIサーバー (http://localhost:3001)

3. ビルド

npm run build

デプロイ

Renderでのデプロイ

render.yamlを使用してフロントエンドとAPIサーバーの両方をデプロイ:

services:
  - type: web
    name: study-english-idiom
    env: static
    buildCommand: npm run build
    staticPublishPath: ./dist
    
  - type: web
    name: study-english-idiom-api
    env: node
    buildCommand: npm install
    startCommand: npm run start
    envVars:
      - key: NODE_ENV
        value: production

学んだこと・課題

✅ 成功した点

  • 動的データ取得: 外部APIとローカルデータの組み合わせで豊富なコンテンツを提供
  • キャッシュ制御: ブラウザキャッシュを適切に制御して常に新しいデータを取得
  • エラーハンドリング: API失敗時のフォールバック機能で安定性を確保
  • 型安全性: TypeScriptでランタイムエラーを最小限に抑制

🔧 技術的課題と解決策

  1. CORSエラー: APIサーバー側のヘッダー設定で解決
  2. 取得数の不整合: フォールバック機能と最終的な制限で統一
  3. レート制限: API呼び出し間の待機時間を設定
  4. 著作権問題: 外部APIとローカルデータの組み合わせで回避

🚀 今後の改善点

  • ユーザー認証: 学習進捗の保存機能
  • スコア機能: 正答率の記録とランキング
  • 音声機能: 発音の確認機能
  • オフライン対応: Service Workerでのキャッシュ機能

まとめ

このプロジェクトを通じて、以下の技術を実践的に学ぶことができました:

  • React + TypeScriptでの型安全な開発
  • Node.jsでのAPIサーバー構築
  • 外部API連携とエラーハンドリング
  • キャッシュ制御とパフォーマンス最適化
  • CORSとセキュリティ考慮
  • Renderでのフルスタックデプロイ

特に、外部APIとローカルデータを組み合わせた動的コンテンツ生成の仕組みは、実際のWebアプリケーションでよく使われるパターンで、実践的なスキルを身につけることができました。

参考リンク


技術スタック: React, TypeScript, Node.js, Vite, Tailwind CSS, Render
開発期間: 約4時間
コード行数: 約1,500行

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?