Expo에서 현재 위치 조회, 화면 구독, 백그라운드 추적은 어떻게 나누어 처리하나요?
현재 위치 1회 조회, 화면 구독, 백그라운드 추적은 권한과 제약이 다릅니다. 필요한 범위를 정하고 거절·실패를 표시합니다. 화면 종료 뒤 늦게 생성된 구독도 해제해야 합니다.
적용 조건·범위: 학습 목표·선수 지식: 현재 위치·화면 구독·백그라운드 작업을 나누고 비동기 구독 생성과 화면 종료의 경쟁 상태를 처리합니다. 현재 위치 조회에는 async/await, 화면 구독에는 useEffect cleanup을 알아야 합니다. 개발 빌드는 별도의 백그라운드 위치 검증에서 준비합니다.
본문 근거: Expo Location 공식 문서 · Expo TaskManager 공식 문서 · Android 위치 권한 공식 문서
Expo Location 구현 핵심 요약
현재 위치 1회 조회, 화면이 열린 동안의 구독, 백그라운드 추적은 권한과 실행 제약이 서로 다릅니다. 필요한 범위를 먼저 정하고, foreground 권한부터 요청한 뒤 background는 제품 핵심 기능일 때만 별도 설명과 동의 흐름을 만드세요. 백그라운드 위치는 운영체제·사용자 설정·앱 종료 상태에 따라 중단될 수 있으므로 “항상 추적”을 보장하지 않습니다.
버전 기준: 2026-09-13 최신 공식 문서의 API를 대조했습니다. 고정 버전 숫자를 다른 프로젝트로 복사하지 말고 npx expo install로 현재 Expo SDK와 호환되는 의존성을 설치하고 lockfile을 보관하세요.
목차
1. 기능 요구를 API로 바로 매핑합니다

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

코드 위치: 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 빌드, 외부 인증 서버 연동을 수행하지 않았습니다. 본문의 기기 동작은 독자가 확인할 기대 결과입니다.
독립 실습: 버튼에서 현재 위치 한 번 요청
React 함수 컴포넌트·props·state와 TypeScript import를 알고 시작합니다. 첫 Expo 앱 환경 준비의 blank-typescript 프로젝트를 별도 연습 폴더에 사용합니다. 생성된 index.ts·package.json·lockfile은 유지하고 아래 App.tsx만 연결합니다. TaskFlow 코드는 필요 없습니다. npx expo start 후 대상 기기에서 확인합니다.
npx expo install expo-location 후 위의 readCurrentLocation.ts 전체 모듈을 src/location/에 저장하고 아래 App.tsx를 연결합니다. 독립 실습은 foreground 한 번 조회이므로 background 옵션·TaskManager 설정을 추가하지 않습니다.
App.tsx 전체 파일
import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import { readCurrentLocation } from './src/location/readCurrentLocation';
export default function App() {
const [busy, setBusy] = useState(false);
const [status, setStatus] = useState('아직 요청하지 않음');
async function read() {
setBusy(true);
try {
const result = await readCurrentLocation();
if (result.kind === 'success') setStatus('현재 위치 확인: 화면에만 사용');
else if (result.kind === 'denied') setStatus(result.canAskAgain ? '권한 거부' : '시스템 설정에서 권한 확인');
else if (result.kind === 'services-off') setStatus('위치 서비스 꺼짐');
else setStatus('현재 위치 조회 실패');
} finally { setBusy(false); }
}
return (
<View style={{ flex: 1, justifyContent: 'center', padding: 24 }}>
<Button title="현재 위치 한 번 확인" disabled={busy} onPress={read} />
<Text>{status}</Text>
</View>
);
}
기대 결과: 허용·거부·서비스 꺼짐을 각각 확인합니다. 좌표를 서버 전송하거나 콘솔에 기록하지 않으며 앱의 나머지 화면은 계속 사용할 수 있어야 합니다.
자가 과제: 권한 거부 후 버튼을 다시 눌렀을 때 canAskAgain에 맞는 안내인지 확인합니다. background.ts는 영속 큐가 필요한 서비스 연결 부분 코드이며 이 독립 앱에 import하지 않습니다.
선택 실습: 할 일 앱에 적용하기
TaskFlow Mobile은 할 일을 관리하는 모바일 앱입니다. 이 선택 실습에서는 기존 앱에 이번 기능을 이어서 적용합니다.
이번 주제: Expo Location 사용법: 현재 위치와 백그라운드 추적 처리.
누적 프로젝트: TaskFlow Mobile
시작 위치: 아래 기존 환경·파일 구성에서 시작합니다. 이 글의 완료 ZIP은 결과 확인용입니다.
시작 체크포인트: Expo SecureStore 사용법: 토큰 저장과 생체 인증 처리의 누적 파일까지 적용한 상태에서 이어갑니다.
npx expo install expo-location 후 위치 모듈·패널·상세 화면을 갱신합니다. foreground 한 번 조회만 사용하고 background 설정은 넣지 않습니다. 첨부 좌표는 메모리의 작업에만 연결하며 외부 전송하지 않습니다. 거부해도 완료·삭제·뒤로 기능이 유지되는지 확인합니다.
전체 파일·코드 보기
src/location/readCurrentLocation.ts — 이번 회차 전체 파일
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;
}
}
src/components/LocationPanel.tsx — 이번 회차 전체 파일
import { useState } from 'react';
import { Button, Text, View } from 'react-native';
import { readCurrentLocation } from '../location/readCurrentLocation';
import { useTasks } from '../features/TaskProvider';
export default function LocationPanel({ id }: { id: string }) {
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState('위치 미첨부');
const { locate } = useTasks();
async function attach() {
setBusy(true);
try {
const result = await readCurrentLocation();
if (result.kind === 'success') {
const { latitude, longitude } = result.location.coords;
locate(id, `${latitude.toFixed(3)}, ${longitude.toFixed(3)}`);
setMessage('현재 위치 첨부 완료');
} else setMessage(`위치 첨부 안 됨: ${result.kind}`);
} finally { setBusy(false); }
}
return <View>
<Button title="현재 위치 첨부" disabled={busy} onPress={attach} />
<Text>{message}</Text>
</View>;
}
src/app/task/[id].tsx — 이번 회차 전체 파일
import LocationPanel from '../../components/LocationPanel';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Button, Text, View } from 'react-native';
import { useTasks } from '../../features/TaskProvider';
export default function Detail() {
const { id } = useLocalSearchParams<{ id: string }>();
const { tasks, toggle, remove } = useTasks();
const router = useRouter();
const task = tasks.find(item => item.id === id);
if (!task) return <Text>작업을 찾을 수 없습니다.</Text>;
return <View style={{ padding: 24, gap: 12 }}>
<Text>{task.title}</Text>
<Text>{task.location ?? "위치 없음"}</Text>
<LocationPanel id={task.id} />
<Text>{task.completed ? '완료' : '진행 중'}</Text>
<Button title="완료 상태 바꾸기" onPress={() => toggle(task.id)} />
<Button title="작업 삭제" onPress={() => { remove(task.id); router.replace('/'); }} />
</View>;
}
다음 회차로 넘길 상태: 위 추가·수정 파일을 적용한 로컬 연습 폴더입니다. 실제 실행한 플랫폼과 결과를 기록하고 원래 독립 ZIP과 구분해 보관합니다.
아래 원래 과제의 확장 방향은 위 전체 파일이 제공하는 범위와 구분해 읽습니다. 아이콘 액션 확장·실제 세션 인증·background tracking은 추가 구현 과제입니다.
회차별 확장 질문과 확인 기준
독자 질문: 위치 권한을 거부해도 앱은 계속 쓸 수 있게 하려면?
앱에서 따라 하기
- 8회차 작업 상세에 현재 위치 첨부 버튼을 추가합니다.
- foreground 권한을 요청하고 위치 서비스 활성 상태를 확인한 뒤 허용되면 좌표를 작업에 연결합니다.
- 권한 거부 또는 서비스 꺼짐에서는 안내만 보여주고 작업 목록·상세 화면은 유지합니다.
기기·빌드별 확인
예상 확인: 현재 위치 1회 조회는 Android/iOS의 foreground 기능입니다. background tracking은 Expo Go 실습 범위 밖이며 별도 development build와 권한 검증이 필요합니다. 실제 위치·백그라운드 실행은 미확인입니다.
실행 확인 범위: 위의 예상 동작은 직접 따라 할 때 확인할 기준입니다. Android/iOS 기기·에뮬레이터, 웹 브라우저, development build, EAS 원격 빌드와 APK 설치의 실제 결과는 미확인입니다. 환경별로 실행한 뒤 화면, 오류 로그, 빌드 상태를 각각 확인하세요.
오류 점검과 다음 학습
권한보다 먼저 API를 호출하지 않았는지, 구독을 썼다면 화면 종료 시 제거했는지 확인합니다. 위치 정확도는 필요한 수준으로 제한합니다.
글 소개·학습 준비·전체 목차
학습 목표·선수 지식: 현재 위치·화면 구독·백그라운드 작업을 나누고 비동기 구독 생성과 화면 종료의 경쟁 상태를 처리합니다. 현재 위치 조회에는 async/await, 화면 구독에는 useEffect cleanup을 알아야 합니다. 개발 빌드는 별도의 백그라운드 위치 검증에서 준비합니다.
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 13개 · 필수 9개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.