😗

Web フロントエンドエンジニアが初めてのモバイルアプリ開発! - VocabBoost の開発

2024/08/27に公開


こんにちは!普段は Web のフロントエンドエンジニアとして働いている私が、お盆休み中に初めてモバイルアプリケーション開発に挑戦しました。今回は、その過程で学んだことや苦労した点、そして感じたことをお伝えしたいと思います。

プロジェクト概要:VocabBoost

VocabBoost は、私が英語を喋れるようになりたいと思い開発した経緯があります。学生時代に勉強をほとんどしなかったので、英単語を全然覚えていないしわかりません。まずは英単語を覚えることができるアプリを作りたいと思いました。そんな思いからこのアプリは誕生しています。結果的に語彙力向上を支援するアプリと謳っていますが、本当は単語暗記アプリです。

主な機能は以下の通りです:

  • 新しい単語の追加と管理
  • 画像から単語を抽出
  • 単語リストの表示と学習
  • 単語の発音機能

使用技術

  • React Native
  • Expo
  • OpenAI API(画像解析用)
  • AsyncStorage(データ保存用)

開発過程と学んだこと

1. Expo の選択

最初に直面した課題は、開発環境の選択でした。React Native と Expo について説明します:

React Native:
React Native は、Facebook が開発したオープンソースのモバイルアプリケーションフレームワークです。JavaScript と React を使用して、iOS と Android 向けのネイティブアプリを開発できます。

Expo:
Expo は、React Native の開発をより簡単にするツールセットとフレームワークです。多くの一般的な機能が事前に構成されており、開発者はコーディングに集中できます。

最終的に Expo を選びました。その理由は:

  • セットアップが簡単
  • クロスプラットフォーム開発が容易
  • 豊富な組み込み API とライブラリ
  • 今回の要件では、簡単な構成で手軽に開発できる Expo の方がより適していた

Expo を選んだことで、開発の初期段階でのハードルを大きく下げることができました。(多分)

Expo と言っても、初めてのモバイルアプリ開発なので、結構わからないことが多かったです。

2. 状態管理とデータ永続化

Web アプリケーションでは通常、状態管理に Redux などを使用していましたが、このアプリでは React のuseStateuseEffectフックを使用しました。データの永続化にはAsyncStorageを使用しました。

Redux:
Redux は、アプリケーション全体の状態を一元管理するためのライブラリです。大規模なアプリケーションで特に有用ですが、セットアップが複雑になる場合があります。

useState:
useState は、React のフックの一つで、コンポーネント内で状態を管理するために使用します。シンプルで使いやすく、小規模なアプリケーションや個別のコンポーネントの状態管理に適しています。

useEffect:
useEffect は、副作用を扱うための React フックです。コンポーネントのレンダリング後に実行される処理を定義するのに使用します。データの取得やイベントリスナーの設定などに活用されます。

AsyncStorage:
AsyncStorage は、React Native アプリケーションでデータを永続的に保存するための API です。キーと値のペアでデータを保存し、非同期で操作を行います。ローカルストレージに似ていますが、モバイルアプリ向けに最適化されています。

このアプリでは、Redux の複雑さを避け、useState と useEffect を組み合わせて状態管理を行い、AsyncStorage でデータを永続化することで、シンプルかつ効果的な実装を実現しました。

以下は、AsyncStorage を使用して単語リストを保存および取得する基本的な例です:

app/(tabs)/wordList.tsx
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useState, useEffect } from 'react';

export default function WordListScreen() {
  const [words, setWords] = useState([]);

  useEffect(() => {
    // アプリ起動時に保存されている単語リストを読み込む
    loadWords();
  }, []);

  const loadWords = async () => {
    try {
      const savedWords = await AsyncStorage.getItem('wordList');
      if (savedWords !== null) {
        setWords(JSON.parse(savedWords));
      }
    } catch (error) {
      console.error('単語リストの読み込みに失敗しました:', error);
    }
  };

  const saveWord = async (newWord) => {
    try {
      const updatedWords = [...words, newWord];
      await AsyncStorage.setItem('wordList', JSON.stringify(updatedWords));
      setWords(updatedWords);
    } catch (error) {
      console.error('単語の保存に失敗しました:', error);
    }
  };

  // ... その他のコンポーネントのロジック
}

3. 画像処理と AI 統合

画像から単語を抽出する機能の実装は、思っていたより簡単に実装できて流石 OpenAI!! Expo の ImagePicker と OpenAI API を組み合わせて実現しました。

app/utils/imageAnalysis.ts
import * as ImagePicker from 'expo-image-picker';
import { OpenAI } from 'openai';
import { OPENAI_API_KEY } from '@env';

export const analyzeImage = async (): Promise<string[]> => {
  // 画像の選択
  const result = await ImagePicker.launchImageLibraryAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    allowsEditing: true,
    aspect: [4, 3],
    quality: 1,
  });

  if (result.canceled) {
    return [];
  }

  // OpenAI APIの設定
  const openai = new OpenAI({
    apiKey: OPENAI_API_KEY,
    dangerouslyAllowBrowser: true,
  });

  try {
    // 画像をBase64エンコード
    const imageBase64 = await FileSystem.readAsStringAsync(result.assets[0].uri, {
      encoding: FileSystem.EncodingType.Base64,
    });

    // OpenAI APIを呼び出して画像を解析
    const response = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      messages: [
        {
          role: "user",
          content: [
            { type: "text", text: "この画像に含まれる英単語を抽出してください。" },
            { type: "image_url", image_url: { url: `data:image/jpeg;base64,${imageBase64}` } },
          ],
        },
      ],
    });

    // 応答から単語リストを抽出
    const extractedWords = response.choices[0].message.content
      .split('\n')
      .filter(word => word.trim() !== '')
      .map(word => word.trim());

    return extractedWords;
  } catch (error) {
    console.error('画像解析中にエラーが発生しました:', error);
    return [];
  }
};

基本的に公式のOpenAI 公式ドキュメントを見ながらすると出来ると思います。

※補足
Base64 とは、バイナリデータをテキストに変換するエンコーディング方式の一つです。画像などのバイナリデータを API 経由で送信する際によく使用されます。

Base64 エンコーディングの主な特徴:

  1. 64 種類の印字可能な英数字のみを使用
  2. バイナリデータを安全にテキストとして扱える
  3. データサイズが約 33%増加する

OpenAI API に画像を送信する際、Base64 エンコードが必要な理由:

  1. HTTP 通信の制限: バイナリデータをそのまま送信できない
  2. テキストベースの JSON フォーマットとの互換性
  3. クロスプラットフォームでの一貫性確保

Base64 エンコードを使用することで、画像データを安全かつ確実に API に送信できます。ただし、データサイズが増加するため、大きな画像の場合は注意が必要です。

4. UI デザインとアニメーション

React Native のコンポーネントは、HTML と CSS の知識があれば比較的簡単に理解できますし、普段 React を使っている身からしたらほとんどつまずくことはありません。その点は React Native にしてよかった点です。しかし、つまずくことはないといっても見慣れないタグなどがあるので、違和感はありますね。

また、今回はプロジェクトを作成したときのテンプレートをフル活用して出来る限りシンプルにして開発の工数を減らしました。

感じたこと

  1. React の知識が活かせる: スマホが普及したこの時代、モバイルアプリ開発は出来た方が良いと思う。
  2. デバッグの難しさ: Web ブラウザでのデバッグに慣れていたので、モバイルアプリのデバッグプロセスに適応するのに苦労しました。(面倒で手間)
  3. クロスプラットフォーム開発の魅力: 1 つのコードベースで複数のプラットフォームに対応できる点は、非常に魅力的でした。
  4. コミュニティの重要性: 困ったときに Expo や React Native のコミュニティが非常に助けになりました。(今回の場合は Expo の公式ドキュメントと OpenAI のドキュメントあれば OK)
  5. 依存関係の多さが問題: 最新版が良いと思うが、バージョンが上がるとパッケージが動かなくなることがよくある。(これに時間をかなり割いた)

まとめ

初めてのモバイルアプリ開発は、非常に楽しい経験でした。Web の知識(React)が多く活かせる一方で、モバイル特有の考え方や技術を学ぶ必要がありました。この経験を通じて、フロントエンド開発の視野が大きく広がったと感じています。

これから同じように Web からモバイル開発に挑戦する方々へ:最初は戸惑うこともあるかもしれませんが、根気強く取り組めば必ず道は開けます。ぜひ挑戦してみてください!(上から目線ですいません)

次回は、このアプリのリリースプロセスや、ユーザーフィードバックを基にした改善について書いていきたいと思います。

ぶっちゃけリリース作業が一番めんどいと感じた。
画像作成したりなど、いろいろあるが普段開発だけしているとめんどくさく感じてしまう。
一応、Playストアに公開まで出来たので良ければ下記のリンクからダウンロードしていただけると嬉しいです。
VocabBoost

開発メモ

ビルドコマンドの実行(preview)

eas build --profile preview --platform android

本番用ビルド(AAB)

eas build --profile production --platform android

学習リソース

コミュニティとサポート

Discussion