React Native(Expo)를 이용한 GPS 위치추적 - 러닝 트래킹 앱 만들기

사요·2022년 2월 6일
4

2022 GDSC KR WINTER HACK

목록 보기
3/3

개요

우리팀원분중 한명이 만든 플로깅화면의 프로토타입이다. 조깅하면서 쓰레기를 줍는 활동을 장려하는 어플이기 때문에 마치 여타 다른 러닝어플 (나이키런,런데이) 처럼 사용자의 위치를 실시간으로 추적하면서 거리,칼로리,페이스 등을 화면에 같이 보여줄 수 있어야했다. 이 화면을 구현하기 위해서는

  1. 사용자의 실시간 위치추적 -> 화면상에 경로 보이기
  2. 이동한 거리,칼로리,페이스계산
  3. 타이머 및 러닝 시작 및 중지
  4. 쓰레기통 위치 마커로 표시하기

총 4가지 기능이 필요했다. GPS Tracking과 관련하여 수없이 많은 자료들을 찾아봤는데,, 죄다 3~4년전 쯤 된 오래된 자료이거나 React Native CLI 에서만 이용할 수 있는 라이브러리를 사용하는 코드였기 때문에 Expo로 개발을 결정한 우리 팀은 정말 그야말로 난항을 겪었다. 그래도 정말 황금같은 도움이 되어준 글이 바로 아래 링크에 있다. 아마 이 글이 없었다면 이 기능을 제대로 구현하지 못했을 것 같다. 러닝 트래킹 어플을 어떻게하면 쉽게 구현할 수 있을지 로직에 관해 정말 간결하고 명료하게 작성된 글이었다.

https://medium.com/quick-code/react-native-location-tracking-14ab2c9e2db8

이 글의 튜토리얼을 차근차근 따라하면 될 것 같은데 ? 라고 묻는다면.. 세가지 어려움이 있었다.우선 첫번째로 이 글은 무려 3~4년전에 작성되었고, navigator.geolocation.watchPosition 등 구현된 코드의 상당수가 더이상 지원하지 않는deprecated 된 라이브러리/모듈을 사용하고 있었어서 해당 부분을 expo에 존재하는 라이브러리로 대체하는 작업을 해야했다. 그리고 두번째는 클래스 컴포넌트로 작성되었다는 점이다. 내가 React 를 처음 배우기 시작할 때는 클래스형 컴포넌트가 아닌 리액트 측에서 권장하는 함수형 컴포넌트위주로 공부를 했었어서 코드를 해독하기가 난감했다. 프론트엔드 분야는 백엔드 분야에 비해 보수적이지 않다는 말이 실감나는 순간이었다. 마지막 세번째는 동기/비동기에 대한 이해가 부족하다는 점이었다. 그리고 이 과정들 속에서 시간이 엄청 지연되고 수많은 오류를 겪었다. 그래도 덕분에 클래스형 컴포넌트 공부도 제대로 하게 된 느낌 ☺ 여러분들은 저처럼 삽질하지 마시고 아래 튜토리얼을 따라 천천히 구현해보신다면 빠른 시간내에 러닝 트래킹 어플을 구현할 수 있을거에요.

react-native-maps

npm i react-native-maps

: 우선 사용자의 위치를 지도상에 표시하기 위해서는 지도 가 필요하다. 위 명령어로 해당 라이브러리를 설치해준다. 그리고 러닝 트래킹 어플 구현에 필요한 state 변수들을 초기화해준다. LATITUDE,LONGITUDE 에는 지도 컴포넌트가 렌더링 될때 맨 처음 표시될 지역의 위도,경도를 입력해주면된다. 그리고 'react-native-maps'에서 AnimatedRegion를 import 해준다. 위치가 업데이트 될 때마다 마커를 animate 해주는 기능을 지닌다.

위치 추적을 구현하기 위해 필요한 기본적인 state 변수들을 세팅해준다.

constructor(props) {
  super(props);
  this.state = {
    latitude: LATITUDE,
    longitude: LONGITUDE,
    routeCoordinates: [],
    distanceTravelled: 0,
    prevLatLng: {},
    coordinate: new AnimatedRegion({
     latitude: LATITUDE,
     longitude: LONGITUDE
    })
  };
}

만일 위치추적뿐 아니라 kcal계산, 쓰레기통 위치 마커 찍기 기능까지 구현하고 싶다면 아래 예시대로 초기화해준다.


    this.state = {
      mode : 'wait', 
      kcal : 0,
      latitude: LATITUDE,
      longitude: LONGITUDE,
      routeCoordinates: [],
      distanceTravelled: 0, // 이동한 거리 
      prevLatLng: {},
      coordinate: new AnimatedRegion({
       latitude: LATITUDE,
       longitude: LONGITUDE,
       latitudeDelta: LATITUDE_DELTA,
       longitudeDelta: LONGITUDE_DELTA,
      }),
      trashLocations: [{ // 쓰레기통 위치
        latitude: 37.78825,
        longitude: -122.4324,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      },{
        latitude: 37.80825,
        longitude: -122.4324,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      }],
    };

위치변화감지하기

가장 난항을 겪었던 부분이다. 내가 봤던 설명 글에서는
CLI에서만 지원되는 Google maps geolocation API 의 비동기 함수인 watchPosition() 를 이용하는데, react native expo에서는 본 라이브러리가 지원되지 않았다.
따라서 해당 기능을 대체할 수 있는 라이브러리를 찾는데 시간이 꽤 많이 소요되었다.
그래서 결국 공식문서를 뒤져 찾아낸 .. expo-location의 Location.watchPositionAsync() 함수를 대신하여 이용해주었다.
componentDidMount() 는 useEffect() 처럼 render()함수가 호출되기 전에 호출되는 함수이다.

import * as Location from 'expo-location';
https://docs.expo.dev/versions/latest/sdk/location/#locationwatchpositionasyncoptions-callback

componentDidMount() {

    // 실시간으로 위치 변화 감지
    Location.watchPositionAsync({ accuracy: Location.Accuracy.Balanced, timeInterval: 300, distanceInterval: 1 },
      position => {
        const { coordinate, routeCoordinates, distanceTravelled,kcal } =   this.state;
        const { latitude, longitude } = position.coords;
        
        //새롭게 이동된 좌표
        const newCoordinate = {
          latitude,
          longitude
        };
        
        if (Platform.OS === "android") {
          if (this.marker) {
            this.marker.animateMarkerToCoordinate(
              newCoordinate,
              500
            );
           }


         } else {
           coordinate.timing(newCoordinate).start();
         }
         
         // 좌표값 갱신하기
         this.setState({
           latitude,
           longitude,
           routeCoordinates: routeCoordinates.concat([newCoordinate]), //이동경로
           distanceTravelled:distanceTravelled + this.calcDistance(newCoordinate), // 이동거리
           kcal:this.calcKcal(distanceTravelled), //칼로리 계산
           prevLatLng: newCoordinate
         });
      }
    );

  }

거리계산하기

: 사실 실제 개발에 들어가기전에 이 거리를 구하는 과정이 제일 걱정 됐었다😥.
평소에 수학을 잘하는 편이 아니었기에.. 사용자의 위치좌표를 구한다고 해도 어떻게 거리를 계산하지? 시작점? 끝점? 이동거리? 속도? 시간? 증가값? 델타? 미분? 가속도? 까지 생각의 나래를 펼쳤다.
그런데 거리계산에 도움을 주는 haversine npm package 가 있다는 사실을 알고 환호했다 😂
haversine(prevLatLng, newLatLng) 함수는 이전 위도경도 (prevLatLng)와 새로운 위도경도(newLatLng) 값을 인자로 주면 자동으로 거리를 계산해 반환하는 어메이징한 함수였다. 자세한 증명과정은 Haversine formula 에 들어가면 볼 수 있다. ~~근데 안들어가는게 정신건강에 이롭다^_^ ~~ 이럴떈 그냥 수학자분들께 감사합니다 하고 이미 만들어진 라이브러리를 감사해하며 이용해주자!

  calcDistance = newLatLng => { //거리 계산
    const { prevLatLng } = this.state;
    return haversine(prevLatLng, newLatLng) || 0;
  };

지도에 표시하기 (Rendering)

<MapView> component의 region props에 현재 위치를 넣어주면 그 위치를 중심으로 지도가 렌더링 된다. showsUserLocation이라는 속성값을 주면 지도상 어디에 유저가 위치해 있는지 점으로 표시해준다.

import MapView,{Marker,AnimatedRegion,Polyline,MarkerAnimated} from 'react-native-maps';

기본형

<MapView
  style={styles.map}
  showUserLocation
  followUserLocation
  loadingEnabled
  region={this.getMapRegion()}
>

쓰레기통 위치 마커로 찍기 기능이 추가된 Map

<MapView
  style={styles.map}
  showUserLocation
  followUserLocation
  loadingEnabled
  region={this.getMapRegion()}
  showsUserLocation
  onPress={(e)=>{
    console.log(e.nativeEvent.coordinate)
    const newTrashLocation=e.nativeEvent.coordinate;
    const trashLocations =this.state.trashLocations; //기존 쓰레기통 위치들 
    this.setState({trashLocations:[...trashLocations,newTrashLocation]})
    }}
>

이동경로 표시하기

: 유저가 움직인 경로를 그리기 위해서 Polyline 을 활용한다. Polyline 은 coordinates 라는 props를 지니고 있는데 여기에 사용자가 여태껏 이동해온 경로 좌표를 배열 형태로 넣어주면 점(위치) 들을 하나씩 이어가면서 경로를 그려준다.

 routeCoordinates: [{ // 이동경로
        latitude: 37.78825,
        longitude: -122.4324,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      },{ 
        latitude: 37.78825,
        longitude: -122.4324,
        latitudeDelta: 0.0922,
        longitudeDelta: 0.0421,
      }]
<Polyline coordinates={this.state.routeCoordinates} strokeWidth={5}  />

마커로 쓰레기통 위치 찍기

MapView 의 onPress 부분에 집중! e.nativeEvent.coordinate에는 유저가 현저 맵에서 클릭한 곳의 좌표값이 들어있다. 따라서 이를 이용해 유저가 찍은 쓰레기통 위치들을 저장해주면 된다.
이때 이전 trashLocations 를 펼쳐서 활용하기 위해 스프레드 (...) 연산자를 이용해야한다.

<MapView
  style={styles.map}
  showUserLocation
  followUserLocation
  loadingEnabled
  region={this.getMapRegion()}
  showsUserLocation
  onPress={(e)=>{
    console.log(e.nativeEvent.coordinate)
    const newTrashLocation=e.nativeEvent.coordinate;
    const trashLocations =this.state.trashLocations; //기존 쓰레기통 위치들 
    this.setState({trashLocations:[...trashLocations,newTrashLocation]})
    }}
>

{/*쓰레기통 위치*/this.state.trashLocations.map((location,idx)=><Marker
  key={idx}
  coordinate={location}
  title={'쓰레기통 위치'}
  description={'쓰레기통 위치'}
  onPress={(e)=>{console.log(e.nativeEvent.coordinate)}}
/>)}

[결과]
: 원하는 좌표에 쓰레기통 위치를 마커로 찍을 수 있게 된다.

칼로리 계산하기

: 이동한 거리를 바탕으로 칼로리를 계산해주는 함수이다. 처음에 칼로리를 어떤식으로 계산할지 굉장히 고민이 많았다.

1. 시간 당 거리 증가량( distanceDelta) 을 이용해 kcal를 계산할 것인지
2. 최종 축적된 거리 (distanceTravelled) 를 이용해 kcal를 계산할 것인지

1번은 3kcal + 1.5kcal + 1.5kcal ... 식으로 델타값을 더해서 칼로리 계산이 되고
2번은 3kcal -> 4.5kcal -> 6kcal 식으로 이동 거리가 바뀜에 따라 값이 새롭게 renewal되면서 칼로리가 계산된다

아래 코드는 1번으로 구현된 코드이다.

  calcKcal = distanceDelta=>{
    // 이동한 거리를 이용해 kcal 계산해주는 함수. 0.1m당 7kcal로 계산함.
    return distanceDelta/0.1 * 7;
  }

최종결과

: 위 튜토리얼을 차근차근 모두 따라했다면 아래와 같은 화면을 만날 수 있다!
이동할때마다 거리 + 칼로리가 계산된다.

사실 글에서는 생략된 코드 부분이 몇개 있다.
전체 소스코드를 보고 싶다면 아래 깃허브 주소로 들어가서 확인해봐도 좋다 :)
거리 추적을 구현하고자 하는 사람들에게 많은 도움이 되길 바라며..😁

https://github.com/jupging/jupgging-Frontend/blob/main/src/screens/PloggingScreens/PloggingScreen.js

profile
하루하루 나아가는 새싹 개발자 🌱

2개의 댓글

comment-user-thumbnail
2022년 7월 11일

정보 공유해주셔서 감사합니다!

답글 달기
comment-user-thumbnail
2023년 7월 22일

훌륭하세요^^

답글 달기