Optional SDK가 Core Infrastructure처럼 행동할 때

Vibe-coded 모바일 앱에서 흔한 launch failure 중 하나는 analytics, attribution, crash reporting이 정말로 critical한 service와 나란히 초기화되는 것입니다. 그런 optional SDK 중 하나가 특정 device나 특정 network condition에서 오작동하면, 첫 screen이 뜨기 전에 앱 전체가 죽을 수 있습니다.

이런 일이 생기는 이유는 AI가 생성한 setup code가 모든 dependency를 똑같이 중요한 것으로 취급하는 경향이 있기 때문입니다. 명시적인 tier system이 없으면 must-have infrastructure와 nice-to-have observer 사이에 아키텍처 차이가 사라집니다. 그러면 모든 것이 실수로 must-have가 됩니다.

세 가지 Dependency Tier

앱의 모든 dependency는 세 가지 범주 중 하나에 들어갑니다. 이 선을 명시적으로 그어 두는 것이, 성장 중에도 살아남는 앱과 production crash 천 번에 무너지는 앱을 가르는 차이입니다.

  • Hard dependency: 이것이 실패하면 앱을 렌더링할 수 없다. config parsing, storage 생성, core service 구성. 앱의 토대다.
  • Soft dependency: 우아하게 실패할 수 있다. auth, network 의존 feature, permission 의존 flow. 앱 shell은 여전히 렌더링되고, 사용자는 crash가 아니라 degraded experience를 본다.
  • Optional dependency: 조용히 실패해도 된다. analytics, crash reporting, A/B testing. 절대 앱을 막아서는 안 된다. 이들은 participant가 아니라 observer다.

대부분의 Vibe-coded 앱이 저지르는 실수는 모든 것을 hard로 취급하는 것입니다. 모든 SDK를 startup 때 초기화합니다. 모든 실패가 startup crash가 됩니다. 하지만 analytics SDK는 storage layer와 같은 지위를 가질 이유가 없습니다.

Hard Dependency Boundary

hard boundary는 config를 검증하고 services를 생성합니다. 실패하면 앱은 retry 버튼이 있는 error screen을 보여 줘야지, 사용자가 App Store에서 별점 1개를 남기게 만드는 하얀 공백을 보여 줘서는 안 됩니다.

// 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를 children에게 전달합니다. children이 services를 생성하지 않습니다. construction은 정확히 한 번만 일어나고, 실패는 production crash report로 새어 나가지 않고 boundary에서 잡힙니다.

Optional Boundary

optional dependency는 완전히 다르게 다뤄야 합니다. 이런 것들은 앱을 절대 크래시시키면 안 됩니다. Mixpanel이 죽어도 앱은 아무 일 없다는 듯 계속 돌아가야 합니다.

// 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 앱의 production crash 상당수는 한 가지 실수에서 나옵니다. soft dependency나 optional dependency를 hard dependency처럼 취급하는 것입니다. Autotomy Expo Starter Pack은 첫날부터 이 선을 분명하게 그어 두기 때문에, 새벽 2시에 이 교훈을 비싸게 배우지 않게 해 줍니다.