🌤️

React Native(Web版)でお天気アプリのプロトタイプを作った体験記

に公開

はじめに

この記事は、React Nativeの知識が0のフロントエンドエンジニアが、2ヶ月でExpoを使ってお天気アプリのプロトタイプを作った体験記です。
なお、今回はReact Nativeの学習を目的として、まずはReact Native Webでブラウザ動作版を作成し、将来的にはネイティブアプリ化を目指すステップとして取り組みました。

記事の目的

生成AIが注目される中、フロントエンド技術として再注目されているReact Native + Expoを実際に触ってみました。
React Nativeは以前「学習コストの高い技術」とされていましたが、Expoの登場により大幅に開発体験が向上しており、以下の点で魅力的でした。

  1. Expoが提供するAPIを通じてネイティブコードを書かずに多くの機能が使える(カメラ・GPS・通知など)
  2. Reactベースなので、Web開発者がスムーズに始められる
  3. Web・モバイル両方に展開できるコストパフォーマンス

今後「アプリとWebサイトのUI統一」のニーズが高まると予想し、まずはプロトタイプ制作に挑戦しました。

対象読者

  • React Native/Expoに興味があるフロントエンドエンジニア
  • モバイル開発初学者
  • 短期間でのプロトタイプ開発を検討している方

この記事で分かること

  • 初心者が2ヶ月で到達できるレベル感(週末の作業で合計約40時間程度)
  • React Native + Expoの導入から公開まで
  • 実際に遭遇した技術的課題と解決策

作ったプロトタイプの概要

こんな感じになっています。

コンセプト

「天気に応じたチークカラーを提案するサービス」をテーマに、以下の機能を実装してみました。

  1. 現在位置の天気情報取得
  2. 天気に応じたアイコン表示
  3. 天気ごとの背景グラデーション変化

技術的制約

なお、このプロトタイプにはいくつかの技術的制約があります。
位置情報APIを使用するためユーザーの位置情報許可が必要で、セキュリティ上HTTPS環境でのみ動作します。
また、今回は学習目的のためWebブラウザでの動作確認に留めており、実機でのテストは今後の課題としています。

開発方針

目標設定

「2ヶ月で動くものを作る」を最優先とし、以下に限定。

  • ブラウザでの動作確認
  • Expo一択(ネイティブ開発は対象外)
  • 単一ファイル構成(App.tsx内で完結)

技術構成

開発環境

  "npm": "10.1.0",
  "node": "v20.8.0",
  "expo": "~51.0.0",
  "react": "18.2.0",
  "react-native": "0.74.5",
  "react-native-web": "^0.19.0"

使用API

OpenMeteo APIを採用
https://open-meteo.com/

  • APIキー不要で利用可能
  • 無料プランあり
  • シンプルなレスポンス形式

当初はOpenWeatherMapを使用していましたが、学習目的なので設定を簡単にするためOpenMeteoに変更しました。

デプロイ環境

Netlifyで静的ホスティング(React Native Webでブラウザ対応)

実装の詳細

位置情報と天気データの取得

import * as Location from 'expo-location';
import { useState, useEffect } from 'react';

const WeatherApp = () => {
  const [weatherData, setWeatherData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const getLocationAndWeather = async () => {
      try {
        // 位置情報の権限チェック
        const { status } = await Location.requestForegroundPermissionsAsync();
        if (status !== 'granted') {
          throw new Error('位置情報の権限が拒否されました');
        }

        // 現在位置を取得
        const location = await Location.getCurrentPositionAsync({
          accuracy: Location.Accuracy.Balanced,
        });

        const { latitude, longitude } = location.coords;

        // 天気データを取得
        const response = await fetch(
          `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,weather_code&timezone=auto`
        );

        if (!response.ok) {
          throw new Error('天気データの取得に失敗しました');
        }

        const data = await response.json();
        setWeatherData({
          temperature: data.current.temperature_2m,
          weatherCode: data.current.weather_code,
          location: { latitude, longitude }
        });

      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    getLocationAndWeather();
  }, []);

  return (
    // JSX...
  );
};

天気コードのマッピング

OpenMeteoAPIの天気コードを独自の表示に変換

const weatherCodeMap: Record<number, string> = {
  0: 'sunny',    // 快晴
  1: 'sunny',    // ほぼ快晴
  2: 'cloudy',   // 部分的に曇り
  3: 'cloudy',   // 曇り
  45: 'cloudy',  // 霧
  48: 'cloudy',  // 霧氷
  51: 'rainy',   // 小雨
  53: 'rainy',   // 雨
  61: 'rainy',   // 小雨
  63: 'rainy',   // 雨
  71: 'snow',    // 小雪
  73: 'snow',    // 雪
};

const mapWeatherCodeToKey = (code: number): keyof typeof gradients => {
  if ([0, 1].includes(code)) return 'sunny';
  if ([2, 3, 45, 48].includes(code)) return 'cloudy';
  if (code >= 50 && code < 70) return 'rainy';
  if (code >= 70 && code < 80) return 'snow';
  return 'default';
};

グラデーション背景の実装

React Native Webでのグラデーション実装は制約があるため、CSSグラデーションで対応

import { ViewStyle } from 'react-native';

// Web専用のスタイル型を定義
interface WebViewStyle extends ViewStyle {
  background?: string;
}

const gradients: Record<string, string[]> = {
  sunny: ['#FFB8D0', '#FEE5E1'],
  cloudy: ['#A1CACF', '#ABA4C6'],
  rainy: ['#033495', '#AEE4FF'],
  snow: ['#AEE4FF', '#6A9CFD'],
  default: ['#FEE3C6', '#FFBEC2'],
};

// コンポーネント内での使用
const [gradient, setGradient] = useState(gradients.default);

const gradientString = `linear-gradient(135deg, ${gradient[0]} 0%, ${gradient[1]} 100%)`;

const gradientStyle: WebViewStyle = {
  position: 'absolute',
  top: 0,
  left: 0,
  right: 0,
  bottom: 0,
  background: gradientString,
  zIndex: -1,
};

return (
  <View style={styles.container}>
    <View style={gradientStyle} />
    {/* その他のコンポーネント */}
  </View>
);

苦戦したポイントと解決策

1. パッケージ依存関係の衝突

【問題】
初期構成でexpo-linear-gradientを使用しようとした際、Reactのバージョンとパッケージの互換性で問題が発生。

npm ERR! peer dep missing: react@^18.0.0, required by expo-linear-gradient@12.3.0

【解決策】
Reactを18.2.0にダウングレード。
環境を一から再構築。
最新のExpo SDKに合わせてパッケージバージョンを統一。

【学んだこと】
依存関係は連鎖するため、一つのパッケージ変更が全体に影響することを実感しました。

2. React Native WebでのCSS制約

【問題】
expo-linear-gradientがWebビルドで正しく表示されない(色が分離して表示される)。

【解決策】
ネイティブのlinear-gradient関数を直接使用

// ❌ 動作しない
import { LinearGradient } from 'expo-linear-gradient';

// ✅ 動作する
const gradientString = `linear-gradient(135deg, ${color1} 0%, ${color2} 100%)`;

【学んだこと】
React Native WebはWebとモバイルの橋渡しだが、すべての機能が同等に動作するわけではない。

3. Netlifyデプロイでのビルド設定

【問題】
デフォルトのexpo exportコマンドではweb-buildディレクトリが生成されない。

【解決策】

  • package.json
{
  "scripts": {
    "build:web": "expo export --platform web --output-dir web-build"
  }
}
  • webpack.config.js(新規作成)
const createExpoWebpackConfigAsync = require('@expo/webpack-config');

module.exports = async function (env, argv) {
  const config = await createExpoWebpackConfigAsync(env, argv);
  return config;
};
  • Netlify設定
Build command: npm run build:web
Publish directory: web-build

Web開発者から見たReact Nativeの違い

良かった点

  • 既存知識の活用: Reactの知識がそのまま使える
  • 開発速度: Web開発と同様のスピード感
  • デバッグ: Chrome DevToolsが使用可能

戸惑った点

  • DOM概念がない: <div> → <View>, <span> → <Text>
  • CSS-in-JS必須: 外部CSSファイルが使用できない
  • レイアウトシステム: FlexboxがデフォルトだがWebと微妙に違う

DOM(Web開発の感覚)

<div className="container">
  <span>Hello World</span>
</div>

React Nativeの書き方

<View style={styles.container}>
  <Text>Hello World</Text>
</View>

今後の改善点

機能面

  • リバースジオコーディング(緯度経度 → 地名変換)
  • パーソナルカラー診断機能の追加
  • 週間天気予報の表示
  • オフライン対応

技術面

  • TypeScript型定義の強化
  • コンポーネント分割
  • 状態管理ライブラリの導入(Redux Toolkit など)
  • テストコードの追加
  • 実機での動作確認

参考サイト

https://reactnative.dev/
https://qiita.com/akaishi_linc/items/61743f39a5e53be5b46d
https://dev.classmethod.jp/articles/react-native-for-web-with-typescript-while-referring-to-the-official-introduction/

まとめ

2ヶ月で学んだこと

①技術的な学び

  • React Native WebはWebとモバイルの中間的存在
  • パッケージ依存関係の管理の重要性
  • プロトタイピングにおける要件定義の大切さ

②開発体験

  • Expoにより、モバイル開発のハードルは確実に下がった
  • Web開発者でも短期間でモバイルライクなアプリが作れる
  • ただし、本格的なモバイル最適化には追加学習が必要

初学者でも2ヶ月で動くものは作れますが、プロダクションレベルにするにはさらなる学習と最適化が必要だと実感しました。それでも、アイデアを素早く形にできるExpo + React Nativeの組み合わせは、プロトタイピングに向いているんだろうなと思います!

この記事が、React Native/Expoに興味を持つフロントエンドエンジニアの参考になれば幸いです!

Discussion