글

Expo를 위한 로컬 우선 HealthKit·Health Connect 브리지

UI에 플랫폼 조건문 없이 재사용 가능한 하나의 React 훅으로 Apple HealthKit과 Google Health Connect를 통합하는 설계 패턴을 소개합니다.

Expo를 위한 로컬 우선 HealthKit·Health Connect 브리지

안녕하세요 여러분! 👋 크로스 플랫폼 React Native 앱에서 건강, 피트니스, 유틸리티 기능을 만들어 본 적이 있다면 그 고통을 아실 겁니다. 하나의 기능 세트를 작성하는 것이 아니라 완전히 다른 두 개의 무거운 네이티브 생태계와 씨름하게 됩니다.

1. 시작

iOS에는 성숙한 Apple HealthKit API가 있습니다. Android에는 기존 Google Fit SDK를 대체한 Google의 통합 API 프레임워크 Health Connect가 있습니다. 새 앱 WaterPaw의 수분 섭취 추적 아키텍처를 만들 때 완전히 모듈화된 해법을 원했습니다.

UI 계층을 어지럽히는 플랫폼 조건 검사(Platform.OS === 'ios')는 원하지 않았습니다. 가용성, 권한, 데이터 가져오기를 한곳에서 처리하는 하나의 추상 커스텀 React 훅을 원했습니다. 다음은 두 플랫폼을 통합하기 위해 사용한 깔끔한 설계 패턴이며, 네이티브 구성은 Expo Config Plugins로 자동 처리합니다.

먼저 프런트엔드가 어떤 네이티브 플랫폼에서 실행되는지 신경 쓰지 않도록 구체적인 청사진(인터페이스)을 정의했습니다.

export type HydrationData = {
  value: number; // in milliliters
  date: string;
};

export interface HealthBridge {
  isAvailable(): Promise<boolean>;
  requestPermissions(): Promise<boolean>;
  getHydrationData(date: Date): Promise<HydrationData[]>;
}

2. 구체적인 브리지

그다음 정확한 구현 세부 사항을 격리된 네이티브 래퍼로 분리했습니다.

  • iOS 래퍼: react-native-health 를 기반으로 초기화 상태를 확인하고 표준 HealthKit 권한 매트릭스를 전달합니다.
  • Android 래퍼: react-native-health-connect 를 기반으로 필수 2단계 실행 흐름(초기화 확인 후 별도의 권한 인텐트 계약)을 사용합니다.

3. 훅: 동적 플랫폼 내보내기

플랫폼에 따라 브리지를 동적으로 내보내면 컴포넌트가 하나의 커스텀 훅만 호출할 수 있다는 점에서 마법이 일어납니다.

import { Platform } from 'react-native';
import { iOSHealthBridge } from './iOSHealthBridge';
import { AndroidHealthBridge } from './AndroidHealthBridge';

const HealthAPI = Platform.OS === 'ios' ? iOSHealthBridge : AndroidHealthBridge;

export function useHydrationSync() {
  const syncData = async () => {
    const available = await HealthAPI.isAvailable();
    if (!available) return;

    const hasPermission = await HealthAPI.requestPermissions();
    if (hasPermission) {
      const records = await HealthAPI.getHydrationData(new Date());
      // Commit directly to a local-first store (e.g. AsyncStorage or Zustand)
    }
  };

  return { syncData };
}

4. Expo의 큰 함정 💡

두 네이티브 패키지 모두 크게 커스터마이징된 네이티브 코드(Objective-C와 Kotlin)를 사용하므로 표준 Expo Go 안에서는 컴파일되지 않습니다. Xcode나 Android Studio를 직접 만지지 않고 깔끔하게 우회하려면 app.json에 config plugins를 선언해야 합니다. Health Connect는 최소 SDK 26을 명시적으로 요구합니다.

{
  "expo": {
    "plugins": [
      ["react-native-health", { "NSHealthShareUsageDescription": "Sync your hydration records." }],
      ["expo-health-connect"],
      [
        "expo-build-properties",
        { "android": { "minSdkVersion": 26 } }
      ]
    ]
  }
}

이렇게 업데이트한 뒤 npx expo prebuild --clean를 실행하면 네이티브 생성이 자동으로 처리됩니다.

왜 이렇게까지 하나요?

미니멀 유틸리티를 만들 때는 성능과 마찰 없는 경험이 전부입니다. 이 아키텍처를 사용하면 UI는 60 FPS로 부드럽게 유지되고 업데이트는 즉각적이며, 로컬 우선이므로 앱이 사용자의 데이터 프라이버시를 온전히 존중합니다. 서드파티 서버도, 필수 회원가입도 없습니다.

이 구조적 로직으로 방금 출시한 WaterPaw의 스마트 알림과 마스코트 성장 시스템을 구현했습니다.