Flutter 지도 - 네이버지도 키 발급 및 프로젝트 설정, 그리기 API 구현하기

kkensu
조현철의 개발로그
21 min readMay 6, 2022

지난 시간에는 구글 지도를 사용하는 방법에 대해서 포스팅 했습니다. 혹시 못 보신 분들은 아래 링크에서 보시면 됩니다!

이번에는 네이버 지도와 관련해서 포스팅 해보려고 합니다.

1. 요금정책

제 기억으로는 네이버 지도가 무료제공하다가 본격적으로 유료화를 시행했었던 기억이 있습니다. 그래서 자료를 찾아보다 관련 자료를 좀 찾았습니다.

그러다가 네이버가 사실상 무료형태로 변경을 했습니다. 이것도 기사를 좀 찾아봤습니다. 기사 내용으로는 할인 프로모션 형태로 진행하다가 2020년 1월 부터 과금을 실시할 예정 이었지만 네이버 정책 변경이 있었습니다.

Mobile Dynamic Map 은 구글과 동일하게 무료입니다. 그렇지만 월간 최대 한도가 1억건이 넘어가면 고객지원 → 문의하기로 연락하면 더 늘릴 수도 있을 듯 합니다. 월에 1억건 넘는 트래픽이 발생하는 서비스가 얼마나 될까요?

2018년 구글 지도가 유료화가 되고 나서 네이버가 무료화되면서 국내 서비스들은 네이버 지도로 많이 전환 되었던 것으로 기억합니다.

2. 새 프로젝트 생성 및 라이브러리 설치

저는 Android Studio 로 개발합니다. 새 Flutter 프로젝트 생성할 때 Organization을 설정하고 Project name 을 설정해 주었습니다. 이렇게 하면 Android의 패키지명은 kr.co.kkensu.flutter_naver_map_sample이 되고 iOS의 번들ID는 kr.co.kkensu.flutterNaverMapSample이 됩니다.

pub.dev에 가서 flutter naver map 으로 검색하면 flutter_naver_map 라는 패키지가 보입니다.

아래의 명령어를 통해 라이브러리를 설치합니다. 그 후에 각 기기 별 설정을 해주어야 합니다.

flutter pub add flutter_naver_map

그 후 Android, iOS 각각 설정 해 주어야 하는데 이것은 API KEY 발급 후에 진행하도록 하겠습니다.

3. API KEY 발급하기

API KEY 를 발급받기 위해서는 엔클라우드 콘솔에 가입되어 있어야 합니다. 콘솔에 접속하여 Services → AI.NAVER API 를 선택합니다.

그 후 Application 등록 버튼을 눌러 주세요.

Application 이름은 flutter-naver-map (띄어쓰기 대신 하이픈으로 해야 함), Maps 중 추 후 다룰 수 있다는 생각에 Web Dynamic Map만 제외하고 나머지 체크 했습니다. 실제 필요한 것은 Mobile Dynamic Map만 있으면 되긴 합니다.

서비스 환경 등록 부분은 Android 패키지명과 iOS Bundle ID를 입력해야 합니다. 위에서 프로젝트 생성 할 때 Android 패키지 명은 kr.co.kkensu.flutter_naver_map_sample 이고, iOS Bundie ID는 kr.co.kkensu.flutterNaverMapSample 이었습니다. 아래와 같이 되도록 입력 후 추가 버튼을 눌러야 합니다. 최종적으로 저장 버튼을 눌러 주세요.

그러면 아래와 같이 flutter-naver-map 으로 추가된 것을 볼 수 있습니다.

App 이름의 flutter-naver-map 하단에 “변경” 버튼을 누르면 아래와 같이 인증 정보를 볼 수 있는 팝업창이 나옵니다. 여기에서 “Client ID”가 API KEY 입니다.

4. API KEY 추가하기

flutter_naver_map 패키지의 Readme 에 적혀 있는 대로 진행 하면 Android, iOS 각각 API KEY 를 추가 할 수 있습니다.

4–1. Android

위 이미지에 있는대로 넣고 빌드를 하면 정상적으로 빌드 되고 앱이 구동 됩니다. 저는 여기에 추가적으로 Git 에 올릴 때 API KEY를 숨기기 위해서 몇가지 설정을 추가 했습니다.

local.properties

naver.api.key=12341234

12341234 부분에 본인 API KEY를 넣어주세요

android/app/build.gradle

def naverApiKey = localProperties.getProperty("naver.api.key")
if (naverApiKey == null) {
throw new GradleException("Google Api Key not found. Define location with google.api.key in the local.properties file.")
}
android {
buildTypes {
debug {
manifestPlaceholders = [naverApiKey: naverApiKey]
}
release {
signingConfig signingConfigs.debug
manifestPlaceholders = [naverApiKey: naverApiKey]
}
}

flutterVersionName 정의되어 있는 부분 하단에 def naverApiKey 부분을 추가해 주세요. 또 android → buildTypes → debug와 release에 manifestPlaceholders 부분을 추가 해 주세요.

android/app/src/main/AndroidManifest.xml

<meta-data
android:name="com.naver.maps.map.CLIENT_ID"
android:value="${naverApiKey}" />

기존에 key를 String으로 직접 넣은 것 대신에 ${naverApiKey} 라고 적어줍니다. 이렇게 하면 API KEY가 git에 올라가는 것을 방지 할 수 있습니다.

4–2.iOS

위의 설명처럼 진행 했습니다. brew install git-lfs 후에 git lfs install 하였습니다. 그리고 Info.plist 에 Client ID와 권한에 대해 선언했습니다. 여기서 Android와 마찬가지로 Client ID를 숨기고 싶었는데 제가 iOS는 개발 경험이 적다 보니 방법을 찾지는 못했습니다… 😅 결국 위 이미지와 같이 진행했습니다.

설정을 완료하여 빌드를 했는데 빌드에 실패하였습니다.

위와 같은 에러가 발생했는데 원인을 찾기위해 검색을 해봤습니다.

위 블로그를 보면 3가지 스텝으로 해결 방안이 나오는데 저는 3번째 스텝만 했을 때 해결이 되었습니다.

Podfile 파일 하단에 아래와 같이 되어 있습니다.

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

이 부분을 아래와 같이 변경합니다.

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |build_configuration|
build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386'
end
end
end

이렇게 한 후에 다시 빌드 해 보았더니 빌드가 완료되고 앱이 정상적으로 실행되었습니다. 혹시 이렇게 해서 해결이 안되시는 분은 스탭1, 2 도 진행 해보시면 될 듯 합니다.

5. Flutter 네이버 지도 생성

Flutter 에서 네이버 지도를 보여주려면 NaverMap 위젯을 이용하면 됩니다.

NaverMap(
initialCameraPosition: initPosition,
onMapCreated: (NaverMapController controller) {
_controller.complete(controller);
},
mapType: MapType.Basic,
pathOverlays: pathOverlays,
circles: circles.toList(),
polygons: polygons.toList(),
markers: markers.toList(),
onMapTap: (LatLng latLng) async {
final NaverMapController controller = await _controller.future;
controller.moveCamera(CameraUpdate.scrollTo(latLng), animationDuration: 2);

setState(() {
markers.add(Marker(markerId: latLng.toString(), position: latLng));
});
},
useSurface: kReleaseMode,
),

6. 직선 그리기(PathOverlay) API

구글 지도에서는 Polyline 라는 명칭인데 네이버 지도는 PathOverlay 라는 명칭을 사용합니다. 실제 사용 방법은 거의 비슷합니다.

Set<PathOverlay> pathOverlays = {};

이렇게 선언 해 준 후에 버튼이 선택되면 직선을 하나 그리도록 아래와 같이 구현 했습니다.

ElevatedButton(
child: const Text('직선'),
onPressed: () {
List<LatLng> list = const [
LatLng(37.3625806, 126.9248464),
LatLng(37.3626138, 126.9264801),
LatLng(37.3632727, 126.9280313),
];

setState(() {
clearMap();

pathOverlays.add(PathOverlay(
PathOverlayId("1"),
list,
width: 4,
color: Colors.red,
outlineColor: Colors.white,
));

fitBounds(list);
});
},
),

pathOverlays 변수에 PathOverlay 하나를 추가하였습니다. 만약 여러개의 선을 그린다면 add를 계속 해주면 됩니다.

직선 그리기 실행 결과

7. 원 그리기(Circle) API

원 그리기는 구글 지도와 거의 유사합니다.

Set<CircleOverlay> circles = {};

이렇게 선언 해 준 후에 버튼을 누르면 원을 하나 그리도록 구현했습니다.

ElevatedButton(
child: const Text('원'),
onPressed: () {
LatLng center = const LatLng(37.3626138, 126.9264801);

clearMap();

setState(() {
circles.add(CircleOverlay(
overlayId: center.toString(),
center: center,
radius: 35.0,
color: Colors.transparent,
outlineColor: Colors.red,
outlineWidth: 4,
));
});
},
),
원 그리기 실행 결과

8. 다각형 그리기

다각형 그리기 역시 구글 지도 다룰 때와 크게 다르지 않습니다. 일반적으로 Polygon 을 구현하는 방법과 구멍(?)이 뚫려있는 형태로 Polygon을 구현하는 방법입니다.

8–1. Polygon(Multi Polygon)

Set<PolygonOverlay> polygons = {};

이렇게 polygons 변수를 선언해 줍니다. 그 후에 다각형을 두 개 그리도록 아래와 같이 구현했습니다.

ElevatedButton(
child: const Text('다각형'),
onPressed: () {
List<LatLng> polygon1 = const [
LatLng(37.3625806, 126.9248464),
LatLng(37.3626138, 126.9264801),
LatLng(37.3632727, 126.9280313),
];

List<LatLng> polygon2 = const [
LatLng(37.36119, 126.9193982),
LatLng(37.3534215, 126.9295909),
LatLng(37.3549206, 126.9327015),
];

setState(() {
clearMap();

polygons.add(PolygonOverlay(
polygon1.toString(),
polygon1,
color: Colors.transparent,
outlineColor: Colors.red,
outlineWidth: 4,
));
polygons.add(PolygonOverlay(
polygon2.toString(),
polygon2,
color: Colors.transparent,
outlineColor: Colors.red,
outlineWidth: 4,
));

fitBounds([...polygon1, ...polygon2]);
});
},
),

8–2. Polygon(Multi Polygon) Hole 구현

ElevatedButton(
child: const Text('다각형-반전'),
onPressed: () {
List<LatLng> polygon1 = const [
LatLng(37.3625806, 126.9248464),
LatLng(37.3626138, 126.9264801),
LatLng(37.3632727, 126.9280313),
];

List<LatLng> polygon2 = const [
LatLng(37.36119, 126.9193982),
LatLng(37.3534215, 126.9295909),
LatLng(37.3549206, 126.9327015),
];

setState(() {
clearMap();

polygons.add(PolygonOverlay(
polygon1.toString(),
createOuterBounds(),
holes: [polygon1, polygon2],
color: Colors.black38,
outlineColor: Colors.red,
outlineWidth: 4,
));

fitBounds([...polygon1, ...polygon2]);
});
},
),

구글 지도에서와 같이 createOuterBounds라는 함수를 만들어서 지도 전체를 색칠했습니다. 후에 다각형을 그리는 부분을 hole 영역으로 구멍을 내서 구현했습니다.

List<LatLng> createOuterBounds() {
double delta = 0.01;

List<LatLng> list = [];

list.add(LatLng(90 - delta, -180 + delta));
list.add(LatLng(0, -180 + delta));
list.add(LatLng(-90 + delta, -180 + delta));
list.add(LatLng(-90 + delta, 0));
list.add(LatLng(-90 + delta, 180 - delta));
list.add(LatLng(0, 180 - delta));
list.add(LatLng(90 - delta, 180 - delta));
list.add(LatLng(90 - delta, 0));
list.add(LatLng(90 - delta, -180 + delta));

return list;
}
다각형-반전 그리기 실행 결과

9. 마커 그리기(Marker) API

마커 그리는 부분도 구글 지도와 거의 유사합니다. 저는 지난 번과 마찬가지로 Set으로 선언했는데 같은 좌표에 여러개의 음식점이 있는 것 과 같은 상황일 때는 List로 선언해 주어야 합니다.

Set<Marker> markers = {};

그리고 지도를 선택한 부분에 마커를 그려주도록 했습니다.

onMapTap: (LatLng latLng) async {
final NaverMapController controller = await _controller.future;
controller.moveCamera(CameraUpdate.scrollTo(latLng), animationDuration: 2);

setState(() {
markers.add(Marker(markerId: latLng.toString(), position: latLng));
});
},

이렇게 하면 지도를 선택했을 때 해당 지점이 화면의 중앙으로 오면서 마커가 찍히게 됩니다.

마커 그리기 실행 결과

10. 마커 정보창(InfoWindow) API

InfoWindow는 마커를 터치하면 해당 마커의 정보를 상세하게 보여줄 때 사용합니다. InfoWindow는 마커를 선택하면 보이게 되어 있고 다른 마커를 선택하면 기존에 떠 있던 InfoWindow는 안보이게 되고 새로 선택한 마커에 InfoWindow 가 보이게 됩니다.

onMapTap: (LatLng latLng) async {
final NaverMapController controller = await _controller.future;
controller.moveCamera(CameraUpdate.scrollTo(latLng), animationDuration: 2);

setState(() {
markers.add(
Marker(markerId: latLng.toString(), position: latLng, infoWindow: "${latLng.latitude} / ${latLng.longitude}"),
);
});
},
마커 선택시 InfoWindow 실행 결과

11. 추가 기능

위 예시들에서 clearMap() 함수가 존재하는데 직선, 원, 다각형, 각형-반전 버튼을 선택하면 지도에 그려졌던 것을 전부 삭제 하도록 만든 함수 입니다. 아래와 같이 구현하였습니다.

clearMap() {
pathOverlay.clear();
circles.clear();
polygons.clear();
markers.clear();
}

도형이 그려진 모든 좌표들을 한 화면에 보여주도록 하기 위한 기능을 구현했습니다.

fitBounds(List<LatLng> bounds) async {
final NaverMapController controller = await _controller.future;
controller.moveCamera(CameraUpdate.fitBounds(LatLngBounds.fromLatLngList(bounds)), animationDuration: 2);
}

12. 전체소스

위 샘플을 github에 올려두었습니다.

프로젝트 받으셔서 실행시켜 보시면 어떻게 실행되는지 볼 수 있습니다. API KEY 관련 부분을 위 내용 참조 하셔서 작성해 주셔야 합니다.

13. 정리

구글지도를 사용하기 위해 설정 했던 것에 비교하면 쉽다고 볼 수 있지만 의도하지 않은 iOS 빌드시 발생 했던 문제는 저를 좀 당황하게 만들었습니다. 구글지도 포스팅하고 내용은 동일하게 구성하였습니다. 혹시 궁금한 점이 있다면 말씀 해 주세요! 제가 잘못 한 점도 있다면 피드백 해주세요! 저에게 큰 도움이 됩니다~!

감사합니다! 🙏🏻

--

--