Skip to main content
네이티브
입문네이티브React Native 플랫폼 · 21

React Native 센서 데이터 집계: 재시작과 중복 전달에도 값을 어떻게 유지하는가

Android 걸음 센서의 누적값을 매번 더해 과대 집계하는 실패를 재현하고, 마지막 기준값과 집계값을 함께 저장해 같은 측정값의 재전달·앱 재시작·기기 재부팅을 구분한다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

callback은 센서 값을 받을 때 플랫폼 연결 코드가 호출하는 함수다. 그 함수로 전달된 측정값을 더하기 전에 값이 이번 변화량인지, 특정 원점부터의 누적값인지, 어느 시간 구간의 합계인지 먼저 확인한다.

이 글에서 counter는 특정 기준부터 누적된 계수값, baseline은 다음 변화량을 계산하기 위한 첫 기준값이다. checkpoint는 제품 합계와 마지막으로 반영한 기준을 함께 둔 저장 기록이다. 시간 구간(interval)은 시작·종료 시각 사이의 범위다.

원본 의미       변화량 / 누적 counter / 시간 구간 합계
기준 정보       재부팅 세대 / 구간 시작·종료 / 마지막 처리 지점
영속 기록       제품 합계 + 마지막 기준 정보를 함께 저장
중복 전달       이미 처리한 기준이면 합계 변화 0
새 기준         첫 값은 baseline(첫 기준값)으로만 저장한 뒤 다음 값부터 계산

반복해서 기억할 문장은 이것이다.

센서 숫자를 바로 더하지 말고, 그 숫자의 원점과 마지막 반영 지점을 함께 저장한다.

이 글을 관통하는 상황: 전달된 누적 걸음 수를 매번 더했다

걷기 추적을 켠 뒤 앱은 걸음 수를 0부터 세려 한다. Android 걸음 counter를 흉내 낸 fixture가 같은 기기 재부팅 세대 boot-A에서 100 → 101 → 101 → 103을 차례로 전달한다. 100은 추적을 켠 시점의 기준값이고, 두 번째 101은 같은 값이 다시 전달된 경우다.

이 글의 질문은 하나다.

앱이 다시 시작되고 같은 센서 측정값이 다시 전달돼도, 새로 늘어난 걸음만 어떻게 한 번 반영하는가?

센서 측정값의 뜻부터 확정한다

두 API는 모두 “걸음”을 다루지만 계산식은 다르다.

TYPE_STEP_COUNTER   100 → 101 → 103    변화량은 1, 2
TYPE_STEP_DETECTOR  1 → 1 → 1          각 사건 자체가 한 걸음

누적 counter 100, 101, 101, 103을 모두 더하면 405가 된다. 추적 시작 뒤 실제 변화는 3이므로 이 계산은 측정값 전달 횟수와 누적값을 새 걸음으로 중복 해석한다.

권한 거절과 센서 없음은 합계 0이 아니다. 아직 측정할 수 없는 상태와 실제 0걸음을 같은 숫자로 합치지 않는다.

잘못된 덧셈을 실행해 본다

9편과 20편에서 Async Storage 3.1.1을 사용한 같은 React Native 프로젝트 루트를 이어 쓴다. 실제 센서나 권한 없이 계산을 먼저 검증하는 고정 fixture이므로 App.tsx만 교체한다.

CounterReading은 플랫폼 연결 코드가 JavaScript에 전달할 재부팅 세대와 누적값의 모양이다. AggregateState는 제품 합계와 마지막으로 반영한 기준을 한 문자열에 함께 저장할 모양이다. 20편과 같이 JSON.parse 결과를 unknown으로 두고 속성을 확인해 손상된 저장값을 초기 상태와 구분한다. Reduction은 새 상태와 그 판단 기록을 함께 돌려주는 결과다. operationChainrunSequentially는 읽기→계산→쓰기를 한 번에 하나씩 순서대로 실행해 두 저장 작업이 서로 끼어들지 않게 한다.

import React, { useState } from 'react'
import { Button, SafeAreaView, StyleSheet, Text, View } from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
 
const USE_CHECKPOINT_DELTA = false
const STATE_KEY = '@sensor/step-counter-aggregate'
 
type CounterReading = {
  epoch: string
  total: number
}
 
type AggregateState = {
  steps: number
  lastEpoch: string | null
  lastCounter: number | null
}
 
type Reduction = {
  state: AggregateState
  decision: string
}
 
const INITIAL_STATE: AggregateState = {
  steps: 0,
  lastEpoch: null,
  lastCounter: null,
}
 
const BOOT_A: CounterReading[] = [
  { epoch: 'boot-A', total: 100 },
  { epoch: 'boot-A', total: 101 },
  { epoch: 'boot-A', total: 101 },
  { epoch: 'boot-A', total: 103 },
]
 
const REPLAY_LAST: CounterReading[] = [{ epoch: 'boot-A', total: 103 }]
 
const BOOT_B: CounterReading[] = [
  { epoch: 'boot-B', total: 2 },
  { epoch: 'boot-B', total: 5 },
]
 
function isAggregateState(input: unknown): input is AggregateState {
  if (typeof input !== 'object' || input === null) {
    return false
  }
 
  const steps = 'steps' in input ? input.steps : undefined
  const lastEpoch = 'lastEpoch' in input ? input.lastEpoch : undefined
  const lastCounter = 'lastCounter' in input ? input.lastCounter : undefined
 
  if (typeof steps !== 'number' || !Number.isSafeInteger(steps) || steps < 0) {
    return false
  }
 
  const epochIsValid = typeof lastEpoch === 'string' || lastEpoch === null
  const counterIsValid =
    lastCounter === null ||
    (typeof lastCounter === 'number' &&
      Number.isSafeInteger(lastCounter) &&
      lastCounter >= 0)
 
  return epochIsValid && counterIsValid
}
 
async function loadState(): Promise<AggregateState> {
  const raw = await AsyncStorage.getItem(STATE_KEY)
 
  if (raw === null) {
    return INITIAL_STATE
  }
 
  try {
    const input: unknown = JSON.parse(raw)
    return isAggregateState(input) ? input : INITIAL_STATE
  } catch {
    return INITIAL_STATE
  }
}
 
function addRawReadingValue(
  current: AggregateState,
  reading: CounterReading,
): Reduction {
  return {
    state: {
      steps: current.steps + reading.total,
      lastEpoch: reading.epoch,
      lastCounter: reading.total,
    },
    decision: `${reading.epoch}:${reading.total} 전체를 다시 더함`,
  }
}
 
function addDeltaFromCheckpoint(
  current: AggregateState,
  reading: CounterReading,
): Reduction {
  if (current.lastEpoch !== reading.epoch || current.lastCounter === null) {
    return {
      state: {
        steps: current.steps,
        lastEpoch: reading.epoch,
        lastCounter: reading.total,
      },
      decision: `${reading.epoch}:${reading.total} 새 baseline`,
    }
  }
 
  if (reading.total === current.lastCounter) {
    return {
      state: current,
      decision: `${reading.epoch}:${reading.total} 중복, +0`,
    }
  }
 
  if (reading.total < current.lastCounter) {
    return {
      state: current,
      decision: `${reading.epoch}:${reading.total} 오래되거나 잘못된 값, 무시`,
    }
  }
 
  const delta = reading.total - current.lastCounter
 
  return {
    state: {
      steps: current.steps + delta,
      lastEpoch: reading.epoch,
      lastCounter: reading.total,
    },
    decision: `${reading.epoch}:${reading.total} 변화량 +${delta}`,
  }
}
 
async function persistReading(reading: CounterReading): Promise<Reduction> {
  const current = await loadState()
  const reduction = USE_CHECKPOINT_DELTA
    ? addDeltaFromCheckpoint(current, reading)
    : addRawReadingValue(current, reading)
 
  if (reduction.state !== current) {
    await AsyncStorage.setItem(STATE_KEY, JSON.stringify(reduction.state))
  }
 
  return reduction
}
 
let operationChain: Promise<void> = Promise.resolve()
 
function runSequentially<T>(operation: () => Promise<T>): Promise<T> {
  const result = operationChain.then(operation, operation)
  operationChain = result.then(
    () => undefined,
    () => undefined,
  )
  return result
}
 
export default function App() {
  const [snapshot, setSnapshot] = useState(INITIAL_STATE)
  const [trace, setTrace] = useState('아직 전달하지 않음')
  const [busy, setBusy] = useState(false)
 
  async function run(operation: () => Promise<void>) {
    setBusy(true)
 
    try {
      await operation()
    } catch {
      setTrace('fixture 실행 오류')
    } finally {
      setBusy(false)
    }
  }
 
  async function resetFixture() {
    await run(async () => {
      await runSequentially(async () => {
        await AsyncStorage.removeItem(STATE_KEY)
      })
      setSnapshot(INITIAL_STATE)
      setTrace('fixture 초기화 완료')
    })
  }
 
  async function inspectStoredState() {
    await run(async () => {
      const stored = await runSequentially(loadState)
      setSnapshot(stored)
      setTrace('저장 상태 읽기 완료')
    })
  }
 
  async function playReadings(readings: CounterReading[]) {
    await run(async () => {
      const decisions: string[] = []
      let latest = await runSequentially(loadState)
 
      for (const reading of readings) {
        const reduction = await runSequentially(() => persistReading(reading))
        latest = reduction.state
        decisions.push(reduction.decision)
      }
 
      setSnapshot(latest)
      setTrace(decisions.join(' / '))
    })
  }
 
  return (
    <SafeAreaView style={styles.screen}>
      <Text style={styles.title}>걸음 counter 집계 fixture</Text>
      <Text>mode: {USE_CHECKPOINT_DELTA ? 'checkpoint-delta' : 'raw-sum'}</Text>
      <Text>제품 합계: {snapshot.steps}</Text>
      <Text>마지막 세대: {snapshot.lastEpoch ?? '없음'}</Text>
      <Text>마지막 counter: {snapshot.lastCounter ?? '없음'}</Text>
      <Text>판단: {trace}</Text>
      <View style={styles.actions}>
        <Button title="fixture 초기화" disabled={busy} onPress={() => void resetFixture()} />
        <Button
          title="boot-A 100→101→101→103"
          disabled={busy}
          onPress={() => void playReadings(BOOT_A)}
        />
        <Button
          title="boot-A 103 다시 전달"
          disabled={busy}
          onPress={() => void playReadings(REPLAY_LAST)}
        />
        <Button
          title="boot-B 2→5"
          disabled={busy}
          onPress={() => void playReadings(BOOT_B)}
        />
        <Button
          title="저장 상태 읽기"
          disabled={busy}
          onPress={() => void inspectStoredState()}
        />
      </View>
    </SafeAreaView>
  )
}
 
const styles = StyleSheet.create({
  screen: { flex: 1, padding: 16, gap: 12 },
  title: { fontSize: 22, fontWeight: '700' },
  actions: { gap: 8 },
})

실행 전에 예측한다.

  1. boot-A 100→101→101→103을 누르면 raw-sum 합계는 얼마가 될까?
  2. Reload 뒤 boot-A 103 다시 전달을 누르면 같은 값인지 구분할 수 있을까?
  3. boot-B에서 counter가 2로 작아지면 이전 103과 어떤 기준으로 나눠야 할까?

fixture 초기화boot-A 100→101→101→103 순서로 누른다.

mode: raw-sum
제품 합계: 405
마지막 세대: boot-A
마지막 counter: 103
판단: boot-A:100 전체를 다시 더함 / boot-A:101 전체를 다시 더함 / boot-A:101 전체를 다시 더함 / boot-A:103 전체를 다시 더함

잘못된 구현은 누적값 전체를 새 변화량으로 취급했다. 개발자 메뉴의 Reload로 JavaScript 실행을 새로 만든 뒤 저장 상태 읽기를 누르면 405가 그대로 남는다. 이어 boot-A 103 다시 전달을 누르면 합계는 508이 된다. 마지막 counter를 저장했지만 계산에 쓰지 않았으므로 중복 전달을 막지 못한다.

마지막 기준값과 합계를 함께 저장한다

다음 상수 하나만 바꾼다.

const USE_CHECKPOINT_DELTA = true

fixture 초기화부터 다시 실행한다.

mode: checkpoint-delta
제품 합계: 3
마지막 세대: boot-A
마지막 counter: 103
판단: boot-A:100 새 baseline / boot-A:101 변화량 +1 / boot-A:101 중복, +0 / boot-A:103 변화량 +2

첫 100은 추적 시작 기준으로만 저장한다. 같은 세대에서 101은 101 - 100 = 1, 중복 101은 0, 103은 103 - 101 = 2만 더한다. 제품 합계와 마지막 기준값을 같은 기록으로 갱신하므로 다음 측정값은 항상 마지막으로 저장에 성공한 지점부터 계산한다.

이제 Reload → 저장 상태 읽기boot-A 103 다시 전달 순서로 실행한다.

제품 합계: 3
마지막 세대: boot-A
마지막 counter: 103
판단: boot-A:103 중복, +0

같은 누적 측정값을 다시 처리해도 제품 합계 효과는 그대로다. 단, 이 코드는 한 JavaScript 실행 안에서 저장 작업을 operationChain으로 한 번에 하나씩 처리한다. 실제 native 연결에서 센서 측정값 여러 개가 가까운 시각에 전달될 수 있어도 같은 단일 처리 입구나, 읽기→계산→쓰기를 하나로 묶는 저장 연산으로 보호해야 한다.

기기 재부팅은 새 counter 기준을 만든다

이 fixture에는 재부팅 직후부터 첫 측정값 2가 도착하기 전까지의 신뢰할 기준이 없다. 따라서 새 세대의 첫 값 2는 새 baseline으로만 저장하고 제품 합계에는 넣지 않는다. 이 선택은 그 구간의 2걸음을 의도적으로 집계하지 않는다. 제품이 그 구간까지 보존해야 한다면 플랫폼의 역사 조회, 기기 쪽 영속 기록 또는 서버 원본처럼 재부팅 전후를 잇는 별도 근거가 필요하다.

앞 결과 3에서 boot-B 2→5를 누른다.

제품 합계: 6
마지막 세대: boot-B
마지막 counter: 5
판단: boot-B:2 새 baseline / boot-B:5 변화량 +3

새 세대의 첫 값 2는 기준만 바꾸고, 다음 5에서 3을 더한다. 같은 세대에서 counter가 뒤로 작아진 값은 오래된 전달이나 플랫폼 연결 오류로 기록하고 합계를 바꾸지 않는다. 재부팅 세대를 모르면서 작은 값만 보고 기준을 바꾸면 늦게 도착한 이전 측정값도 새 세대로 오인할 수 있다.

iOS는 재부팅 counter가 아니라 시간 구간을 돌려준다

따라서 Android의 boot-A:103 모양을 iOS에 억지로 만들지 않는다.

Android TYPE_STEP_COUNTER
  구분 기준: 기기 재부팅 세대
  원본 값: 그 세대에서 활성화된 뒤 누적 counter
  병합 방법: 같은 세대의 마지막 counter와 차이
 
iOS CMPedometerData
  구분 기준: 응답 startDate와 endDate가 나타내는 실제 시간 구간
  원본 값: 해당 구간의 numberOfSteps
  병합 방법: 같은 고정 구간은 교체하거나, 확인된 다음 구간만 더함

예를 들어 “오늘 0시부터 현재까지”를 매번 다시 조회한다면 응답의 실제 startDateendDate로 같은 하루 행을 확인해 새 값으로 교체한다. 이전 조회값에 다시 더하지 않는다. 구간별 증가분을 저장한다면 마지막으로 확정한 endDate부터 요청하되, 요청한 시작 시각과 응답 startDate를 반드시 비교한다. 응답이 더 늦게 시작하면 중간 데이터가 없는 부분 응답이므로 연속 합계에 바로 더하지 않고 복구 공백 상태로 남긴다. CMPedometer만으로 최근 7일보다 긴 공백의 연속 복구를 보장할 수 없다. 시간대 변경과 자정 경계도 제품이 날짜를 정의하는 별도 규칙으로 고정한다.

플랫폼 연결 층은 숫자와 함께 의미를 전달한다

Android adapter
  입력: BOOT_COUNT + TYPE_STEP_COUNTER values[0]
  출력: { kind: 'cumulative', epoch, total }
 
iOS adapter
  입력: CMPedometerData startDate, endDate, numberOfSteps
  출력: { kind: 'interval', startAt, endAt, steps }

공통 JavaScript 새 상태 계산 함수(reducer) 하나를 원한다면 kind별 분기를 유지한다. 필드 이름을 모두 value로 바꾸고 같은 덧셈 함수에 넣지 않는다. 권한 거절, 센서 없음, native 오류와 손상된 숫자는 새 상태 계산 함수에 들어오기 전에 상태나 오류로 분류한다.

이 fixture는 측정값이 전달된 뒤의 집계만 증명한다. Android step counter가 세는 범위는 센서가 활성화된 동안이고, iOS CMPedometer는 시스템이 이미 모은 기록을 시간 구간으로 다시 조회할 수도 있다. 앱 프로세스가 없는 동안 무엇이 계속 수집되는지는 센서 측정값을 받도록 등록한 함수(리스너)·플랫폼 저장·background 실행 계약을 별도로 확인하며, 새 상태 계산 함수의 성공을 연속 수집 성공으로 확대하지 않는다.

가상 fixture와 실제 센서 증거를 나눈다

검증 단계Android emulatoriOS Simulator지원 실기기
고정 측정값의 첫 기준·중복·재부팅 세대 계산확인확인회귀 확인
Reload 뒤 Async Storage checkpoint 복원확인확인회귀 확인
권한 거절·기능 없음 UI플랫폼 연결 층 대역으로 확인플랫폼 연결 층 대역으로 확인실제 권한과 지원 여부 확인
실제 걸음 변화·측정값 전달 의미환경이 기능을 실제 제공할 때만 보조 확인실기기 증거를 대신하지 않음Android counter와 iOS 시간 구간을 각각 확인
background·재시작 장기 흐름통제된 재시작통제된 재시작화면 전환·프로세스 종료·기기 재부팅 포함 최종 확인

실기기에서는 원본 누적값·재부팅 세대·구간 시각·계산 변화량을 함께 기록하되 사용자 식별 정보나 상세 동선은 남기지 않는다. Android step counter는 최대 약 10초 지연될 수 있으므로 즉시 화면 숫자가 바뀌는지만 성공 조건으로 삼지 않는다.

pull request에서 달라져야 할 행동

센서 집계 코드를 검토할 때 다음 표를 먼저 채운다.

원본 의미       변화량 / 누적 counter / 시간 구간 합계 중 무엇인가
단위·기준       단위, 원점, 초기화 조건, 시작·종료 시각은 무엇인가
기능 상태       권한 거절, 센서 없음, 아직 측정 전과 실제 0을 구분하는가
영속 checkpoint 제품 합계와 마지막 기준 정보를 어디에 함께 저장하는가
중복·역순       같은 값과 오래된 값이 오면 합계를 바꾸지 않는가
동시 처리       읽기→계산→쓰기를 한 처리 입구나 하나로 묶은 저장 연산으로 보호하는가
플랫폼 연결 층  Android counter와 iOS 시간 구간 의미를 보존하는가
복구            앱 재시작·기기 재부팅·날짜 경계 뒤 기준을 어떻게 다시 잡고, iOS 요청·응답 시작 시각의 공백과 7일 한계를 어떻게 표시하는가
관찰            원본 기준, 계산 변화량, 저장 성공과 오류를 민감 정보 없이 남기는가
실기기          지원 기기에서 권한·실제 걸음·지연·재시작을 확인했는가

검토자는 다음을 확인한다.

  • 센서 측정값을 설명 없이 React state 합계에 바로 더하지 않는가?
  • 제품 합계만 저장하고 마지막 counter나 시간 구간을 메모리에 남기지 않는가?
  • 같은 측정값의 재전달과 늦게 도착한 값을 별도 입력으로 검증했는가?
  • Android 재부팅과 iOS 날짜·시간 구간을 같은 reset 규칙으로 합치지 않았는가?
  • native 연결이 전달한 측정값 여러 개가 영속 상태를 동시에 읽고 덮어쓸 수 없는가?
  • 가상 fixture 통과를 실제 센서·권한·실기기 성공으로 확대하지 않는가?

한 장으로 다시 보기

문제       누적 counter 100, 101, 101, 103을 전달될 때마다 더해 405가 됨
오판       센서 측정값은 언제나 이번 변화량임
관찰       Reload 뒤 103 재전달로 합계가 508까지 다시 증가
원인       제품 합계와 마지막 반영 기준을 함께 저장하지 않음
행동       세대+counter checkpoint 저장, 같은 세대에서는 차이만 반영
재부팅     새 세대 첫 값은 baseline, 다음 값부터 변화량 계산
iOS        응답 startDate·endDate 구간으로 병합하고 7일 초과·부분 응답은 공백으로 표시
경계       fixture는 상태 계산을 증명하고 실제 센서·권한·지연은 실기기 검증

센서 집계의 핵심은 측정값을 많이 받는 기술이 아니다. 원본 숫자가 무엇을 기준으로 만들어졌는지 보존하고, 마지막으로 저장에 성공한 기준에서만 다음 변화를 계산하는 계약이다.

스스로 확인할 질문

  1. TYPE_STEP_COUNTERTYPE_STEP_DETECTOR는 같은 걸음 수를 왜 다르게 더해야 하는가?
  2. 제품 합계와 마지막 counter를 함께 저장해야 같은 측정값의 재전달을 어떻게 무효화할 수 있는가?
  3. 기기 재부팅 뒤 첫 counter를 이전 값과 빼면 왜 안 되는가?
  4. iOS CMPedometerData를 Android 재부팅 counter와 같은 reducer에 바로 넣으면 어떤 정보가 사라지는가?

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01Android TYPE_STEP_COUNTER 측정값이 100, 101, 103으로 전달됐다면 화면 합계에 100+101+103을 더해야 할까?

    정답

    아니다. 이 값은 마지막 재부팅 뒤 센서가 활성화된 동안의 누적값이다. 추적 시작 때 100을 기준값으로 저장했다면 제품 합계에 더할 변화량은 1과 2, 총 3이다.

    왜 그런가

    측정값 전달 횟수와 새로 늘어난 걸음 수는 같은 값이 아니다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02마지막 counter는 메모리에 두고 계산한 걸음 합계만 저장해도 재시작 뒤 중복 전달을 막을 수 있을까?

    정답

    아니다. 제품 합계와 마지막으로 반영한 counter 기준값·기준 세대를 한 기록으로 함께 저장해야 같은 누적값이 다시 와도 변화량 0으로 판단할 수 있다.

    왜 그런가

    합계만 남기면 다음 측정값이 새 변화량인지 이미 반영한 누적값인지 구분할 근거가 사라진다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03Android 기기 재부팅 뒤 step counter가 작은 값으로 돌아오면 이전 counter에서 그대로 빼도 될까?

    정답

    아니다. 재부팅 세대가 바뀌면 첫 값은 새 기준값으로만 저장하고, 같은 세대의 다음 값부터 변화량을 더한다.

    왜 그런가

    서로 다른 원점에서 시작한 누적값끼리 빼면 음수나 과대 집계가 생긴다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04iOS CMPedometer의 numberOfSteps도 Android 재부팅 기준 counter와 같은 필드로 바로 합쳐도 될까?

    정답

    아니다. CMPedometerData의 걸음 수는 응답 startDate와 endDate가 나타내는 실제 시간 구간의 데이터다. 요청 시작보다 응답 시작이 늦으면 누락 구간으로 표시해야 하며, CMPedometer의 최근 7일 보존만으로 그보다 긴 연속 복구를 보장할 수 없다.

    왜 그런가

    제품 단위가 둘 다 걸음 수여도 플랫폼 원본 값의 기준 시점은 다르다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. Native PlatformReact Native · 공식 문서 · 확인 2026-08-09
  2. Advanced Topics on Native Modules DevelopmentReact Native · 공식 문서 · 확인 2026-08-09
  3. React Native repository requirementsMeta Open Source · 소스 코드 · 확인 2026-08-09
  4. Motion sensorsAndroid Developers · 공식 문서 · 확인 2026-08-09
  5. SensorAndroid Developers · 공식 문서 · 확인 2026-08-09
  6. SensorEventAndroid Developers · 공식 문서 · 확인 2026-08-09
  7. Settings.Global BOOT_COUNTAndroid Developers · 공식 문서 · 확인 2026-08-09
  8. Sensors overviewAndroid Developers · 공식 문서 · 확인 2026-08-09
  9. Use Sensor Manager to measure steps from a mobile deviceAndroid Developers · 공식 문서 · 확인 2026-08-09
  10. Extended controls, settings, and helpAndroid Developers · 공식 문서 · 확인 2026-08-09
  11. CMPedometerApple Developer · 공식 문서 · 확인 2026-08-09
  12. CMPedometerDataApple Developer · 공식 문서 · 확인 2026-08-09
  13. CMPedometerData endDateApple Developer · 공식 문서 · 확인 2026-08-09
  14. queryPedometerData(from:to:withHandler:)Apple Developer · 공식 문서 · 확인 2026-08-09
  15. CMPedometer isStepCountingAvailable()Apple Developer · 공식 문서 · 확인 2026-08-09
  16. Running your app on simulated or physical devicesApple Developer · 공식 문서 · 확인 2026-08-09
  17. React Native Async StorageReact Native Async Storage · 공식 문서 · 확인 2026-08-09
  18. Using Async StorageReact Native Async Storage · 공식 문서 · 확인 2026-08-09
  19. Async Storage ChangelogReact Native Async Storage · 공식 문서 · 확인 2026-08-09
이 문서의 마지막까지 읽었습니다.