OpenAIExpo2026/03/31 13:30

How Fig keeps millions eating safely with a five-engineer team and Expo

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

5人チームのFigがExpoで数百万の食の安全を守る方法

Key Points

  • 少人数でExpo採用
  • SVGはexpo-imageで最適化
  • EASでOTA運用

Summary

Fig(Food is Good)は食事制限やアレルギーを持つ数百万のユーザーに安全な食情報を提供するアプリです。5人のエンジニアチームは、保守性と開発速度を両立するためにExpo SDK群とEAS Updatesを中心に採用し、安定したネイティブ体験と迅速な緊急修正配信を実現しています。

Key Points

  • 小規模チームの方針: コミュニティ製パッケージの破壊的変更を避けるため、公式Expoモジュールへ段階的に移行。
  • 画像・SVG: 静的アイコンはexpo-imageでネイティブにレンダリングし、react-native-svgは内部操作やアニメーション時のみ使用(パフォーマンス優先のトレードオフ)。
  • 位置情報戦略: expo-locationで概算と高精度位置を並列取得し、最大3分のキャッシュを活用して素早く地図・検索結果を表示。位置はContextで共有して再利用。
  • ストアレビュー: OSのAPI変更に強いexpo-store-reviewを採用し、レビュー誘導の互換性を確保。
  • OTA(EAS Updates): AppCenter/CodePush廃止後にEASへ移行し、クリティカルなJavaScriptパッチを即時配信。ビルドはBitriseで継続し、OTAはEASで補完する棲み分け。
  • 将来方針: さらなるExpoモジュール導入でReact Nativeのアップグレード負荷と保守コストを低減。

Practical takeaways

  • 小チームならまず公式Expoモジュールを優先して保守リスクを下げる。
  • 静的SVGはexpo-imageへ移し、必要時のみreact-native-svgで内部操作を行う。
  • 位置情報は「概算→高精度」の逐次更新+短時間キャッシュでUXと精度を両立する。
  • EAS Updatesを使ってOTA運用を整備し、クリティカルな修正を迅速に届けられるようにする。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

5人のエンジニアチームとExpoで、Figが何百万人の安全な食事を守る方法

Users • 2026-03-31 • 5分読了

執筆: Jake Lynch(ゲスト著者)、Taylor Harrison(ゲスト著者)

Fig(Food is Good)は、食事制限やアレルギーを持つ人々が実際に食べられる安全な食品を見つけ、重篤な反応を避けられるよう支援します。成分のスキャンや安全な製品の発見、レストランが特定のニーズにどの程度対応しているかの確認まで、Figは複雑な食事ニーズを持つ人々の外食や買い物をより簡単かつ安全にします。何百万人もの人々(Figチームのほとんども含む!)が安全で自信を持った食事選びにFigを頼っています。

昨年、私たちはExpo App Awardのファイナリストになりました。Expoチームはアプリのネイティブ感とキビキビした操作性を評価してくれました。本記事ではその点について詳しく紹介します。

なぜExpoなのか

Figは当初標準的なReact Nativeアプリとして始まりましたが、開発の加速と信頼性向上のために徐々にExpoを採用してきました。エンジニアは5人しかいないため、複雑さに時間を浪費する余裕がありません。コミュニティ製パッケージが壊れてメンテナンスされなくなり痛い目にあった経験から、SDKを段階的に取り込む決断をしました。ライブラリへの信頼が高まり、将来的にさらに統合する計画です。

また、MicrosoftがAppCenterを廃止した昨年、重要なJavaScriptパッチのOTA配信をExpoのEAS Updatesに移行しました。

Expo SDKはFigの成功に不可欠だった

ここでは、小規模チームで高速かつ高性能なアプリを開発するうえで役立ったExpo SDKパッケージのいくつかを紹介します。

expo-image

expo-imageは、Figが画像やSVGを効率的にレンダリングして、コミュニティのメンバーにとって使いやすいアプリ体験を提供するのに役立ちました。標準のReact NativeのImageコンポーネントはSVGをサポートしないため、当初は react-native-svg を使ってアイコンなどのSVG描画を行っていました。SVG文字列をSvgXmlに渡すと、strokeやstroke-width、fillの属性を簡単に設定できます。

しかしSoftware Mansionのブログを読んで、SVGからコンポーネントツリーを作ることにはコストが伴い、静的なアイコンには過剰であると判断しました。アイコンをexpo-imageに移行することでネイティブライブラリにSVGレンダリングを任せ、SVG内部を操作・アニメーションしたい場合だけreact-native-svgを使うようにしています。

expo-imageはSVGの変更能力が限定的(tintColorは非透明ピクセル全体の色を設定する)なため、stroke-widthなどを変えたい場合は追加のSVGをバンドルする必要がありますが、パフォーマンスのトレードオフは十分に価値があると考えています。

react-native-svg(例):

// Imports svg as a string
import thumbsUpIcon from '@assets/icons/thumbs-up.svg';
import { iconDefault } from '@utils/colors.util';
const Icon: FunctionComponent<IconProps> = ({ iconSize, color, rotation, transform, height, width, ...otherProps }) => {
  const parsedIconSize = !height && !width ? getIconSize(iconSize) : undefined;
  const sizeProps: { height?: NumberProp; width?: NumberProp } = {};
  if (parsedIconSize || height) {
    sizeProps.height = parsedIconSize ?? height;
  }
  if (parsedIconSize || width) {
    sizeProps.width = parsedIconSize ?? width;
  }
  // svg string is passed via `xml` in otherProps
  return (
    <SvgXml
      color={color ?? iconDefault}
      transform={rotation ? [{ rotate: `${rotation}deg` }] : transform}
      {...sizeProps}
      {...otherProps}
    />
  );
};

expo-image(例):

import { Image } from 'expo-image';
import styled, { css } from 'styled-components/native';
// Icons are `require`d from the assets folder, like other static assets
export const iconAssets = {
  thumbsUp: require('@assets/icons/thumbs-up.svg'),
  ...
} as const;
export const StyledImage = styled(Image)<{ width?: number; height?: number; rotation?: number }>`
  ${({ width }) => width !== undefined && css`width: ${width}px;`}
  ${({ height }) => height !== undefined && css`height: ${height}px;`}
  ${({ rotation }) => !!rotation && css`transform: rotate(${rotation}deg);`}
`;
const Icon: FunctionComponent<IconProps> = ({ iconSource: iconName, iconSize, height, width, rotation, color, ...otherProps }) => {
  const parsedIconSize = !height && !width ? getIconSize(iconSize) : undefined;
  // Icons are referenced by iconAssets key, native image libraries handle the rendering
  return (
    <StyledImage
      tintColor={color}
      source={typeof iconName === 'object' ? iconName : iconAssets[iconName]}
      width={parsedIconSize ?? width}
      height={parsedIconSize ?? height}
      rotation={rotation}
      contentFit="contain"
      {...otherProps}
    />
  );
};

expo-location

Figはレストラン検索機能で位置情報を扱うためにexpo-locationを使用しています。@react-native-community/geolocationから移行し、より積極的にメンテナンスされているライブラリで位置情報を細かく制御できるようになりました。

私たちは近似座標とより正確な座標を同時に取得し、キャッシュされた位置情報が十分新しければそれを利用します。これにより、地図ビューやレストラン検索を素早く近隣の店舗で埋められるようにしています。位置情報はコンテキストに保存され、アプリ全体(特に新しいレストラン検索機能)で利用します。

Figの位置情報許可を求める画面や、許可後のMapBoxビューがあります。

コード例:

import * as Location from 'expo-location';
// Max age of a cached location in milliseconds
const locationMaximumAge = 3 * 60 * 1000; // 3 minutes
const locationRequiredAccuracy = 11; // meters, just above the LocationAccuracy.High
const approximateLocationRequiredAccuracy = 3001; // meters, just above the LocationAccuracy.Lowest
export const LocationProvider: FunctionComponent<PropsWithChildren<LocationProviderProps>> = ({ children }) => {
  const [coordinates, setCoordinates] = useState<Coordinates>();
  const [approximateCoordinates, setApproximateCoordinates] = useState<Coordinates>();
  // You can alternatively use expo-location methods to request permission
  const { permissionStatus: locationPermissionStatus, triggerRequestPermission: triggerRequestLocationPermission, } = usePermission('location');
  const fetchCoordinates = useCallback(async (setNewCoords: (coords: Coordinates) => void, requiredAccuracy: number, locationAccuracy: Location.LocationAccuracy, maxAge?: number,) => {
    try {
      // Try to use a cached location first
      let location = await Location.getLastKnownPositionAsync({ requiredAccuracy, maxAge, });
      if (!location) {
        // If no cached location, get the current position, which may take some time
        location = await Location.getCurrentPositionAsync({ accuracy: locationAccuracy, });
      }
      const newCoordinates = location?.coords;
      setNewCoords(newCoordinates);
      return newCoordinates;
    } catch (e) {
      logFigError('Error getting location', { error: e, });
    }
  }, [mockLatitude, mockLongitude]);
  const updateLocation = useCallback(async () => {
    const [, newCoordinates] = await Promise.all([
      // Fetch approximate coordinates for faster loading
      fetchCoordinates(setApproximateCoordinates, approximateLocationRequiredAccuracy, Location.LocationAccuracy.Lowest),
      // Fetch more accurate coordinates for better accuracy
      fetchCoordinates(setCoordinates, locationRequiredAccuracy, Location.LocationAccuracy.High, locationMaximumAge),
    ]);
    return newCoordinates;
  }, [fetchCoordinates]);
  useEffectOnce(locationPermissionStatus === 'granted', () => { updateLocation(); });
  return (
    <LocationContext.Provider value={{ coordinates, approximateCoordinates, updateLocation }}>
      {children}
    </LocationContext.Provider>
  );
};

expo-store-review

プラットフォームのアップデートでAPIが非推奨になり、コミュニティパッケージが壊れることがありました。例えばiOS 18でSKStoreReviewControllerが非推奨になり、react-native-in-app-reviewが動作しなくなったことがありました。expo-store-reviewを使うことで、必要なOSバージョンをサポートするレビュー促進のライブラリに素早く移行できました。スクリーンへの組み込みもシンプルで、多くのコミュニティライブラリとは異なり、基盤となるAPIが変わってもExpoのライブラリは継続的にメンテナンスされています。

コード例:

import * as StoreReview from 'expo-store-review';
if (await StoreReview.hasAction()) {
  StoreReview.requestReview();
}

App Center廃止後のExpoのOTAアップデートによる重要パッチの配信

App Centerが2025年3月にCodePushを廃止した後、Figは重要な修正や実験的な体験テストを即座に配信するためにExpo EAS Updatesを採用しました。これによりコード出荷に自信を持てるようになり、本番に到達したバグに迅速に対処できるようになりました。生産ビルドは今もBitriseを使用していますが、必要に応じてExpoのOTA更新システムが重要なアップデートをユーザーに確実に届けてくれる点を高く評価しています。

Expoとともに歩むFigの未来

Figは今後もメンテナンス負荷を下げ、ビルドの複雑さを簡素化し、プラットフォームの拡張に伴う新しいネイティブ機能を活用するため、さらに多くのExpoモジュールを統合していきます。以前はReact Nativeのバージョンアップが恐怖のタスクで、壊れるまで避けていましたが、より多くのExpoパッケージへ移行したことでアップグレードプロセスは容易で信頼できるものになり、サポートされているReact Nativeのバージョンを保つために開発サイクルを無駄にしなくて済むようになりました。