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 時に叩き込まれずに済みます。