콘텐츠로 이동

5. 네이버 지도 연동하기

3장에서 만들어 둔 메인 화면의 회색 상자를 기억하십니까? 그 자리에 "지도가 들어갈 자리"라고 적어 두었습니다. 이번 장에서 그 약속을 지킬 차례입니다. 진짜로 움직이는 네이버 지도를 띄우고, 지도 위에 마커를 꽂고, 내 위치까지 표시해 보겠습니다.

이번 장이 끝나면 버킷맵은 "지도 앱"이라고 부를 수 있는 모습이 됩니다.

지도 SDK 란?

여러분이 지도를 직접 그린다고 상상해 봅시다. 전국의 도로, 건물, 지하철 노선을 모두 그려야 하고, 손가락으로 밀면 부드럽게 움직이게 만들어야 하고, 확대하면 더 자세한 그림으로 바꿔치기해야 합니다. 혼자서는 몇 년이 걸려도 못 만듭니다.

그래서 지도 회사들은 자기네 지도를 앱에 통째로 넣을 수 있는 부품 상자를 나눠 줍니다. 이런 부품 상자를 SDK(Software Development Kit)라고 한다. 개발자는 부품 상자를 가져다 조립만 하면 되고, 지도 그림과 데이터는 지도 회사의 서버가 실시간으로 보내 준다.

안드로이드에서 쓸 수 있는 지도 SDK 는 여러 가지가 있다.

지도 SDK 만든 곳 특징
Google Maps SDK 구글 전 세계 지도. 해외 지역이 상세하다
네이버 지도 SDK 네이버 국내 도로·건물·대중교통 정보가 매우 정밀하다
카카오맵 SDK 카카오 국내 지도. 카카오 서비스와 연동이 쉽다

버킷맵은 네이버 지도 SDK 를 사용합니다. 우리가 담을 장소 대부분이 국내이고, 국내 지도는 네이버가 가장 정밀하기 때문입니다.

GoogleMap 을 배웠다면

안드로이드 교재의 지도 단원은 보통 GoogleMap 으로 진행됩니다. 지도를 붙이는 회사만 다를 뿐, 지도 뷰를 화면에 넣고 → 카메라를 옮기고 → 마커를 꽂는 핵심 개념은 완전히 같습니다. 네이버 지도로 익혀 두면 GoogleMap 도 금방 다룰 수 있습니다.

지도 키 발급받기

부품 상자는 공짜로 열리지 않습니다. 네이버는 "누가 우리 지도를 쓰는지"를 알아야 하므로, 개발자마다 열쇠(Client ID) 를 발급해 줍니다. 앱이 지도를 요청할 때 이 열쇠를 함께 보내면, 네이버 서버가 확인하고 지도를 내려 준다.

[CH-1 아래 과정을 그대로 진행하시오.]

  1. 브라우저에서 https://www.ncloud.com (네이버 클라우드 플랫폼)에 접속해 회원 가입 후 로그인한다.
  2. 상단의 콘솔 버튼을 눌러 콘솔로 들어간다.
  3. 왼쪽 메뉴에서 Services → Application Services → Maps 를 찾아 들어간다.
  4. Application 등록 버튼을 누른다.
  5. 애플리케이션 이름에 bucketmap 처럼 알아볼 수 있는 이름을 적는다.
  6. Application 서비스 선택에서 Dynamic Map 에 반드시 체크한다.
  7. Android 앱 패키지 이름 항목에 com.example.myapplication 을 정확히 입력하고 등록한다.
  8. 등록된 애플리케이션의 인증 정보를 열어 Client ID 값을 복사해 둔다.

콘솔 화면이 책과 다르게 보인다면

웹 콘솔 화면은 수시로 개편됩니다. 메뉴 위치가 다르면 네이버 지도 SDK 공식 시작 가이드(https://navermaps.github.io/android-map-sdk/guide-ko/1.html)의 최신 절차를 함께 확인하십시오.

[설명]

  1. Dynamic Map 체크를 빼먹으면 열쇠는 발급되지만 지도 서비스 사용 허가가 없는 열쇠가 된다. 앱 실행 시 인증 오류(429)가 난다.
  2. Android 앱 패키지 이름은 열쇠를 쓸 수 있는 앱을 제한하는 장치다. 1장에서 프로젝트 패키지명을 com.example.myapplication 으로 만들었는데, 바로 여기 등록할 이름과 맞추기 위해서였다. 철자가 하나라도 다르면 인증 오류(401)가 난다.
  3. Client ID 는 열쇠 그 자체다. 다른 사람에게 보여 주지 않는 것이 원칙이다.

열쇠를 안전하게 보관하기

복사한 Client ID 를 어디에 둘까요? 가장 쉬운 방법은 Java 코드에 직접 적는 것이지만, 이 방법은 쓰지 않습니다.

[CH-2 아래 내용을 local.properties 파일 끝에 추가하시오.]

Android Studio 왼쪽 파일 목록에서 Project 보기로 전환하면 프로젝트 루트에 local.properties 파일이 있다. 파일을 열고 맨 아래에 두 줄을 추가한다. (PLACES_API_KEY 는 6장에서 채운다 — 지금은 빈 값으로 둔다.)

NCP_KEY_ID=발급받은_클라이언트_ID
PLACES_API_KEY=

[설명]

  1. 코드에 열쇠를 직접 적으면, 코드를 친구에게 보내거나 GitHub 에 올리는 순간 열쇠도 함께 공개된다. 남이 내 열쇠로 지도를 쓰면 사용량이 내 몫으로 쌓인다.
  2. local.properties 는 "이 컴퓨터에서만 쓰는 값"을 담는 파일로, 버전 관리(Git)에서 자동으로 제외되도록 처음부터 설정되어 있다. 열쇠를 두기에 알맞은 자리다.
  3. = 양옆에 공백이나 따옴표를 넣지 않는다.

[CH-3 아래 코드를 그대로 작성하고 실행하시오.]

이제 빌드 설정이 local.properties 의 값을 읽어 코드에서 쓸 수 있게 만들어 줍니다. app/build.gradle 파일을 열어 세 군데를 수정한다.

먼저 plugins { ... } 블록 바로 아래에 읽기 코드를 추가한다.

// API 키는 VCS 에 커밋되지 않는 local.properties 에서 읽어 온다
def localProps = new Properties()
def localPropsFile = rootProject.file('local.properties')
if (localPropsFile.exists()) {
    localPropsFile.withInputStream { localProps.load(it) }
}

다음으로 defaultConfig { ... } 안의 versionName "1.0" 아래에 두 줄을 추가한다.

        buildConfigField "String", "NCP_KEY_ID", "\"${localProps.getProperty('NCP_KEY_ID', '')}\""
        buildConfigField "String", "PLACES_API_KEY", "\"${localProps.getProperty('PLACES_API_KEY', '')}\""

마지막으로 defaultConfig { ... } 블록이 끝난 다음 줄에 블록 하나를 추가한다.

    buildFeatures {
        buildConfig true
    }

수정을 마치면 편집기 위에 나타나는 Sync Now 를 누른다.

실행 결과

BUILD SUCCESSFUL in 2s

[설명]

  1. buildConfigField 는 빌드할 때 BuildConfig 라는 Java 클래스를 자동으로 만들어 그 안에 값을 넣어 준다. 이후 코드에서 BuildConfig.NCP_KEY_ID 라고 쓰면 열쇠 값이 나온다.
  2. 열쇠의 실제 값은 local.properties 에만 있고, 코드에는 BuildConfig.NCP_KEY_ID 라는 이름만 등장한다. 코드를 통째로 공개해도 열쇠는 새지 않는다.
  3. buildFeatures { buildConfig true } 는 "BuildConfig 클래스를 만들어 달라"는 스위치다.

부품 가게 등록과 SDK 내려받기

라이브러리(부품)는 인터넷의 저장소(repository) 라는 부품 가게에서 내려받습니다. 구글·마벤센트럴 같은 큰 가게는 프로젝트를 만들 때부터 등록되어 있지만, 네이버 지도 SDK 는 네이버의 전용 가게에만 있습니다. 가게 주소부터 등록해야 한다.

[CH-4 아래 코드를 그대로 작성하고 실행하시오.]

프로젝트 루트의 settings.gradle 을 열어 dependencyResolutionManagement 블록 안 repositories 를 다음처럼 만든다.

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        // 네이버 지도 SDK 는 네이버 전용 저장소에서 받는다 (5장)
        maven { url = uri("https://repository.map.naver.com/archive/maven") }
    }
}

[CH-5 아래 코드를 그대로 작성하고 실행하시오.]

app/build.gradledependencies { ... } 안, 4장에서 추가한 RecyclerView 아래에 두 줄을 추가하고 Sync Now 를 누른다.

    // 네이버 지도 + 현위치 (5장)
    implementation 'com.naver.maps:map-sdk:3.22.1'
    implementation 'com.google.android.gms:play-services-location:21.0.1'

실행 결과

BUILD SUCCESSFUL in 41s

[설명]

  1. com.naver.maps:map-sdk:3.22.1 이 네이버 지도 부품 상자다. 버전 숫자까지 책과 똑같이 맞춘다. 버전이 다르면 책의 코드가 그대로 동작하지 않을 수 있다. 최신 버전 번호는 공식 가이드에서 확인할 수 있다.
  2. play-services-location 은 구글이 제공하는 위치 부품이다. "지금 폰이 어디에 있는가"를 알아내는 데 쓰이고, 네이버 지도의 현위치 기능이 이 부품을 이용한다.
  3. 첫 Sync 는 부품을 인터넷에서 내려받느라 시간이 걸린다.

위치 권한 선언하기

지도는 인터넷에서 내려오고, 내 위치는 민감한 개인정보입니다. 안드로이드는 앱이 이런 것을 쓰려면 미리 신고(선언)하고, 민감한 것은 사용자에게 허락까지 받도록 정해 두었다.

[CH-6 아래 코드를 그대로 작성하고 실행하시오.]

app/src/main/AndroidManifest.xml<manifest ...> 여는 태그 바로 아래에 세 줄을 추가한다.

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

[설명]

  1. INTERNET — 지도 그림을 네이버 서버에서 받아 오려면 인터넷이 필요하다. 선언만 하면 되는 순한 권한이다.
  2. ACCESS_FINE_LOCATION — GPS 급의 정확한 위치. 민감한 권한이라 실행 중에 사용자 허락 창이 뜬다.
  3. ACCESS_COARSE_LOCATION — 기지국·와이파이 수준의 대략적 위치. 사용자가 "대략적인 위치만 허용"을 고를 수 있게 함께 선언한다.

드디어 지도 띄우기

준비가 끝났습니다. 회색 상자를 걷어 내고 진짜 지도를 넣습니다.

[CH-7 아래 코드를 그대로 작성하고 실행하시오.]

res/layout/activity_main.xml 에서 3장에 만든 map_container FrameLayout(안의 TextView 포함)을 통째로 지우고, 그 자리에 아래 조각을 넣는다. 파일 전체는 다음과 같아진다.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/map_fragment"
        android:name="com.naver.maps.map.MapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_margin="16dp"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_find_place"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginEnd="8dp"
            android:text="@string/btn_find_place" />

        <Button
            android:id="@+id/btn_my_places"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="@string/btn_my_places" />

    </LinearLayout>

</FrameLayout>

strings.xml 에서 이제 쓰지 않는 map_placeholder 문자열 한 줄은 지워도 된다.

[설명]

  1. FragmentContainerView 는 "다른 화면 조각(Fragment)을 끼워 넣는 액자"다. android:name 에 적힌 com.naver.maps.map.MapFragment 가 네이버 SDK 가 만들어 둔 지도 조각이고, 액자에 자동으로 끼워진다.
  2. 지도가 화면 전체를 덮고, 버튼들은 FrameLayout 의 겹치기 성질 덕분에 지도 위에 떠 있게 된다 — 3장에서 배운 구조가 그대로 살아 있다.

[CH-8 아래 코드를 그대로 작성하고 실행하시오.]

MainActivity.java 를 아래 내용으로 통째로 바꾼다.

package com.example.myapplication;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import com.naver.maps.geometry.LatLng;
import com.naver.maps.map.CameraUpdate;
import com.naver.maps.map.LocationTrackingMode;
import com.naver.maps.map.MapFragment;
import com.naver.maps.map.NaverMap;
import com.naver.maps.map.NaverMapSdk;
import com.naver.maps.map.OnMapReadyCallback;
import com.naver.maps.map.overlay.Marker;
import com.naver.maps.map.util.FusedLocationSource;

public class MainActivity extends AppCompatActivity implements OnMapReadyCallback {

    private static final int LOCATION_PERMISSION_REQUEST_CODE = 1000;

    private NaverMap naverMap;
    private FusedLocationSource locationSource;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 지도를 그리기 전에 네이버 클라우드에서 발급받은 키를 SDK 에 알려 준다
        NaverMapSdk.getInstance(this).setClient(
                new NaverMapSdk.NcpKeyClient(BuildConfig.NCP_KEY_ID));

        setContentView(R.layout.activity_main);

        Button findButton = findViewById(R.id.btn_find_place);
        Button myPlacesButton = findViewById(R.id.btn_my_places);

        findButton.setOnClickListener(v -> {
            Intent intent = new Intent(this, SearchActivity.class);
            startActivity(intent);
        });

        myPlacesButton.setOnClickListener(v -> {
            Intent intent = new Intent(this, MyPlacesActivity.class);
            startActivity(intent);
        });

        locationSource = new FusedLocationSource(this, LOCATION_PERMISSION_REQUEST_CODE);

        MapFragment mapFragment = (MapFragment) getSupportFragmentManager()
                .findFragmentById(R.id.map_fragment);
        mapFragment.getMapAsync(this);
    }

    @Override
    public void onMapReady(@NonNull NaverMap naverMap) {
        this.naverMap = naverMap;

        // 시작 위치: 서울 시청 (현위치 권한을 허용하면 내 위치로 이동한다)
        naverMap.moveCamera(CameraUpdate.scrollTo(new LatLng(37.5665, 126.9780)));

        // 연습용 마커 — 7장에서 저장된 장소들의 마커로 바뀐다
        Marker marker = new Marker();
        marker.setPosition(new LatLng(37.5796, 126.9770));
        marker.setCaptionText("경복궁");
        marker.setMap(naverMap);

        naverMap.setLocationSource(locationSource);
        naverMap.getUiSettings().setLocationButtonEnabled(true);
        naverMap.setLocationTrackingMode(LocationTrackingMode.Follow);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        if (locationSource.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
            if (!locationSource.isActivated()) {
                // 권한을 거부하면 위치 추적만 끈다 — 지도는 그대로 쓸 수 있다
                naverMap.setLocationTrackingMode(LocationTrackingMode.None);
            }
            return;
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

실행 결과

첫 실행 화면: 서울 지도가 렌더링된 위로 "버킷맵에서 이 기기의 위치 정보에 액세스하도록 허용하시겠습니까?" 위치 권한 대화상자가 떠 있다

앱을 실행하면 진짜 지도가 그려지고, 그 위로 위치 권한을 묻는 창이 뜬다. 앱 사용 중에만 허용을 누른다.

권한 허용 후 화면: 서울시청 위치에 파란 현위치 점이 있고 위쪽에 경복궁 초록 마커가 표시되어 있으며 왼쪽 아래에 현위치 버튼이 있다

허용하면 파란 점이 내 위치를 가리키고, 경복궁 자리에 연습용 마커가 꽂혀 있다. 지도를 손가락으로 끌고, 두 손가락으로 확대·축소해 보십시오.

[설명]

  1. NaverMapSdk...setClient(new NaverMapSdk.NcpKeyClient(BuildConfig.NCP_KEY_ID)) — 지도를 그리기 전에 열쇠를 SDK 에 건네는 코드다. setContentView() 보다 먼저 와야 한다.
  2. getMapAsync(this) 의 Async 는 "비동기"라는 뜻이다. 지도는 준비에 시간이 걸리므로, "준비되면 알려 줘" 하고 부탁만 해 두고 코드는 계속 진행한다. 준비가 끝나는 순간 onMapReady() 가 호출된다.
  3. implements OnMapReadyCallback — "지도가 준비되면 연락받는 쪽"이 되겠다는 선언이다. 그래서 onMapReady() 메서드를 반드시 만들어야 한다.
  4. LatLng 는 위도(latitude)·경도(longitude) 한 쌍이고, CameraUpdate.scrollTo() 는 지도 카메라를 그 좌표로 옮긴다. 지도는 세상 전체이고, 화면은 카메라가 비추는 부분이다.
  5. Marker 는 지도 위에 꽂는 압정이다. 좌표를 정하고(setPosition), 이름표를 달고(setCaptionText), setMap(naverMap) 하는 순간 지도에 나타난다.
  6. FusedLocationSource 는 아까 추가한 구글 위치 부품과 네이버 지도를 이어 주는 다리다. LocationTrackingMode.Follow 는 "내 위치를 따라다녀라"라는 모드로, 이 모드를 켜는 순간 SDK 가 알아서 권한 요청 창을 띄운다.
  7. onRequestPermissionsResult() 는 사용자가 허용/거부를 고른 결과가 돌아오는 곳이다. 결과 처리를 locationSource 에 위임하고, 거부라면 위치 추적만 끈다.
  8. 에뮬레이터는 진짜 GPS 가 없어서 가짜 위치를 쓴다. 기본값이 미국(구글 본사)이라 파란 점이 미국에 찍힐 수 있다. 에뮬레이터 오른쪽 도구에서 ⋮ (Extended Controls) → Location 을 열어 서울 좌표(위도 37.5665, 경도 126.9780)를 SET POINT 하면 된다.

잘 안 될 때

증상 원인 해결
"인증에 실패하였습니다" 401 패키지명 불일치 또는 Client ID 오타 NCP 콘솔의 Android 패키지명이 com.example.myapplication 인지, local.properties 의 값에 오타·공백이 없는지 확인
인증 오류 429 Application 등록 때 Dynamic Map 미체크 NCP 콘솔에서 해당 Application 수정 → Dynamic Map 체크
지도가 회색으로만 나온다 인터넷 권한 누락 또는 네트워크 문제 CH-6 의 INTERNET 권한, 에뮬레이터의 인터넷 연결 확인
파란 점이 미국에 있다 에뮬레이터 기본 가짜 위치 Extended Controls → Location 에서 서울 좌표 설정

[self Test 1] - 앱의 시작 위치를 우리 학교(또는 우리 집) 좌표로 바꾸고, 연습용 마커도 그 근처의 좋아하는 장소로 옮겨 이름표를 바꿔 보시오. (좌표는 네이버 지도 웹에서 원하는 지점을 우클릭하면 확인할 수 있다.)

[self Test 2] - CameraUpdate 에는 scrollTo() 말고 좌표와 확대 수준을 한 번에 정하는 scrollAndZoomTo(좌표, 줌레벨) 도 있다. 줌 레벨을 8, 12, 16 으로 바꿔 가며 화면이 어떻게 달라지는지 실행해 보시오.