optional SDK が core infrastructure の顔をするとき

Vibe-coded な mobile app でよくある launch failure は、analytics、attribution、crash reporting が、本当に critical な service と同列で初期化されてしまうことです。その optional SDK のひとつが特定の device や network condition で不調になると、最初の screen が出る前に app 全体が落ちます。

こうなるのは、AI が生成する setup code がすべての dependency を同じ重要度で扱いがちだからです。明示的な tier system がなければ、must-have な infrastructure と nice-to-have な observer の architectural な区別は存在しません。気づけば全部が must-have になっています。

依存関係には 3 つの tier がある

app の依存関係はすべて、次の 3 つの bucket のどれかに入ります。この線を明示的に引くことが、成長に耐える app と、本番 crash を積み重ねて死んでいく app の分かれ目です。

  • Hard dependencies: これが失敗すると app は render できない。config parsing、storage の作成、core service の構築。土台そのものです。
  • Soft dependencies: graceful に失敗できるもの。auth、network 依存の feature、permission 依存の flow。app shell は render され、ユーザーには crash ではなく degraded experience が見える。
  • Optional dependencies: silent に失敗してよいもの。analytics、crash reporting、A/B testing。app を block してはいけない。これらは participant ではなく observer です。

多くの Vibe-coded app がやってしまうミスは、全部を hard 扱いすることです。すべての SDK を startup 時に初期化する。どんな failure でも startup crash になる。でも analytics SDK は storage layer と同じ地位であるべきではありません。

Hard Dependency Boundary

hard boundary は config を validate し、service を構築する場所です。ここで失敗したら、app は retry button 付きの error screen を出すべきです。App Store で one-star を付けられるような真っ白な画面ではなく。

// src/context/hard-dependency-boundary.tsx
// This is the gatekeeper. If it fails, nothing else renders.

import * as SplashScreen from 'expo-splash-screen'

void SplashScreen.preventAutoHideAsync()

export function HardDependencyBoundary({
  children,
}: {
  children: (services: Services) => ReactNode
}) {
  const { services, ready, error, retry } =
    useHardDependencies()

  useEffect(() => {
    if (ready || error) void SplashScreen.hideAsync()
  }, [ready, error])

  if (error) {
    return (
      <ErrorScreen
        title='Startup failed'
        message={error.message}
        onRetry={retry}
      />
    )
  }

  if (!ready || !services) return null

  return <>{children(services)}</>
}

render prop pattern に注目してください。boundary は初期化済みの services を子に渡す。子は service を構築しない。受け取るだけです。構築は一度だけ行われ、失敗は boundary で捕捉される。production の crash report にそのまま漏れることはありません。

Optional Boundary

optional dependencies は対照的です。これらは 絶対に app を crash させてはいけない。Mixpanel が落ちても、app 側は平然と動き続けるべきです。

// src/context/optional-dependency-boundary.tsx
// If analytics fails, we log a warning and keep going.

export function OptionalDependencyBoundary({
  children,
}: PropsWithChildren) {
  const { analytics, crashReporting } = useServices()

  useEffect(() => {
    try {
      analytics.track('app_started')
    } catch (error) {
      if (__DEV__) console.warn('Analytics failed:', error)
    }

    try {
      crashReporting.setContext('app', { started: true })
    } catch (error) {
      if (__DEV__) console.warn('Crash reporting failed:', error)
    }
  }, [analytics, crashReporting])

  return <>{children}</>
}

姿勢の違いははっきりしています。hard boundary は失敗したら error screen を出す。optional boundary は失敗したら warning を出して先へ進む。どちらも明示的で、どちらも相手のふりをしません。

Vibe-coded app の本番 crash の多くは、たった一つのミスから起きます。soft か optional であるべき dependency を hard 扱いしてしまうことです。Autotomy Expo Starter Pack はこの線引きを初日から入れてあるので、その教訓を午前 2 時に叩き込まれずに済みます。