콘텐츠로 이동

@gj-kit/expo-workouts 핵심 개념

Expo에서 HealthKit·Health Connect 운동 데이터를 다룹니다. 저장된 데이터를 지워 버리는 실수는 컴파일 단계에서 막힙니다.

Health Connect의 upsert는 full-state입니다. saveWorkout에서 route를 빠뜨리면 이미 저장돼 있던 route가 함께 지워지고, 더 낮은 version으로 쓰면 insert가 같은 record id를 돌려주며 정상 반환해서 성공한 쓰기와 구분되지 않습니다 — read-back 말고는 알아낼 방법이 없습니다. HealthKit은 읽기 권한 상태를 아예 돌려주지 않기 때문에, scope 하나를 선언하지 않으면 모든 워크아웃의 distanceM이 아무 에러 없이 undefined로만 들어옵니다. 그리고 방금 받은 항목보다 cursor를 먼저 커밋하는 sync 루프는 그 사이의 워크아웃을 영구히 잃습니다.

native 경계는 드러난 채로 둡니다

섹션 제목: “native 경계는 드러난 채로 둡니다”

HealthKit과 Health Connect에는 development build와 플랫폼 권한이 필요합니다. Expo Go나 웹이 native 작업을 할 수 있는 척하지 않고 availability를 노출합니다.

쓰기 결과는 narrowing해야 합니다

섹션 제목: “쓰기 결과는 narrowing해야 합니다”

잠긴 기기, 거부된 권한, 저장된 운동은 서로 다른 결과입니다. native ID는 saved 분기를 증명한 뒤에만 사용할 수 있습니다.

  • route 누락은 컴파일 에러입니다 — Health Connect의 upsert는 full-state여서 route 없이 다시 저장하면 저장돼 있던 route가 지워지기 때문에, WorkoutWrite.route를 readonly RoutePoint[] | 'none' 필수 필드로 두어 누락을 TS2741로 막습니다.
  • 잠긴 기기 분기를 건너뛸 수 없습니다 — SaveResult는 status로 갈라지는 discriminated union이라, 잠긴 기기에서만 나타나는 pendingUnlock 분기를 status === 'saved'로 좁혀내기 전에는 saved.nativeId가 TS2339로 막힙니다.
  • 빠진 scope를 읽기 전에 알려줍니다 — unpopulatedWorkoutMetrics(state)는 워크아웃을 한 건도 읽기 전에, 읽기 scope가 거부됐거나 요청된 적이 없어서 undefined로 들어올 Workout 필드 이름을 distanceM까지 포함해 돌려줍니다.
  • 어디서 import해도 안전합니다 — node/browser 조건은 빌드 산출물에 expo가 전혀 없는 index.unsupported로 라우팅되고 Expo Go에서는 requireOptionalNativeModule이 null을 돌려주므로, import는 던지지 않고 getAvailability()가 unavailable을 resolve합니다.
  • 동기화 갭을 Node에서 재현합니다 — createFakeWorkouts()가 대체하는 것은 WorkoutsApi가 아니라 그 아래 NativeWorkoutsModule seam이라, CursorResetReason 6종과 drain 도중 크래시를 실제 ./core 코드로 vitest에서 돌려 볼 수 있습니다.

이는 native module입니다. Expo Go와 web/Node에서는 native 호출을 의도적으로 지원하지 않습니다. 권한 요청 전에 건강 데이터 권한의 이유를 설명하세요.

다음은 검증된 레시피 두 개입니다.