위치 추적 기능을 갖춘 나만의 Uber/Lyft 앱 구축하기
Published: 2024-02-22 07:41:21

**아카이브의 이 게시물에서는 PubNub를 사용하여 차량 위치를 추적하는 나만의 Uber 또는 Lyft 클론을 구축하는 방법을 설명합니다. 일부 코드 스니펫은 이전 라이브러리 버전을 참조할 수 있지만, 이 글의 모든 내용은 오늘날의 독자에게 적합합니다.**  
  

다른 디바이스의 실시간 위치를 추적하는 것은 많은 인기 있는 모바일 위치 중심 앱의 기본이며, 특히 Lyft나 Uber와 같이

구축할 때 특히 그렇습니다. 이 기능을 구축하는 것은 어렵고 지루할 수 있지만, PubNub에서 제공하는 운전자와 승객 간의 간편한 게시/구독 모델을 통해 이를 간소화할 수 있습니다.

튜토리얼 개요
-------

**이 튜토리얼의[전체 소스 코드는](https://github.com/PubNubDevelopers/RealtimeTaxiAndroidDemo) 여기에서 확인할 수 있습니다.**

이 튜토리얼에서는 Android 애플리케이션에서 이 일반적인 사용 사례를 구현하는 방법을 보여드리겠습니다. 먼저 빌드할 샘플 앱은 사용자에게 운전자인지 동승자인지 묻습니다. 사용자가 운전자라면 현재 위치가 채널에 게시되고, 5초마다(이 샘플에서는) 또는 앱에 필요한 횟수만큼 업데이트됩니다.

사용자가 동승자인 경우 동일한 채널을 구독하여 운전자의 현재 위치에 대한 업데이트를 받게 됩니다. 이 게시/구독 모델은 아래 비주얼에 표시되어 있습니다.

[![Uber/Lyft App](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1j4o/69ed800cea3e73d95979e9cad4cbfb3b/Screen-Shot-2018-06-06-at-11.58.58-AM.png "Uber/Lyft App")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1j4o/69ed800cea3e73d95979e9cad4cbfb3b/Screen-Shot-2018-06-06-at-11.58.58-AM.png)

Google 지도 API 및 PubNub 설정
-------------------------

먼저 지도 기능을 위해 [PubNub 안드로이드 SDK와](https://pubnub.com/docs/sdks/java/android/) Google Play 서비스를 사용하기 위해 Gradle 종속성을 작성해야 합니다. 또한PubNub 메시지의 기본적인 JSON 구문 분석을 완료하기 위해

```js
com.fasterxml.jackson
```

라이브러리를구현할 것입니다.

종속성 아래에 있는 앱 모듈의 build.gradle 파일에서 아래 명령어 코드 블록을 입력하여 필요한 것이 있는지 확인합니다. 필요한 라이브러리를 구현할 수 있도록 메뉴의 도구 부분에 있는 Android SDK 관리자에서 Google Play 서비스가 설치되어 있는지 확인하세요.

```js
implementation group: 'com.pubnub', name: 'pubnub-gson', version: '4.12.0'
implementation 'com.google.android.gms:play-services-maps:15.0.1'
implementation "com.google.android.gms:play-services-location:15.0.1"
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.9.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.9.2'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.9.2'
```

이제 필요한 종속성을 확보했으므로 Android 매니페스트 파일에 필요한 권한이 있는지 확인해 보겠습니다. 네트워크 상태에 액세스하고 인터넷 연결을 설정할 수 있는 권한이 있으면 이제 사용자가 PubNub API에 연결할 수 있습니다. 위치 권한은 나중에 드라이버의 위치를 요청할 때 사용할 것입니다.

```js
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
```

Android 앱에서 PubNub을 설정하려면 [PubNub 관리자 대시보드에서 Pub](https://dashboard.pubnub.com/signup/)Nub 앱을 생성해야 합니다 **(무료**). 앱을 만들면 구독 키와 퍼블리시 키가 할당됩니다. 다음 부분에서는 MainActivity 클래스에서 이러한 자격 증명을 사용하여 연결을 설정합니다.

이제 애플리케이션에서 사용할 수 있도록 Google 지도 API 키를 설정해야 합니다. 이를 위해 [Google의 개발자 API 콘솔로](https://console.cloud.google.com/projectselector2/apis/dashboard?supportedpurview=project) 이동합니다. 여기에서 새 프로젝트를 생성한 다음 API 키를 생성합니다. 이제 API 키가 생성되었으므로 다음 코드를 작성하여 Google 지도 API를 설정하고 생성된 API 키로 구성할 수 있습니다.

```js
<meta-data
   android:name="com.google.android.gms.version"
   android:value="@integer/google_play_services_version" />
<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value=”ENTER_KEY_HERE" />
```

주요 활동
-----

MainActivity 클래스에는 세 가지 중요한 기능이 있습니다:

1.  운전자인지 승객인지에 따라 사용자를 각각의 인터페이스로 안내합니다.
2.  앱의 모든 사용자를 위해 PubNub에 연결 설정하기
3.  위치 액세스 권한 확인

먼저 사용자를 운전자와 승객으로 구분하여 각각의 사용자 인터페이스를 제공하고자 합니다. 2부와 3부에서 자세히 설명할 DriverActivity 클래스와 PassengerActivity 클래스를 생성하여 이를 수행합니다.

MainActivity에서는 사용자에게 운전자 또는 승객으로서의 역할을 앱에 알릴 수 있는 간단한 버튼 기반 인터페이스를 만들 것입니다. 안드로이드의 Intent 클래스를 사용하면 어떤 버튼을 누르느냐에 따라 MainActivity 클래스에서 DriverActivity 클래스로, 또는 MainActivity 클래스에서 PassengerActivity 클래스로 전환할 수 있습니다.

인터페이스는 다음과 같이 표시됩니다:

[![screenshot](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1j4p/89ff1d0cceea0f257e1b66aacc1fa7a6/screenshot.png "screenshot")](https://images.ctfassets.net/3prze68gbwl1/asset-17suaysk1qa1j4p/89ff1d0cceea0f257e1b66aacc1fa7a6/screenshot.png)

```js
driverButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       startActivity(new Intent(MainActivity.this, DriverActivity.class));
   }
});
passengerButton.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
       startActivity(new Intent(MainActivity.this, PassengerActivity.class));
   }
});
```

다음으로, [관리자 대시보드에서](https://dashboard.pubnub.com/signup) PubNub 자격 증명을 사용하여

```js
PNConfiguration
```

인스턴스를 생성하여 앱과 PubNub을 연결하는

```js
PubNub
```

인스턴스를 생성합니다 . MainActivity 클래스의 이 PubNub 인스턴스는 공개적이고 정적이므로 DriverActivity와 PassengerActivity 모두에서 액세스할 수 있습니다.메인 액티비티의

```js
onCreate()
```

메서드에서아래 메서드를 호출하여 앱의 모든 사용자에 대해 PubNub가 올바르게 초기화되도록 합니다.이 모든 것이 다음 코드에 나와 있습니다.

```js
private void initPubnub() {
        PNConfiguration pnConfiguration = new PNConfiguration();
        pnConfiguration.setSubscribeKey(Constants.PUBNUB_SUBSCRIBE_KEY);
        pnConfiguration.setPublishKey(Constants.PUBNUB_PUBLISH_KEY);
        pnConfiguration.setSecure(true);
        pubnub = new PubNub(pnConfiguration);
    }
```

마지막으로 사용자가 앱의 위치 서비스를 활성화했는지 확인해야 합니다. 이는 다음 코드를 사용하여 수행할 수 있습니다. 사용자가 정밀 위치 또는 대략적인 위치에 액세스할 수 있는 권한을 부여하지 않으면 앱에서 이 권한을 요청합니다.

```js
public void checkPermission() {
   if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ||
           ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
           ) {//Can add more as per requirement
       ActivityCompat.requestPermissions(this,
               new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION},
               123);
   }
}
```

운전자 활동
------

DriverActivity 클래스의 유일한 기능은 운전자의 위치를 파악하고 이 데이터를 특정 시간 간격으로 PubNub 채널에 게시하는 것입니다.

먼저 위치 요청을 하는 방법을 살펴봅시다. Google 위치 API의

```js
FusedLocationProviderClient
```

클래스를 사용하여 드라이버의 위치 업데이트를 요청합니다. 또한

```js
LocationRequest
```

클래스를 사용하여 우선순위, 최소 변위 및 시간 간격과 같은 위치 요청의 중요한 매개 변수를 지정할 것입니다.

샘플에서는 시간 간격을 5000밀리초로 설정하여 5초마다 위치를 업데이트하고 최소 변위를 10미터로 설정하여 최소 10미터의 변화가 있는 경우에만 위치가 업데이트되도록 하겠습니다. 마지막으로, 위치 요청이 모바일 디바이스의 GPS를 사용하도록 하여 정확도가 높은 위치 추적이 가능하도록 합니다. 이러한 높은 정확도는 승객이 목적지까지 이동하는 운전자(자동차 아이콘으로 표시됨)를 지도에서 자세히 볼 수 있도록 하는 데 중요합니다.

```js
LocationRequest locationRequest = LocationRequest.create();
locationRequest.setInterval(5000);
locationRequest.setFastestInterval(5000);
locationRequest.setSmallestDisplacement(10);
locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
```

이제 위치 요청 매개변수를 정의했으므로 이 LocationRequest 객체를 FusedLocationProviderClient 객체의 위치 업데이트 요청에 전달합니다.

```js
mFusedLocationClient.requestLocationUpdates(locationRequest, new LocationCallback() {
   @Override
   public void onLocationResult(LocationResult locationResult) {
       Location location = locationResult.getLastLocation();
...
```

요청에서 얻은 위치를 사용하여 PubNub 채널에서 원하는 위치 메시지 형식에 맞도록 LinkedHashMap으로 변환해야 합니다. LinkedHashMap에는 두 개의 키 "lat"과 "lng"와 이 키에 대응하는 값이 모두 문자열로 포함됩니다.

이제 링크드해시맵 형식의 메시지를 PubNub 채널에 게시할 수 있습니다. 이는 다음 코드에 나와 있습니다.

```js
MainActivity.pubnub
```

를사용하여앞서 메인 액티비티의 onCreate 메서드에서 가져온 PubNub 인스턴스를 가져와야한다는 것을 알 수 있습니다.PubNub을 다시 초기화하여 여러 연결을 설정하고 싶지 않습니다. 대신 MainActivity 클래스에서 이미 생성한 것을 사용합니다.

```js
MainActivity.pubnub.publish()
       .message(message)
       .channel(Constants.PUBNUB_CHANNEL_NAME)
       .async(new PNCallback<PNPublishResult>() {
           @Override
           public void onResponse(PNPublishResult result, PNStatus status) {
               // handle publish result, status always present, result if successful
               // status.isError() to see if error happened
               if (!status.isError()) {
                   System.out.println("pub timetoken: " + result.getTimetoken());
               }
               System.out.println("pub status code: " + status.getStatusCode());
           }
       });
```

승객 활동
-----

PassengerActivity 클래스에서 업데이트 중인 운전자의 위치를 표시하는 MapView를 만들고 싶습니다. 승객 액티비티에 MapView UI 요소를 추가하려면 이 액티비티의 해당 레이아웃.xml 파일에 다음 코드를 포함해야 합니다.

```js
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/map"
   android:name="com.google.android.gms.maps.SupportMapFragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context="com.example.mapwithmarker.MapsMarkerActivity" />
```

그런 다음 레이아웃 파일에서 정의한 ID를 사용하여

```js
MapFragment
```

객체를 인스턴스화합니다 .샘플 코드에서는 ID로 "map"을 사용했습니다. 또한 PassengerActivity가

```js
onMapReadyCallback
```

을 구현하도록 하여

```js
onMapReady
```

메서드를설정합니다 . 그런 다음 MapFragment 객체의

```js
getMapAsync
```

메서드에

```js
PassengerActivity.this
```

를파라미터로 전달합니다 .이렇게 하면 MapFragment 객체에 대한 onMapReady 콜백 메서드를 설정할 수 있습니다.

```js
mMapFragment = (SupportMapFragment) getSupportFragmentManager()
       .findFragmentById(R.id.map);
mMapFragment.getMapAsync(PassengerActivity.this);
```

이제 드라이버의 현재 위치에 대한 업데이트를 받을 수 있도록 드라이버 위치 채널을 구독하려고 합니다. 이 작업은 지도가 준비되면 수행해야 하므로 콜백 메서드에 이 코드를 포함할 것입니다. 또한 앱이 메시지 수신 시 어떻게 행동해야 하는지 알 수 있도록

```js
SubscribeCallback
```

를 사용하여리스너를 추가해야 합니다.

PubNub 안드로이드 문서에 명시된 대로, 먼저 리스너를 추가한 다음 PubNub 인스턴스를 채널에 구독하는 순서로 이 작업을 수행해야 합니다. 메시지를 수신하면 JSON 출력을 LinkedHashMap으로 변환하여 쉽게 파싱하여 경도와 위도를 얻고자 합니다. JSON 출력을 LinkedHashMap으로 변환하기 위해서는 이를 가능하게 하는

```js
fromJson()
```

메서드가 포함된

```js
JsonUtil
```

클래스를 임포트해야 합니다.이 클래스는 전체 소스 코드에 나와 있습니다. 드라이버의 위치를 파악한 후에는

```js
updateUI()
```

메서드를 호출하고 새 위치를 매개변수로 전달하여 UI를 업데이트해야 합니다.

```js
MainActivity.pubnub.addListener(new SubscribeCallback() {
    @Override
    public void status(PubNub pub, PNStatus status) {
    }
    @Override
    public void message(PubNub pub, final PNMessageResult message) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    Map<String, String> newLocation = JsonUtil.fromJson(message.getMessage().toString(), LinkedHashMap.class);
                    updateUI(newLocation);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }
    @Override
    public void presence(PubNub pub, PNPresenceEventResult presence) {
    }
});
MainActivity.pubnub.subscribe()
        .channels(Arrays.asList(Constants.PUBNUB_CHANNEL_NAME)) // subscribe to channels
        .execute();
```

updateUI 메서드에는 두 가지 경우가 있습니다.

첫 번째 경우는 드라이버에 대한 마커가 아직 없는 경우로, 이 경우

```js
Marker
```

객체를 인스턴 스화하고 그 위치를 드라이버의 첫 번째 추적된 위치로 설정합니다. 또한 운전자 차량의 스트리트 뷰를 유지하려면 지도를 확대해야 합니다.

두 번째 경우는 운전자에 대한 마커가 이미 존재하는 경우로, 이 경우에는 마커 객체를 다시 인스턴스화할 필요가 없습니다. 그 대신 지도에서 마커의 위치를 새 위치로 이동하기만 하면 됩니다.

```js
animateCar()
```

메서드를 호출하여 이를 수행합니다. 또한 카메라의 범위를 벗어난 경우 MapView의 카메라를 새 위치로 이동해야 합니다. 따라서 운전자의 마커는 항상 MapView의 경계 안에 있게 됩니다. 이는 다음 코드에 나와 있습니다.

```js
private void updateUI(Map<String, String> newLoc) {
        LatLng newLocation = new LatLng(Double.valueOf(newLoc.get("lat")), Double.valueOf(newLoc.get("lng")));
        if (driverMarker != null) {
            animateCar(newLocation);
            boolean contains = mGoogleMap.getProjection()
                    .getVisibleRegion()
                    .latLngBounds
                    .contains(newLocation);
            if (!contains) {
                mGoogleMap.moveCamera(CameraUpdateFactory.newLatLng(newLocation));
            }
        } else {
            mGoogleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
                    newLocation, 15.5f));
            driverMarker = mGoogleMap.addMarker(new MarkerOptions().position(newLocation).
                    icon(BitmapDescriptorFactory.fromResource(R.drawable.car)));
        }
    }
```

animateCar 메서드에서는 자동차 마커를 부드러운 선 모양으로 새 위치로 이동시켜야 합니다. 앱이 5초마다 운전자의 위치를 업데이트하기 때문에 애니메이션은 5초 동안 지속되어야 합니다. 이렇게 하면 애니메이션이 끝날 때마다 새 위치가 업데이트되어 다음 애니메이션이 시작됩니다. 이렇게 하면 자동차 애니메이션의 지연을 최소화할 수 있습니다.

또한 아래의

```js
LatLngInterpolator
```

인터페이스를 활용합니다. 이를 통해 메서드

```js
interpolate()
```

메서드를 구현할 수 있는데, 이 메서드는 최종 newLocation까지 가는 길의 일부분인 새로운 LatLng 좌표를 반환합니다. 이 분수는 메서드

```js
getAnimatedFraction()
```

. 이 메서드를 호출하려면

```js
ValueAnimator
```

인스턴스에서 이 메서드를 호출하고

```js
onAnimationUpdate()
```

콜백 메서드에서 얻습니다. 이 분수는 일정한 비율로 증가하여 5초가 되면 1이 됩니다(새 위치까지). 이 자동차 애니메이션은 다음 코드 스니펫에 나와 있습니다.

```js
private void animateCar(final LatLng destination) {
        final LatLng startPosition = driverMarker.getPosition();
        final LatLng endPosition = new LatLng(destination.latitude, destination.longitude);
        final LatLngInterpolator latLngInterpolator = new LatLngInterpolator.LinearFixed();
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.setDuration(5000); // duration 5 seconds
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                try {
                    float v = animation.getAnimatedFraction();
                    LatLng newPosition = latLngInterpolator.interpolate(v, startPosition, endPosition);
                    driverMarker.setPosition(newPosition);
                } catch (Exception ex) {
                }
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });
        valueAnimator.start();
    }
```

```js
private interface LatLngInterpolator {
       LatLng interpolate(float fraction, LatLng a, LatLng b);
       class LinearFixed implements LatLngInterpolator {
           @Override
           public LatLng interpolate(float fraction, LatLng a, LatLng b) {
               double lat = (b.latitude - a.latitude) * fraction + a.latitude;
               double lngDelta = b.longitude - a.longitude;
               if (Math.abs(lngDelta) > 180) {
                   lngDelta -= Math.signum(lngDelta) * 360;
               }
               double lng = lngDelta * fraction + a.longitude;
               return new LatLng(lat, lng);
           }
       }
   }
```

추가 개선 사항
--------

이 샘플을 더 발전시키고 싶다면 자동차가 회전할 때 마커가 회전하도록 하는 기능을 추가할 수 있습니다. 이 기능을 추가하려면 메시지 구조에 키-값 쌍을 하나 추가해야 합니다. 키 "방위"와 값을 포함해야 합니다. 그런 다음 운전자의 방위 방향에 따라 마커를 회전시키기만 하면 됩니다.

축하합니다!
------

이제 메인 액티비티, 드라이버 액티비티, 승객 액티비티를 만들었으므로 간단한 Lyft/Uber 안드로이드 데모를 만드는 데 필요한 게시/구독 모델을 성공적으로 구축했습니다. [전체 소스 코드를](https://github.com/PubNubDevelopers/RealtimeTaxiAndroidDemo) 보려면 여기를 클릭하세요.

  
이 페이지에서 **임베드된 콘텐츠를 사용할 수 없는 경우** [https://www.youtube.com/embed/kQ6EzqoQu5A](https://www.youtube.com/embed/kQ6EzqoQu5A) 에서도 볼 수 있습니다.

추가 리소스
------

-   [애플리케이션에 위치 정보 API를 추가해야 하나요?](https://www.pubnub.com/guides/should-you-add-a-geolocation-api-to-your-app/)
    
-   [위치 정보 데모](https://www.pubnub.com/demos/geolocation-demo/)
    
-   [위치 정보 개발자 경로](https://www.pubnub.com/developers/geolocation-developer-path/)
    

PubNub이 어떤 도움을 줄 수 있나요?
=======================

이 문서는 원래 [PubNub.com에](https://www.pubnub.com/blog/build-your-own-uber-lyft-app-geolocation-tracking-android/) 게시되었습니다.

저희 플랫폼은 개발자가 웹 앱, 모바일 앱 및 IoT 디바이스를 위한 실시간 상호작용을 구축, 제공 및 관리할 수 있도록 지원합니다.

저희 플랫폼의 기반은 업계에서 가장 크고 확장성이 뛰어난 실시간 에지 메시징 네트워크입니다. 전 세계 15개 이상의 PoP가 월간 8억 명의 활성 사용자를 지원하고 99.999%의 안정성을 제공하므로 중단, 동시 접속자 수 제한 또는 트래픽 폭증으로 인한 지연 문제를 걱정할 필요가 없습니다.

PubNub 체험하기
-----------

[라이브 투어를](https://www.pubnub.com/tour/introduction/) 통해 5분 이내에 모든 PubNub 기반 앱의 필수 개념을 이해하세요.

설정하기
----

PubNub [계정에](https://admin.pubnub.com/signup/) 가입하여 PubNub 키에 무료로 즉시 액세스하세요.

시작하기
----

사용 사례나 [SDK에](https://www.pubnub.com/docs) 관계없이 [PubNub 문서를](https://www.pubnub.com/docs) 통해 바로 시작하고 실행할 수 있습니다.