Google Maps with AngularDart
Google Maps with AngularDart
이 글에서는 Google Maps를 AngularDart 애플리케이션에 통합해 볼 거예요. 앱 자체는 아주 간단해요. 지도 위에서 선택한 두 마커 사이의 대원 거리(great circle distance, 구 표면에서의 최단 거리)를 계산하는 앱이에요.
본문
이 글에서는 Google Maps를 AngularDart 애플리케이션에 통합해 볼 거예요. 앱 자체는 아주 간단해요. 지도 위에서 고른 두 마커 사이의 대원 거리(great circle distance, 구의 표면에서의 최단 거리)를 계산하는 앱이에요. 그러면서 여러분은 다음을 할 수 있어요:
- 자신만의 Google Maps API 키를 등록하기
- 뼈대만 있는(barebones) Angular 웹 애플리케이션 만들기
- Dart를 Google Maps JavaScript API와 통합하고, 지도 상호작용 처리하기
- Angular 컴포넌트를 다듬는 몇 가지 팁 배우기
이 글은 코드보다 대략 네 배는 깁니다. 원한다면 그냥 전체 소스 코드와 최종 데모를 확인해도 돼요.
Google Maps JavaScript API
Google Maps JavaScript API는 개발자가 자신의 사이트에 Google Maps를 삽입하고 통합할 수 있게 해 줘요. 표시되는 지도의 콘텐츠와 처리를 자신만의 이미지, 데이터, 처리를 사용해 커스터마이즈할 수 있어요. Google Maps API로 개발을 시작하려면 합리적인 양의 사용을 허용하는 무료 API 키를 등록해야 해요. 애플리케이션이 성장하면 유료 플랜으로 업그레이드할 수 있어요. JavaScript API 페이지를 방문해서, 페이지 상단에서 GET A KEY를 클릭해요. 프로젝트 이름을 만들고 CREATE AND ENABLE API를 클릭해요. 키는 몇 초 안에 활성화되어 사용할 준비가 돼요. API 키를 기록해 두세요. Maps JavaScript 라이브러리를 가져올 때 그 키를 사용할 거예요:
https://maps.googleapis.com/maps/api/js?key=KEY_GOES_HERE
JavaScript에서 지도 인스턴스를 만들고 그 위에 마커를 배치하려면, 다음 코드를 쓸 거예요:
var hostElement = document.getElementById('map-id');
var map = new google.maps.Map(hostElement, {
zoom: 2,
center: { lat: 47.4979, lng: 19.0402 }
});
var marker = new google.maps.Marker({
position: { lat: 47.4979, lng: 19.0402 },
map: map,
label: 'A'
});
나중에 보시겠지만, Dart 코드는 이것과 매우 비슷해요(Dart가 주는 모든 추가 이점과 함께요).
AngularDart 애플리케이션
AngularDart 애플리케이션을 시작하는 가장 쉬운 방법은 Get Started 가이드를 따라 WebStorm이나 IntelliJ IDEA Community Edition에서 새 프로젝트를 만드는 거예요. 다른 에디터를 쓰거나 우리 예제 코드의 구조를 그냥 따라가고 싶다면, 여기 최소한의 것이 있어요.
pubspec 파일, pubspec.yaml:
name: google_maps_angular_dart
version: 0.0.1
description: Angular application with Google Maps integration
environment:
sdk: '>=1.19.0 <2.0.0'
dependencies:
angular2: ^2.2.0
dev_dependencies:
dart_to_js_script_rewriter: ^1.0.1
transformers:
- angular2:
platform_directives:
- 'package:angular2/common.dart#COMMON_DIRECTIVES'
platform_pipes:
- 'package:angular2/common.dart#COMMON_PIPES'
entry_points: web/main.dart
- dart_to_js_script_rewriter
호스트 페이지, web/index.html:
<!DOCTYPE html>
<html>
<head>
<!-- other headers -->
<script defer src="main.dart" type="application/dart"></script>
</head>
<body>
<map-control></map-control>
</body>
</html>
앱의 진입점, web/main.dart:
import 'package:angular2/platform/browser.dart';
import 'package:google_maps_angular_dart/component/map_control.dart';
void main() {
bootstrap(MapControl);
}
<map-control> 컴포넌트의 Dart 파일, lib/component/map_control.dart:
import 'package:angular2/core.dart';
@Component(
selector: 'map-control',
template: '{{distance}}',
)
class MapControl {
String distance = 'no distance yet';
}
위 코드는 별로 하는 일이 없지만 시작이에요. pub serve를 사용하고 나서 Dartium에서 페이지를 열면 실행할 수 있어요:
$ pub serve
Loading source assets...
Loading angular2 and dart_to_js_script_rewriter transformers...
Serving google_maps_angular_dart web on http://localhost:8080
Build completed successfully
Google Maps 통합
Google Maps 통합을 시작하려면, web/index.html에 다음 script 태그를 넣어요. API 키를 설정해야 한다는 점에 주의하세요:
<script src="https://maps.googleapis.com/maps/api/js?key=[YOUR_KEY_HERE]"></script>
다행히도 pub에는 바로 사용할 수 있는 Google Maps Dart 패키지가 있어요. pubspec.yaml에 추가해요:
dependencies:
angular2: ^2.2.0
google_maps: ^3.0.0
그런 다음 pub get을 실행해 패키지를 내려받아요. 컴포넌트 템플릿에 지도 영역용 호스트 요소를 만들어야 해요. 기본 스타일링으로 시작하고, 다음 단계에서 요소를 식별하기 위해 #mapArea 앵커를 사용할 거예요:
<div style="width: 300px; height: 300px" #mapArea>[map]</div>
컴포넌트 코드에서 요소의 참조를 이렇게 주입할 수 있어요:
@ViewChild('mapArea') ElementRef mapAreaRef;
요소 참조는 MapControl 클래스가 생성된 직후에는 사용할 수 없으니, Angular의 라이프사이클 콜백에 연결해야 해요:
class MapControl implements AfterViewInit {
@override
void ngAfterViewInit() {
// mapAreaRef is available now
}
}
팁: IDE가 메서드 본문을 대신 작성해 주게 해 보세요. 예를 들어 IntelliJ에서 CMD + N(또는 CTRL + N)을 누르고 "Implement Methods…" 메뉴 항목을 선택하면 돼요. 그러면 누락된 메서드를 고를 수 있고, 여러분은 메서드 본문만 신경 쓰면 돼요. 다음 코드에서 보듯이 Dart API는 JavaScript와 아주 비슷하지만, 타입 검사가 된다는 추가 이점이 있어요.
class MapControl implements AfterViewInit {
// ...
@override
void ngAfterViewInit() {
GMap map = new GMap(
mapAreaRef.nativeElement,
new MapOptions()
..zoom = 2
..center = new LatLng(47.4979, 19.0402) // ^ Budapest, Hungary
);
new Marker(new MarkerOptions()
..map = map
..position = new LatLng(47.4979, 19.0402)
..label = 'A');
}
}
타입 검사의 이점을 보여주는 한 예로, 이벤트를 들을 때 속성에 어떻게 접근할지 생각할 필요가 없다는 점이 있어요. 예를 들어 IDE는 마커의 위치를 담고 있는 MouseEvent 속성(latLng)을 찾는 데 도움을 줘요. 다음은 마커를 드래그하는 것에 반응하는 코드예요:
marker.onDrag.listen((MouseEvent event) {
print('New location while dragging: ${event.latLng}');
});
지도에서 클릭 이벤트를 잡는 것도 비슷해요:
map.onClick.listen((MouseEvent event) {
print('User clicked on position: ${event.latLng}');
});
모두 한데 모으기
두 좌표 사이의 거리를 측정하기 위해, 지도 위에서 두 마커를 추적할 거예요. 사용자는 마커를 드래그하거나 클릭해 배치할 수 있어야 해요. 지도와 마커를 필드로 추적해야 해요:
GMap _map;
Marker _aMarker;
Marker _bMarker;
초기화를 업데이트해서 지도 참조를 저장하고 클릭 핸들러를 등록해요. 클릭 핸들러는 마커 위치와 거리를 갱신해요:
@override
void ngAfterViewInit() {
_map = new GMap(
mapAreaRef.nativeElement,
new MapOptions()
..zoom = 2
..center = new LatLng(47.4979, 19.0402) // ^ Budapest, Hungary
);
_map.onClick.listen((MouseEvent event) {
_updatePosition(event.latLng);
_updateDistance();
});
}
클릭 핸들러의 첫 번째 부분에 대한 코드는 이래요:
void _updatePosition(LatLng position) {
if (_aMarker == null) {
_aMarker = _createMarker(_map, 'A', position);
} else if (_bMarker == null) {
_bMarker = _createMarker(_map, 'B', position);
} else {
_aMarker.position = _bMarker.position;
_bMarker.position = position;
}
}
마커 인스턴스화 코드는 앞선 예시와 비슷해요:
Marker _createMarker(GMap map, String label, LatLng position) {
final Marker marker = new Marker(new MarkerOptions()
..map = map
..draggable = true
..label = label
..position = position);
marker.onDrag.listen((MouseEvent event) {
_updateDistance();
});
return marker;
}
dart:math의 유틸리티 함수 덕분에 대원 거리 계산의 수학을 처리하고, 그 값을 거리 필드에 설정할 수 있어요:
/// Radius of the earth in km.
const int radiusOfEarth = 6371;
double _toRadian(num degree) => degree * PI / 180.0;
void _updateDistance() {
if (_aMarker == null || _bMarker == null) return;
LatLng a = _aMarker.position;
LatLng b = _bMarker.position;
double dLat = _toRadian(b.lat - a.lat);
double sLat = pow(sin(dLat / 2), 2);
double dLng = _toRadian(b.lng - a.lng);
double sLng = pow(sin(dLng / 2), 2);
double cosALat = cos(_toRadian(a.lat));
double cosBLat = cos(_toRadian(b.lat));
double x = sLat + cosALat * cosBLat * sLng;
double d = 2 * atan2(sqrt(x), sqrt(1 - x)) * radiusOfEarth;
distance = '${d.round()} km';
}
여러분이 친척, 유명한 장소, 랜드마크에서 얼마나 멀리 살고 있는지 궁금해한 적 있나요? 이제 앱을 시험해 보고 직접 확인해 볼 시간이에요. 제 집은 Mountain View에 있는 Google 본사에서 9815km 떨어져 있더라고요.
애플리케이션 다듬기
이 시점에서 Google Maps 통합은 완성됐어요. 지도 이벤트를 듣고 반응하며 지도 위에 객체를 만들고 있어요. 마지막 섹션에서는 우리 데모에 멋진 마무리를 더해 줄 기능을 구현할 거예요.
템플릿과 스타일 분리하기
복잡한 UI 템플릿을 별도의 .html 파일에 넣는 것은 좋은 관행이에요. CSS 스타일도 같은 방식으로 할 수 있어요:
@Component(
selector: 'map-control',
templateUrl: 'map_control.html',
styleUrls: const <String>['map_control.css'])
class MapControl implements AfterViewInit {
지도 요소는 CSS 클래스 map-area를 가져요:
<div class="map-area" #mapArea>[map]</div>
우리의 CSS 파일은 이렇게 간단할 수 있어요:
.map-area {
width: 500px;
height: 400px;
margin: 10px;
}
거리 단위 처리하기
어떤 사람들은 km ⟷ miles 변환에 능숙하지만, 나머지 사람들은 거리 단위를 고를 수 있는 드롭다운을 원해요. HTML 템플릿에서 드롭다운은 unit 필드에 모델 바인딩을 가진 간단한 <SELECT> 요소일 수 있어요.
<label>Unit:</label>
<select [(ngModel)]="unit">
<option value="km">km</option>
<option value="miles">miles</option>
</select>
Dart 코드에서는 단위를 저장하고, 단위가 갱신될 때마다 거리를 업데이트하길 원해요:
String _unit = 'km';
String get unit => _unit;
set unit(String value) {
_unit = value;
_updateDistance();
}
그리고 이전에 하드코딩한 거리 계산의 km를 업데이트하는 것도 잊지 마세요:
/// Const value to convert from km to miles.
const double milesPerKm = 0.621371;
// ... same code as earlier
if (unit == 'miles') {
d *= milesPerKm;
}
distance = '${d.round()} $unit';
좌표 형식화하기
마커의 위치를 공개하고 싶다면 어떻게 할까요? 가장 쉬운 방법은 새 위치 값을 Dart로 노출해서, 템플릿에서 {{a}}와 {{b}}를 쓸 수 있게 하는 거예요:
// Expose the position values.
LatLng get a => _aMarker?.position;
LatLng get b => _bMarker?.position;
템플릿은 마커가 아직 초기화되지 않았을 때 라벨을 숨겨서, null 값으로 인한 잠재적 문제를 막을 수 있어요:
<div *ngIf="a != null">A: {{a}}</div>
<div *ngIf="b != null">B: {{b}}</div>
하지만 {{a}}는 LatLng.toString()을 호출하는 것으로 변환되고, 아주 긴 double 값 두 개를 주게 돼요. 반면 몇 자리 숫자면 충분하죠. 한 가지 해결책은 템플릿에서 파이프(pipe)를 쓰는 거예요:
<div *ngIf="a != null">A: {{a.lat | number : '1.4-4'}}, {{a.lng | number : '1.4-4'}}</div>
<div *ngIf="b != null">B: {{b.lat | number : '1.4-4'}}, {{b.lng | number : '1.4-4'}}</div>
템플릿 문법 가이드는 더 나은 테스트를 위해 그 로직을 컨트롤러 클래스에 넣을 것을 제안해요:
/// Formatted position of the 'A' marker.
String get aPosition => _formatPosition(a);
/// Formatted position of the 'B' marker.
String get bPosition => _formatPosition(b);
String _formatPosition(LatLng pos) {
if (pos == null) return null;
return '${pos.lat.toStringAsFixed(4)}, '
'${pos.lng.toStringAsFixed(4)}';
}
그렇게 하면 템플릿은 훨씬 더 단순해질 수 있어요:
<div *ngIf="a != null">A: {{aPosition}}</div>
<div *ngIf="b != null">B: {{bPosition}}</div>
템플릿 정리하기
마지막 단계로, 템플릿에 남은 코드 부분을 모두 컨트롤러로 옮겨요:
/// Whether the 'A' marker's positions should be shown
bool get showA => a != null;
/// Whether the 'B' marker's positions should be shown
bool get showB => b != null;
/// Whether the 'distance' label should be shown
bool get showDistance => distance != null;
이렇게 하면 템플릿은 getter만 참조해요:
<div *ngIf="showA">A: {{aPosition}}</div>
<div *ngIf="showB">B: {{bPosition}}</div>
<p *ngIf="showDistance">
Distance: {{distance}}<br/>
</p>
마무리하며
보시다시피 Google Maps와의 양방향 통합을 구현하는 건 쉬워요. 소스 코드가 깔끔하고 읽기 쉽거든요. Dart 툴체인의 완전한 지원 덕분에, 다른 곳에서 무엇을 깨뜨릴지 걱정할 것 없이 쉽게 확장할 수 있어요. 독자를 위한 연습 문제로, Dart에서 같은 API를 사용해 지도에 히트맵(heatmap) 시각화를 추가해 보세요. 모든 가능성에 대해 google_maps 패키지를 확인해 보세요.