실시간 위치 정보를 가지고 오기 위해서 여러가지 도전을 했다.
mauron85/react-native-background-geolocation
검색을 했을 때 가장 먼저 나오는 부분이 react native의 해당 라이브러리이다.
대부분의 글이 해당 라이브러리를 기준으로 작성이 되어 있고 유튜브에 검색했을 때도 똑같은 라이브러리를 사용했었다.
그래서 나도 해당 라이브러리를 먼저 사용해보려고 생각했다.
설치를 위해서 npm을 들어가봤는데,,,,, 165개??
일주일에 165번 밖에 다운받아지지 않는 라이브러리를 사용하는게 맞을지 고민이 생겼다.
그래서 본격적으로 라이브러리를 사용하기 전에 유튜브 영상을 먼저 보고 내가 필요한 기능이 정상적으로 동작하는지
확인하기 위해서 보면서 댓글을 확인했는데, 걱정이 해소되긴 커녕 더 생겼다...
iOS에서 100% 안될 수 있다구요?!
이 글을 본 순간부터 기대감은 사라졌다... 하지만 관련 글이 대부분 해당 라이브러리를 사용하기 때문에 우선 개발을 해보기로 했다.
놀랍게도 패키지를 설치하고 pod install을 하니 프로젝트가 실행되지 않았다....
최신 버전의 react native와는 호환이 되지 않는지 문제가 있었고 깃헙 이슈도 최신 글엔 답변이 없었다.
( 4년전이 마지막 업데이트니 그럴법도... )
해당 라이브러리를 사용하는 것은 포기하고 다른 방법을 찾아보기 시작했다.
react-native-background-actions & react-native-geolocation-service
백그라운드에서 동작하는 라이브러리와 위치 정보를 수집하는 라이브러리를 따로 가지고 오는 방법을 고려했다.
기본적인 동작은 모두 성공했지만 iOS에서 백그라운드 동작에 문제가 있었는데, 기본적으로 15초? 정도만 동작했다.
iOS: This library relies on iOS's UIApplication beginBackgroundTaskWithName method, which won't keep your app in the background forever by itself. However, you can rely on other libraries like react-native-track-player that use audio, geolocalization, etc. to keep your app alive in the background while you excute the JS from this library.
분명 geolocalization을 사용하면 더 유지할 수 있다고 안내하고 있지만 iOS정책 문제인지, 아니면 권한을 열어주지 않는 것이 있는지
백그라운드에서 계속 동작하도록 구현하지 못하고 해결 방법을 찾지 못했다.
Objective-C
애초에 백그라운드에서 동작하는 기능을 React Native를 사용해서 구현하는 방법이 예시가 안드로이드 기준으로 많았으며,
iOS에서는 대부분 없었다.
그래서 React Native로 구현하는 것을 포기하고 Objective-C를 사용해서 구현하는 방법으로 눈을 돌렸다.
공식 문서에서도 Objective-C로 구현한 코드를 React Native에서 사용할 수 있게 제공하는 것 같았다.
// 예시 코드의 일부분....
// RCTCalendarModule.m
#import "RCTCalendarModule.h"
@implementation RCTCalendarModule
// To export a module named RCTCalendarModule
RCT_EXPORT_MODULE();
@end
그렇다면 네이티브 기능으로 구현할 수 있지 않을까 싶어서 이것저것 찾아봤다.
우선 Objective-C 코드를 구성할 수 있도록 파일 환경을 먼저 구성했다.
XCode를 사용해서 프로젝트에 new File로 파일을 만들어준다.
Header 파일과 Objective-C 파일을 만들어준다. 두 개의 파일을 만드는데, Header 파일은 클래스의 인터페이스를 정의하고
Objective-C 파일은 실제 구현을 담당한다.
// RCTLocation.h
#import <React/RCTEventEmitter.h>
#import <React/RCTBridgeModule.h>
#import <CoreLocation/CoreLocation.h>
#import <React/RCTLog.h>
@interface LocationManager : RCTEventEmitter <RCTBridgeModule, CLLocationManagerDelegate>
@property (nonatomic, strong, readonly) CLLocationManager *locationManager;
@end
- #import <React/RCTEventEmitter.h> : React Native에서 Objective-C를 사용해서 앱 내에 있는 네이티브 iOS 기능을 사용할 수 있게 해준다.
- #import <React/RCTBridgeModule.h> : Objective-C 클래스가 React Native 모듈로 작동하게 해준다.
- #import <CoreLocation/CoreLocation.h> : iOS 위치 서비스를 사용하기 위해 필요한 CoreLocation 프레임워크의 헤더 파일이다.
- #import <React/RCTLog.h> : 로그가 필요한 경우 출력 시켜준다.
- @interface LocationManager : RCTEventEmitter <RCTBridgeModule, CLLocationManagerDelegate> : LocationManager 클래스는 RCTEventEmitter를 상속하고, RCTBridgeModule 및 CLLocationManagerDelegate 프로토콜을 준수한다고 선언한다.
- @property (nonatomic, strong, readonly) CLLocationManager *locationManager;: locationManager 속성을 읽기 전용으로 선언한다.
#import "RCTLocation.h"
@interface LocationManager() <CLLocationManagerDelegate>
@property (nonatomic, strong, readwrite) CLLocationManager *locationManager;
@end
@implementation LocationManager
RCT_EXPORT_MODULE(LocationManager);
- (instancetype)init {
self = [super init];
if (self) {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
// iOS 14 이상에서는 정확도 권한을 요청할 수 있습니다.
if (@available(iOS 14.0, *)) {
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
}
// 위치 서비스 권한 요청
[self.locationManager requestWhenInUseAuthorization];
[self.locationManager requestAlwaysAuthorization];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.distanceFilter = kCLDistanceFilterNone;
// 백그라운드에서도 위치 업데이트를 받을 수 있도록 설정 (iOS 9 이상)
if ([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
self.locationManager.allowsBackgroundLocationUpdates = YES;
}
self.locationManager.pausesLocationUpdatesAutomatically = NO;
// showsBackgroundLocationIndicator 설정 (iOS 11 이상)
if (@available(iOS 11.0, *)) {
self.locationManager.showsBackgroundLocationIndicator = YES;
}
}
return self;
}
- (NSArray<NSString *> *)supportedEvents {
return @[@"locationUpdated"];
}
RCT_EXPORT_METHOD(startUpdatingLocation) {
[self.locationManager startUpdatingLocation];
}
RCT_EXPORT_METHOD(stopUpdatingLocation) {
[self.locationManager stopUpdatingLocation];
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
CLLocation *newLocation = [locations lastObject];
RCTLogInfo(@"New location: %@", newLocation);
[self sendEventWithName:@"locationUpdated" body:@{@"latitude": @(newLocation.coordinate.latitude), @"longitude": @(newLocation.coordinate.longitude)}];
}
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
switch (status) {
case kCLAuthorizationStatusAuthorizedAlways:
[self.locationManager startUpdatingLocation];
break;
case kCLAuthorizationStatusAuthorizedWhenInUse:
[self.locationManager requestAlwaysAuthorization];
break;
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted:
// 권한 거부 또는 제한
break;
default:
break;
}
}
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
if (@available(iOS 14.0, *)) {
CLAccuracyAuthorization accuracy = manager.accuracyAuthorization;
if (accuracy == CLAccuracyAuthorizationFullAccuracy) {
RCTLogInfo(@"Full accuracy");
} else {
RCTLogInfo(@"Reduced accuracy");
}
}
CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
[self.locationManager requestAlwaysAuthorization];
}
}
@end
Objective-C 파일은 실제 구현 코드가 있기 때문에 Header 파일보다 좀 더 복잡할 수 있다.
하나씩 살펴보자면 다음과 같다.
RCT_EXPORT_MODULE(LocationManager);
React Native에서 해당 클래스를 모듈로 인식할 수 있도록 설정한다.
이때 공식 문서에도 있지만 "LocationManager"가 아닌 LocationManager 이렇게 작성해야 한다.
- (instancetype)init {
self = [super init];
if (self) {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
// iOS 14 이상에서는 정확도 권한을 요청할 수 있습니다.
if (@available(iOS 14.0, *)) {
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
}
// 위치 서비스 권한 요청
[self.locationManager requestWhenInUseAuthorization];
[self.locationManager requestAlwaysAuthorization];
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.distanceFilter = kCLDistanceFilterNone;
// 백그라운드에서도 위치 업데이트를 받을 수 있도록 설정 (iOS 9 이상)
if ([self.locationManager respondsToSelector:@selector(setAllowsBackgroundLocationUpdates:)]) {
self.locationManager.allowsBackgroundLocationUpdates = YES;
}
self.locationManager.pausesLocationUpdatesAutomatically = NO;
// showsBackgroundLocationIndicator 설정 (iOS 11 이상)
if (@available(iOS 11.0, *)) {
self.locationManager.showsBackgroundLocationIndicator = YES;
}
}
return self;
}
필요한 권한을 요청할 수 있도록 설정하는 초기화 코드이다.
사용자의 위치 변동이 어플이 백그라운드 상태일 때도 동작해야하기 때문에 백그라운드 업데이트도 요청한다.
iOS 11 이상 버전부턴 BackgroundLocationIndicator도 지원하기 때문에 사용자 편의성을 위해서 허용으로 설정해준다.
- (NSArray<NSString *> *)supportedEvents {
return @[@"locationUpdated"];
}
React Native에서 수신할 이벤트의 명칭을 설정한다.
위치가 변경될 때 실행되므로 locationUpdated 이벤트를 지원한다.
RCT_EXPORT_METHOD(startUpdatingLocation) {
[self.locationManager startUpdatingLocation];
}
RCT_EXPORT_METHOD(stopUpdatingLocation) {
[self.locationManager stopUpdatingLocation];
}
위치 업데이트를 시작하는 메서드와 종료하는 메서드이다.
어플을 실행하자마자 정보를 가지고 올 수 있지만 대부분의 경우 위치 탐색 버튼 등을 통해서 정보를 가지고 오기 때문에
편의성을 위해서 해당 메서드를 지원한다.
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray<CLLocation *> *)locations {
CLLocation *newLocation = [locations lastObject];
RCTLogInfo(@"New location: %@", newLocation);
[self sendEventWithName:@"locationUpdated" body:@{@"latitude": @(newLocation.coordinate.latitude), @"longitude": @(newLocation.coordinate.longitude)}];
}
위치가 업데이트 될 때마다 호출된다.
새로운 위치 정보를 전달 받으면 로그를 남기고 locationUpdated 이벤트로 위치 정보를 전달 해준다.
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
switch (status) {
case kCLAuthorizationStatusAuthorizedAlways:
[self.locationManager startUpdatingLocation];
break;
case kCLAuthorizationStatusAuthorizedWhenInUse:
[self.locationManager requestAlwaysAuthorization];
break;
case kCLAuthorizationStatusDenied:
case kCLAuthorizationStatusRestricted:
// 권한 거부 또는 제한
break;
default:
break;
}
}
위치의 권한 상태가 변경될 때 호출된다.
권한 상태에 따라 위치 업데이트를 시작하거나 새로운 권한을 요청한다.
- (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager {
if (@available(iOS 14.0, *)) {
CLAccuracyAuthorization accuracy = manager.accuracyAuthorization;
if (accuracy == CLAccuracyAuthorizationFullAccuracy) {
RCTLogInfo(@"Full accuracy");
} else {
RCTLogInfo(@"Reduced accuracy");
}
}
CLAuthorizationStatus status = [CLLocationManager authorizationStatus];
if (status == kCLAuthorizationStatusAuthorizedWhenInUse) {
[self.locationManager requestAlwaysAuthorization];
}
}
iOS 14 이상에서 위치 정확도 권한이 변경될 때 호출된다.
정확도 권한을 로그로 출력하고 필요한 경우 권한을 추가로 요청한다.
이렇게 작업을 하면 네이티브 코드 작업은 끝나게 된다. 이제 해당 모듈을 활용해서 React Native에서 활용하는 방법으론 다음과 같다.
import React, {useEffect, useState} from 'react';
import {
View,
Text,
NativeModules,
NativeEventEmitter,
SafeAreaView,
ScrollView,
Button,
} from 'react-native';
import MapView, {Circle, Polyline, PROVIDER_GOOGLE} from 'react-native-maps';
const {LocationManager} = NativeModules;
const locationManagerEmitter = new NativeEventEmitter(LocationManager);
const App = () => {
const [location, setLocation] = useState<
{latitude: number; longitude: number}[]
>([]);
const [tracking, setTracking] = useState(false);
useEffect(() => {
const subscription = locationManagerEmitter.addListener(
'locationUpdated',
location => {
console.log('Background location:', location);
setLocation(prev => [...prev, location]);
},
);
return () => {
subscription.remove();
LocationManager.stopUpdatingLocation();
};
}, []);
const startTracking = () => {
LocationManager.startUpdatingLocation();
setTracking(true);
};
const stopTracking = () => {
LocationManager.stopUpdatingLocation();
setTracking(false);
};
return (
<SafeAreaView>
<ScrollView>
<View style={{position: 'relative', height: 400}}>
{location.length !== 0 && (
<MapView
provider={PROVIDER_GOOGLE}
region={{
latitude: location[location.length - 1].latitude,
longitude: location[location.length - 1].longitude,
latitudeDelta: 0.00022,
longitudeDelta: 0.00421,
}}
initialRegion={{
latitude: location[location.length - 1].latitude,
longitude: location[location.length - 1].longitude,
latitudeDelta: 0.00022,
longitudeDelta: 0.00421,
}}
style={{height: 400}}>
<Circle
center={{
latitude: location[location.length - 1].latitude,
longitude: location[location.length - 1].longitude,
}}
radius={10}
strokeColor={'#9aeb67'}
fillColor={'#9aeb67'}
/>
<Polyline
coordinates={location}
strokeColors={['#7F0000']}
strokeWidth={6}
/>
</MapView>
)}
</View>
<Text>
Location tracking {tracking ? 'started' : 'stopped'}...{' '}
{location.length}
</Text>
<Button
title={tracking ? 'Stop Tracking' : 'Start Tracking'}
onPress={tracking ? stopTracking : startTracking}
/>
</ScrollView>
</SafeAreaView>
);
};
export default App;
전체 코드가 필요한 분이 있을 것 같아서 코드를 먼저 올린다. 핵심 코드들을 알아보자면 다음과 같다.
( 진짜 전체 코드 없으면 너무 화나는 경우가 많아서 못참지.... )
const {LocationManager} = NativeModules;
const locationManagerEmitter = new NativeEventEmitter(LocationManager);
Objective-C에서 RCT_EXPORT_MODULE(LocationManager); 선언해둔 모듈을 NativeModules을 사용해서 가지고 와서
이벤트를 등록해준다.
useEffect(() => {
const subscription = locationManagerEmitter.addListener(
'locationUpdated',
location => {
console.log('Background location:', location);
setLocation(prev => [...prev, location]);
},
);
return () => {
subscription.remove();
LocationManager.stopUpdatingLocation();
};
}, []);
useEffect에서 앞서 설정해둔 locationUpdated 이벤트가 발생하면 실행할 작업을 선언해준다.
위치 정보를 넘겨받기 때문에 해당 정보를 상태에 저장시키도록 했다. 그리고 return에서 해당 이벤트를 지우는 작업을 했으며
혹시라도 위치 정보를 수집하는 경우 수집을 멈추도록 하는 이벤트로 호출했다.
const startTracking = () => {
LocationManager.startUpdatingLocation();
setTracking(true);
};
const stopTracking = () => {
LocationManager.stopUpdatingLocation();
setTracking(false);
};
원하는 타이밍에 위치 정보를 수집하고 수집 종료할 수 있도록 만들어준 네이티브 이벤트를 호출한다.
이렇게까지 작업하면 위치 정보를 가지고 올 수 있습니다.
아직 간단한 위치 정보를 지도에 노출하는 기능을 구현했는데, 배터리 소모는 2시간 정도 사용했을 때 3%밖에 사용하지 않았기 때문에
크게 문제되는 부분은 아닌 것 같다.
누군가 위치 정보를 가지고 오는 경우가 있다면 나와같이 머나먼 삽질을 하기보단 이 글이 도움이 되었으면 좋겠다 :) bb
'React Native > 공통' 카테고리의 다른 글
iOS 실시간 위치 정보 가져오기 - 확장판 (1) | 2024.12.21 |
---|---|
react native flex:1 의 오류 (1) | 2024.11.16 |
React Native 파이어베이스 배포하기 (1) | 2024.08.10 |
[React Native] 빌드하기 (0) | 2023.08.28 |
[React Native] 첫 프로젝트 (2) | 2022.07.17 |