Expo Location 사용법: 현재 위치와 백그라운드 추적 처리

2026.04.22·수정 2026.09.13·약 20분·작성: 해비·블로그 소개

학습 목표·선수 지식: 현재 위치·화면 구독·백그라운드 작업을 나누고 비동기 구독 생성과 화면 종료의 경쟁 상태를 처리합니다. async/await와 useEffect cleanup, 개발 빌드가 선수 지식입니다.

Expo Location 구현 핵심 요약

현재 위치 1회 조회, 화면이 열린 동안의 구독, 백그라운드 추적은 권한과 실행 제약이 서로 다릅니다. 필요한 범위를 먼저 정하고, foreground 권한부터 요청한 뒤 background는 제품 핵심 기능일 때만 별도 설명과 동의 흐름을 만드세요. 백그라운드 위치는 운영체제·사용자 설정·앱 종료 상태에 따라 중단될 수 있으므로 “항상 추적”을 보장하지 않습니다.

버전 기준: 2026-09-13 최신 공식 문서의 API를 대조했습니다. 고정 버전 숫자를 다른 프로젝트로 복사하지 말고 npx expo install로 현재 Expo SDK와 호환되는 의존성을 설치하고 lockfile을 보관하세요.

1. 기능 요구를 API로 바로 매핑합니다

Expo Location 현재 위치, foreground 구독, background 추적과 geofencing 선택 흐름
요구 API 핵심 제약
최근 좌표를 빠르게 표시 getLastKnownPositionAsync 오래됐거나 부정확할 수 있어 maxAge·accuracy 검사 필요
현재 좌표 1회 조회 getCurrentPositionAsync 센서·환경에 따라 늦거나 실패할 수 있음
화면이 열린 동안 이동 반영 watchPositionAsync foreground에서만 update, 구독 해제 필수
앱이 뒤에 있을 때 경로 기록 startLocationUpdatesAsync TaskManager·background 권한·native config·개발 빌드 필요
영역 진입·이탈 감지 startGeofencingAsync 플랫폼별 region 수와 종료 상태 동작 차이

정확한 최신 API와 플랫폼 표는 Expo Location 공식 문서을 기준으로 확인합니다. 지도에 점 하나를 찍는 기능에 background 권한을 요청하지 말고, 사용자가 기대하는 핵심 기능에 필요한 최소 권한만 사용하세요.

2. 설치와 app config는 빌드 전에 결정합니다

npx expo install expo-location expo-task-manager

설정 예시: app.json의 해당 객체에 병합합니다. 기존 설정은 보존합니다.

{
  "expo": {
    "plugins": [
      [
        "expo-location",
        {
          "locationWhenInUsePermission": "현재 위치를 표시하기 위해 위치를 사용합니다.",
          "locationAlwaysAndWhenInUsePermission": "이동 기록 기능을 켠 동안 위치를 사용합니다.",
          "isIosBackgroundLocationEnabled": true,
          "isAndroidBackgroundLocationEnabled": true
        }
      ]
    ]
  }
}

config plugin 변경은 native 설정을 바꾸므로 새 binary를 만들어야 합니다. foreground 전용 앱이라면 background 옵션을 켜지 않습니다. iOS 설명 문구는 기능 목적을 구체적으로 쓰고, Android background·foreground service 권한은 앱 심사와 개인정보 고지까지 함께 검토합니다.

3. 현재 위치와 foreground 구독

캐시 좌표를 쓸 수 있는 조건을 먼저 정합니다

src/location/readCurrentLocation.ts 전체 모듈입니다. 버튼 이벤트에서 호출해 반환 kind별로 UI를 표시하세요. canAskAgain이 false면 반복 요청 대신 시스템 설정 안내를 제공합니다. 현재 위치 조회는 오래 걸릴 수 있으므로 요청 중 표시를 제공하고 중복 클릭을 막습니다.

import * as Location from 'expo-location';

export async function readCurrentLocation() {
  try {
    const permission = await Location.requestForegroundPermissionsAsync();
    if (!permission.granted) {
      return { kind: 'denied', canAskAgain: permission.canAskAgain } as const;
    }
    if (!(await Location.hasServicesEnabledAsync())) {
      return { kind: 'services-off' } as const;
    }
    const cached = await Location.getLastKnownPositionAsync({
      maxAge: 30_000,
      requiredAccuracy: 500,
    });
    const location =
      cached ??
      (await Location.getCurrentPositionAsync({
        accuracy: Location.Accuracy.Balanced,
      }));
    return { kind: 'success', location, cached: cached !== null } as const;
  } catch {
    return { kind: 'unavailable' } as const;
  }
}

날씨·매장 검색은 수백 미터 오차와 수십 초 캐시가 허용될 수 있지만, 안전 기능이나 정밀 경로 기록은 같은 기준을 쓸 수 없습니다. accuracy와 timestamp를 UI에 전달하고, 권한 거부·위치 서비스 꺼짐·시간 초과 상태를 각각 안내합니다.

watch 구독은 화면 생명주기에 맞춰 해제합니다

src/location/useForegroundLocation.ts 전체 훅입니다. 권한을 요청하는 버튼 흐름이 끝난 뒤 컴포넌트를 마운트하는 예제입니다. watchPositionAsync가 완료되기 전에 화면을 나가도 늦게 만들어진 구독을 즉시 remove해야 합니다. 탭이 비활성화되어도 마운트가 유지되는 Router 화면이라면 focus 상태에 따라 이 훅을 쓰는 자식 컴포넌트를 언마운트하거나 useFocusEffect로 같은 정리 흐름을 옮기세요.

import { useEffect, useState } from 'react';
import * as Location from 'expo-location';

export function useForegroundLocation() {
  const [status, setStatus] = useState('대기');
  const [location, setLocation] = useState<Location.LocationObject | null>(null);
  useEffect(() => {
    let disposed = false;
    let subscription: Location.LocationSubscription | undefined;
    async function start() {
      try {
        const permission = await Location.getForegroundPermissionsAsync();
        if (disposed) return;
        if (!permission.granted) {
          setStatus('위치 권한을 먼저 허용하세요.');
          return;
        }
        const next = await Location.watchPositionAsync(
          { accuracy: Location.Accuracy.Balanced, distanceInterval: 20 },
          (value) => {
            if (!disposed) setLocation(value);
          },
          () => {
            if (!disposed) setStatus('위치를 갱신하지 못했습니다.');
          },
        );
        if (disposed) next.remove();
        else {
          subscription = next;
          setStatus('위치 구독 중');
        }
      } catch {
        if (!disposed) setStatus('위치 구독을 시작하지 못했습니다.');
      }
    }
    void start();
    return () => {
      disposed = true;
      subscription?.remove();
    };
  }, []);
  return { status, location };
}

정확도를 무조건 최고로 두면 배터리와 발열 비용이 커집니다. 제품 요구에 맞춰 accuracy, time interval, distance interval을 결정하고 실기기에서 전력 사용과 update 간격을 측정합니다.

4. background task는 top-level에서 정의합니다

다음은 src/location/background.ts의 서비스 연결용 부분 예제입니다. reportBackgroundErrorenqueueEncryptedLocations는 이 글이 제공하지 않는 오류 수집·암호화 영속 큐 구현입니다. 실제 모듈에서 import해야 하며 이름만 붙여 넣으면 실행되지 않습니다. src/app/_layout.tsx에서 import '../location/background';를 추가해 앱 시작 시 정의 모듈이 로드되게 하세요. 실행 파일 구조에 맞춰 경로를 조정합니다.

Expo TaskManager background 위치 작업, 권한, native config와 서버 전송 검증 흐름

코드 위치: src/location/background.ts. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.

import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';

const TASK_NAME = 'route-location';

// 컴포넌트 내부가 아니라 앱이 로드할 수 있는 모듈의 top-level에 정의
type LocationTaskData = { locations: Location.LocationObject[] };

TaskManager.defineTask<LocationTaskData>(TASK_NAME, async ({ data, error }) => {
  if (error) {
    await reportBackgroundError(error.message);
    throw new Error(error.message);
  }
  if (!data?.locations.length) return;

  // headless 실행이 끝나기 전에 영속 queue 기록을 완료하고 실패를 전달합니다.
  await enqueueEncryptedLocations(data.locations);
});

export async function startTracking() {
  const foreground = await Location.requestForegroundPermissionsAsync();
  if (foreground.status !== 'granted') return false;
  const background = await Location.requestBackgroundPermissionsAsync();
  if (background.status !== 'granted') return false;

  await Location.startLocationUpdatesAsync(TASK_NAME, {
    accuracy: Location.Accuracy.Balanced,
    distanceInterval: 50,
    foregroundService: {
      notificationTitle: '이동 기록 중',
      notificationBody: '위치 추적을 중지하려면 앱 설정을 여세요.',
    },
  });
  return true;
}

Expo 공식 문서는 background task를 top-level scope에서 정의하도록 요구하고 executor 반환형을 Promise로 규정합니다. 위 예제처럼 callback을 async로 만들고 영속 queue 저장을 await해야 TaskManager가 완료·실패를 관찰할 수 있습니다. React 화면 상태에 의존하거나 Promise를 반환하지 않는 fire-and-forget 작업은 headless 실행 중 bundle이 종료될 때 유실될 수 있습니다. iOS background location은 Expo Go에서 지원되지 않으므로 development build와 실제 기기로 검증합니다. Android도 Expo Go의 foreground/background service 제약이 있으므로 Expo TaskManager 공식 문서Expo Location 공식 문서의 현재 지원표를 함께 봅니다.

앱이 강제 종료됐을 때 동작은 플랫폼과 제조사에 따라 다릅니다. Expo 문서상 Android는 종료된 앱이 위치·geofence 이벤트로 자동 재시작되지 않으며, iOS geofence는 시스템이 앱을 다시 시작할 수 있는 차이가 있습니다. 이를 “24시간 누락 없는 추적”으로 광고하지 말고 누락·재개·중복 전송을 견디는 queue와 idempotency를 설계합니다.

src/location/background.ts에 추가할 중지 함수입니다. 사용자에게 보이는 “이동 기록 중지” 버튼에서 호출하고 성공한 뒤 UI를 중지 상태로 갱신하세요. 중지 요청 실패를 숨기지 말고 재시도 안내를 제공합니다.

코드 위치: src/location/background.ts. 앞에서 전체 파일로 명시한 경우를 제외하면 해당 구조를 설명하는 부분 예제입니다. 서로 다른 예제의 export default를 한 파일에 합치지 않습니다.

export async function stopTracking() {
  if (await Location.hasStartedLocationUpdatesAsync(TASK_NAME)) {
    await Location.stopLocationUpdatesAsync(TASK_NAME);
  }
}

위 중지 코드는 같은 파일의 Location import와 TASK_NAME을 사용합니다. 백그라운드 권한은 사용자가 기능을 명시적으로 켤 때 요청합니다. foreground 성공을 background 성공으로 표시하지 마세요. OS가 작업을 중단할 수 있으므로 예상 구독 간격은 보장 주기가 아닙니다.

5. Android와 iOS 권한 흐름을 분리합니다

Android

iOS

6. 개인정보·배터리·실기기 테스트

7. 명시적 결론과 다음 행동

결론: 위치 기능은 API 호출보다 권한·실행 상태·개인정보·배터리 설계가 먼저입니다. 현재 위치만 필요하면 foreground API에서 끝내고, background는 제품 핵심 가치와 플랫폼 정책을 설명할 수 있을 때만 추가하세요. 다음 행동은 요구사항을 1회 조회·foreground 구독·background 추적 중 하나로 고르고, 권한 거부를 포함한 실기기 테스트 표를 먼저 만드는 것입니다.

공식 근거

내부 학습 경로

문서·검증 기준 (2026-09-13): 해당 기능의 Expo 공식 문서와 코드 구조를 대조했습니다. 설치 버전은 프로젝트 Expo SDK에 맞는 npx expo install 결과와 lockfile을 기준으로 합니다. 이번 개정에서는 실제 Android·iOS 기기 실행, 원격 EAS 빌드, 외부 인증 서버 연동을 수행하지 않았습니다. 본문의 기기 동작은 독자가 확인할 기대 결과입니다.

이 글이 도움이 되었나요?

조회 중

Expo 학습 순서

필수 9개 · 전체 13개

읽음 기록 관리

전체 과정 목차 (13개)
  1. 필수 학습 · Expo 첫 앱 만들기: 설치·프로젝트 생성부터 첫 화면 실행까지
  2. 필수 학습 · Expo Safe Area 사용법: 화면 여백을 안전하게 잡는 방법
  3. 필수 학습 · Expo Status Bar 사용법: Safe Area와 화면별 설정
  4. 필수 학습 · Expo vector icons 사용법: 탭바와 커스텀 아이콘 적용하기
  5. 필수 학습 · Expo KeyboardAvoidingView 사용법: 키보드가 화면을 가릴 때 해결하기
  6. 필수 학습 · Expo Router 사용법: app 폴더와 Stack Tabs 구조 잡기
  7. 필수 학습 · Expo WebBrowser 사용법: 외부 링크와 로그인 복귀 처리
  8. 필수 학습 · Expo SecureStore 사용법: 토큰 저장과 생체 인증 처리
  9. 선택 참고 · Expo Location 사용법: 현재 위치와 백그라운드 추적 처리 현재 글
  10. 선택 참고 · Expo React Native Web 사용법: 앱을 웹으로 확장하기
  11. 선택 참고 · Expo Metro unable to resolve module 오류 해결: 경로와 캐시
  12. 필수 학습 · Expo EAS Build 시작하기: Android APK 빌드부터 설치 확인까지
  13. 선택 참고 · Expo EAS Build 오류 해결: 로컬은 되는데 원격 빌드만 실패할 때

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기