Flutter에서 Google Maps 사용하기: 완벽 가이드 📍🗺️
모바일 앱 개발의 세계에서 지도 기능은 필수적인 요소로 자리 잡았습니다. 특히 Flutter를 사용하여 크로스 플랫폼 앱을 개발할 때, Google Maps의 통합은 매우 강력한 도구가 될 수 있습니다. 이 가이드에서는 Flutter 프로젝트에 Google Maps를 통합하는 방법부터 고급 기능 구현까지 상세히 다루겠습니다.
Flutter와 Google Maps의 조합은 위치 기반 서비스, 내비게이션 앱, 부동산 앱 등 다양한 분야에서 활용될 수 있습니다. 재능넷과 같은 플랫폼에서도 위치 기반 서비스를 통해 사용자 경험을 향상시킬 수 있죠. 이제 본격적으로 Flutter에서 Google Maps를 마스터하는 여정을 시작해봅시다! 🚀
1. Google Maps 플러그인 설정하기 🛠️
Flutter 프로젝트에 Google Maps를 추가하는 첫 단계는 필요한 플러그인을 설정하는 것입니다. 이 과정은 몇 가지 중요한 단계로 나눌 수 있습니다.
1.1 pubspec.yaml 파일 수정
먼저, 프로젝트의 pubspec.yaml
파일을 열고 다음 종속성을 추가합니다:
dependencies:
flutter:
sdk: flutter
google_maps_flutter: ^2.2.5
이 플러그인은 Flutter 애플리케이션에서 Google Maps를 사용할 수 있게 해주는 공식 패키지입니다.
1.2 API 키 얻기
Google Maps를 사용하려면 유효한 API 키가 필요합니다. API 키를 얻는 과정은 다음과 같습니다:
- Google Cloud Console에 접속합니다.
- 새 프로젝트를 생성하거나 기존 프로젝트를 선택합니다.
- 사이드바에서 'API 및 서비스' > '사용자 인증 정보'로 이동합니다.
- '사용자 인증 정보 만들기' > 'API 키'를 선택합니다.
- 생성된 API 키를 안전한 곳에 저장합니다.
1.3 플랫폼별 설정
Android와 iOS 플랫폼에서 Google Maps를 사용하기 위해서는 추가적인 설정이 필요합니다.
Android 설정:
android/app/src/main/AndroidManifest.xml
파일에 다음 줄을 추가합니다:
<manifest ...>
<application ...>
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY"/>
</application>
</manifest>
iOS 설정:
ios/Runner/AppDelegate.swift
파일에 다음 코드를 추가합니다:
import UIKit
import Flutter
import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR_API_KEY")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
이렇게 기본적인 설정이 완료되었습니다. 이제 Flutter 앱에서 Google Maps를 사용할 준비가 되었습니다! 🎉
2. 기본 지도 표시하기 🗺️
이제 Flutter 앱에 기본적인 Google Maps를 표시해보겠습니다. 이 과정은 간단하지만, 지도 기능의 기초가 되는 중요한 단계입니다.
2.1 GoogleMap 위젯 사용하기
Flutter에서 Google Maps를 표시하려면 GoogleMap
위젯을 사용합니다. 다음은 기본적인 사용 예시입니다:
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MapScreen extends StatefulWidget {
@override
_MapScreenState createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
GoogleMapController? mapController;
final LatLng _center = const LatLng(37.5665, 126.9780); // 서울의 위도와 경도
void _onMapCreated(GoogleMapController controller) {
mapController = controller;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Google Maps Example'),
),
body: GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: _center,
zoom: 11.0,
),
),
);
}
}
이 코드는 서울 중심부를 초기 위치로 하는 기본적인 Google Maps를 표시합니다.
2.2 지도 컨트롤러 활용하기
GoogleMapController
를 사용하면 프로그래밍 방식으로 지도를 제어할 수 있습니다. 예를 들어, 카메라 위치를 변경하거나 줌 레벨을 조정할 수 있습니다.
void _goToTheLake() {
final CameraPosition _kLake = CameraPosition(
bearing: 192.8334901395799,
target: LatLng(37.43296265331129, -122.08832357078792),
tilt: 59.440717697143555,
zoom: 19.151926040649414);
mapController?.animateCamera(CameraUpdate.newCameraPosition(_kLake));
}
이 함수를 버튼에 연결하면, 사용자가 버튼을 탭할 때 지도가 특정 위치로 부드럽게 이동합니다.
2.3 지도 유형 변경하기
Google Maps는 여러 가지 지도 유형을 제공합니다. mapType
속성을 사용하여 지도 유형을 변경할 수 있습니다:
GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: _center,
zoom: 11.0,
),
mapType: MapType.hybrid, // 위성 이미지와 도로를 함께 표시
)
사용 가능한 지도 유형은 다음과 같습니다:
MapType.normal
: 기본 도로 지도MapType.satellite
: 위성 이미지MapType.hybrid
: 위성 이미지와 도로를 함께 표시MapType.terrain
: 지형 지도
이렇게 기본적인 Google Maps 통합이 완료되었습니다. 이제 사용자들에게 직관적이고 인터랙티브한 지도 경험을 제공할 수 있게 되었습니다! 🌍✨
3. 마커와 정보 창 추가하기 📍💬
지도에 마커를 추가하면 특정 위치나 관심 지점을 표시할 수 있습니다. 또한, 정보 창을 사용하여 각 마커에 대한 추가 정보를 제공할 수 있습니다. 이 기능들은 사용자 경험을 크게 향상시킬 수 있습니다.
3.1 마커 추가하기
마커를 추가하려면 Marker
객체를 생성하고 GoogleMap
위젯의 markers
속성에 추가합니다:
class _MapScreenState extends State<MapScreen> {
Set<Marker> _markers = {};
@override
void initState() {
super.initState();
_markers.add(
Marker(
markerId: MarkerId('seoul_tower'),
position: LatLng(37.5511, 126.9882),
infoWindow: InfoWindow(title: 'N서울타워'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: CameraPosition(
target: _center,
zoom: 11.0,
),
markers: _markers,
),
);
}
}
이 코드는 N서울타워의 위치에 마커를 추가합니다.
3.2 커스텀 마커 아이콘 사용하기
기본 마커 아이콘 대신 커스텀 아이콘을 사용하여 앱의 디자인을 개선할 수 있습니다:
BitmapDescriptor customIcon;
@override
void initState() {
super.initState();
BitmapDescriptor.fromAssetImage(
ImageConfiguration(size: Size(48, 48)),
'assets/custom_marker.png'
).then((icon) {
setState(() {
customIcon = icon;
});
});
}
// 마커 추가 시
Marker(
markerId: MarkerId('custom_marker'),
position: LatLng(37.5511, 126.9882),
icon: customIcon,
)
이 방법을 사용하면 앱의 브랜딩에 맞는 독특한 마커를 만들 수 있습니다.
3.3 정보 창 커스터마이징
기본 정보 창은 제한적이므로, 커스텀 정보 창을 만들어 더 많은 정보와 상호작용을 제공할 수 있습니다:
class CustomInfoWindow extends StatelessWidget {
final String title;
final String snippet;
CustomInfoWindow({required this.title, required this.snippet});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(color: Colors.black26, blurRadius: 5),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(title, style: TextStyle(fontWeight: FontWeight.bold)),
Text(snippet),
ElevatedButton(
child: Text('자세히 보기'),
onPressed: () {
// 상세 정보 페이지로 이동
},
),
],
),
);
}
}
이 커스텀 정보 창을 사용하려면, GoogleMap
위젯에 infoWindowBuilder
속성을 추가합니다:
GoogleMap(
// ... 다른 속성들 ...
markers: _markers,
infoWindowBuilder: (BuildContext context, Marker marker) {
return CustomInfoWindow(
title: marker.infoWindow.title ?? '',
snippet: marker.infoWindow.snippet ?? '',
);
},
)
이렇게 하면 마커를 탭했을 때 커스텀 정보 창이 표시됩니다.
3.4 마커 클러스터링
많은 수의 마커가 있을 경우, 마커 클러스터링을 사용하여 지도를 더 깔끔하게 만들 수 있습니다. Flutter에서는 flutter_map_marker_cluster
패키지를 사용할 수 있습니다:
import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
// ...
MarkerClusterLayerWidget(
options: MarkerClusterLayerOptions(
maxClusterRadius: 120,
size: Size(40, 40),
fitBoundsOptions: FitBoundsOptions(
padding: EdgeInsets.all(50),
),
markers: _markers.toList(),
builder: (context, markers) {
return FloatingActionButton(
child: Text(markers.length.toString()),
onPressed: null,
);
},
),
)
이 기능을 사용하면 가까이 있는 여러 마커가 하나의 클러스터로 그룹화되어 표시됩니다.
마커와 정보 창은 지도 상에서 중요한 정보를 전달하는 핵심 요소입니다. 이를 효과적으로 활용하면 사용자에게 더 풍부하고 유용한 지도 경험을 제공할 수 있습니다. 특히 재능넷과 같은 플랫폼에서는 이러한 기능을 통해 서비스 제공자의 위치나 이벤트 장소 등을 효과적으로 표시할 수 있겠죠! 🎯🗺️
4. 사용자 위치 및 권한 처리하기 📍🔐
Google Maps를 활용하는 많은 앱에서 사용자의 현재 위치를 표시하고 활용하는 것은 매우 중요한 기능입니다. 하지만 이를 위해서는 적절한 권한 처리가 필요합니다. 이 섹션에서는 Flutter 앱에서 사용자의 위치를 얻고, 필요한 권한을 요청하는 방법에 대해 알아보겠습니다.
4.1 위치 권한 요청하기
사용자의 위치 정보를 얻기 위해서는 먼저 권한을 요청해야 합니다. 이를 위해 permission_handler
패키지를 사용할 수 있습니다.
먼저, pubspec.yaml
파일에 다음 종속성을 추가합니다:
dependencies:
permission_handler: ^10.0.0
그런 다음, 다음과 같이 권한을 요청할 수 있습니다:
import 'package:permission_handler/permission_handler.dart';
Future<void> requestLocationPermission() async {
final status = await Permission.location.request();
if (status.isGranted) {
// 권한이 허용됨
print("위치 권한이 허용되었습니다.");
} else if (status.isDenied) {
// 권한이 거부됨
print("위치 권한이 거부되었습니다. 일부 기능이 제한될 수 있습니다.");
} else if (status.isPermanentlyDenied) {
// 권한이 영구적으로 거부됨
openAppSettings(); // 사용자를 앱 설정 페이지로 안내
}
}
이 함수를 앱 시작 시 또는 위치 기능을 사용하기 전에 호출하면 됩니다.
4.2 현재 위치 가져오기
권한을 얻은 후, geolocator
패키지를 사용하여 현재 위치를 가져올 수 있습니다. pubspec.yaml
에 다음을 추가합니다:
dependencies:
geolocator: ^9.0.0
그리고 다음과 같이 현재 위치를 가져올 수 있습니다:
import 'package:geolocator/geolocator.dart';
Future<Position> getCurrentLocation() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
return Future.error('위치 서비스가 비활성화되어 있습니다.');
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
return Future.error('위치 권한이 거부되었습니다.');
}
}
if (permission == LocationPermission.deniedForever) {
return Future.error('위치 권한이 영구적으로 거부되었습니다. 설정에서 변경해주세요.');
}
return await Geolocator.getCurrentPosition();
}
4.3 지도에 현재 위치 표시하기
이제 얻은 위치 정보를 지도에 표시해 봅시다:
class _MapScreenState extends State<MapScreen> {
GoogleMapController? mapController;
Position? _currentPosition;
@override
void initState() {
super.initState();
_getCurrentLocation();
}
void _getCurrentLocation() async {
try {
Position position = await getCurrentLocation();
setState(() {
_currentPosition = position;
});
_goToCurrentLocation();
} catch (e) {
print("Error: $e");
}
}
void _goToCurrentLocation() {
if (_currentPosition != null && mapController != null) {
mapController!.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(_currentPosition!.latitude, _currentPosition!.longitude),
zoom: 15,
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(37.5665, 126.9780), // 서울의 위도와 경도
zoom: 11.0,
),
myLocationEnabled: true,
myLocationButtonEnabled: true,
),
);
}
}
이 코드는 지도를 생성하고, 현재 위치를 가져온 후 그 위치로 카메라를 이동시킵니다. 또한 myLocationEnabled
와 myLocationButtonEnabled
속성을 통해 사용자의 현재 위치를 파란 점으로 표시하고, 현재 위치로 이동하는 버튼을 제공합니다.
4.4 위치 업데이트 받기
사용자의 위치가 변경될 때마다 업데이트를 받고 싶다면, Geolocator
의 스트림을 사용할 수 있습니다:
late StreamSubscription<Position> _positionStreamSubscription;
@override
void initState() {
super.initState();
_startLocationUpdates();
}
void _startLocationUpdates() {
const LocationSettings locationSettings = LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10,
);
_positionStreamSubscription = Geolocator.getPositionStream(locationSettings: locationSettings).listen(
(Position position) {
setState(() {
_currentPosition = position;
});
_updateMarkerPosition(position);
}
);
}
void _updateMarkerPosition(Position position) {
// 마커 위치 업데이트 로직
}
@override
void dispose() {
_positionStreamSubscription.cancel();
super.dispose();
}
이 코드는 사용자의 위치가 10미터 이상 변경될 때마다 업데이트를 받습니다. 이를 통해 실시간으로 사용자의 위치를 추적할 수 있습니다.
사용자의 위치를 활용하는 것은 많은 앱에서 핵심적인 기능입니다. 특히 재능넷과 같은 서비스에서는 주변의 서비스 제공자를 찾거나, 이벤트 장소까지의 경로를 안내하는 등 다양한 방식으로 활용될 수 있습니다. 하지만 항상 사용자의 프라이버시를 존중하고, 필요한 경우에만 위치 정보를 사용하는 것이 중요합니다. 또한, 위치 정보 사용에 대한 명확한 설명과 함께 사용자의 동의를 구하는 것이 좋은 관행입니다. 🔒📍
5. 지오코딩과 역지오코딩 구현하기 🏙️🔍
지오코딩과 역지오코딩은 지도 기반 애플리케이션에서 매우 유용한 기능입니다. 지오코딩은 주소를 위도와 경도로 변환하는 과정이며, 역지오코딩은 그 반대로 위도와 경도를 주소로 변환하는 과정입니다. Flutter에서 이러한 기능을 구현하는 방법을 살펴보겠습니다.
5.1 Google Maps Geocoding API 설정
먼저, Google Cloud Console에서 Geocoding API를 활성화해야 합니다. 이 API는 지오코딩과 역지오코딩 모두에 사용됩니다.
- Google Cloud Console에 로그인합니다.
- 'API 및 서비스' 대시보드로 이동합니다.
- 'API 및 서비스 사용' 버튼을 클릭합니다.
- 검색창에 'Geocoding API'를 입력하고 선택합니다.
- '사용' 버튼을 클릭하여 API를 활성화합니다.
5.2 HTTP 패키지 추가
API 요청을 보내기 위해 http
패키지를 사용할 것입니다. pubspec.yaml
파일에 다음을 추가합니다:
dependencies:
http: ^0.13.5
5.3 지오코딩 구현
주소를 위도와 경도로 변환하는 함수를 만들어 봅시다:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<Map<String, double>> getCoordinatesFromAddress(String address) async {
final apiKey = 'YOUR_API_KEY';
final encodedAddress = Uri.encodeComponent(address);
final url = 'https://maps.googleapis.com/maps/api/geocode/json?address=$encodedAddress&key=$apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final decodedResponse = json.decode(response.body);
if (decodedResponse['status'] == 'OK') {
final location = decodedResponse['results'][0]['geometry']['location'];
return {
'latitude': location['lat'],
'longitude': location['lng'],
};
} else {
throw Exception('Geocoding failed: ${decodedResponse['status']}');
}
} else {
throw Exception('Failed to load geocoding data');
}
}
이 함수를 사용하여 주소를 좌표로 변환할 수 있습니다:
try {
final coordinates = await getCoordinatesFromAddress('서울특별시 중구 세종대로 110');
print('위도: ${coordinates['latitude']}, 경도: ${coordinates['longitude']}');
} catch (e) {
print('Error: $e');
}
5.4 역지오코딩 구현
이번에는 위도와 경도를 주소로 변환하는 함수를 만들어 보겠습니다:
Future<String> getAddressFromCoordinates(double latitude, double longitude) async {
final apiKey = 'YOUR_API_KEY';
final url = 'https://maps.googleapis.com/maps/api/geocode/json?latlng=$latitude,$longitude&key=$apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final decodedResponse = json.decode(response.body);
if (decodedResponse['status'] == 'OK') {
return decodedResponse['results'][0]['formatted_address'];
} else {
throw Exception('Reverse geocoding failed: ${decodedResponse['status']}');
}
} else {
throw Exception('Failed to load reverse geocoding data');
}
}
이 함수를 사용하여 좌표를 주소로 변환할 수 있습니다:
try {
final address = await getAddressFromCoordinates(37.5665, 126.9780);
print('주소: $address');
} catch (e) {
print('Error: $e');
}
5.5 사용자 인터페이스에 통합하기
이제 이 기능들을 사용자 인터페이스에 통합해 봅시다. 예를 들어, 사용자가 주소를 입력하면 지도에 해당 위치를 표시하고, 지도에서 위치를 선택하면 해당 주소를 표시하는 기능을 만들 수 있습니다:
class MapScreen extends StatefulWidget {
@override
_MapScreenState createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
GoogleMapController? mapController;
final TextEditingController _addressController = TextEditingController();
String _selectedAddress = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('지도 예제')),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _addressController,
decoration: InputDecoration(
labelText: '주소 입력',
suffixIcon: IconButton(
icon: Icon(Icons.search),
onPressed: _searchAddress,
),
),
),
),
Expanded(
child: GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(37.5665, 126.9780),
zoom: 11.0,
),
onTap: _handleTap,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text('선택된 주소: $_selectedAddress'),
),
],
),
);
}
void _searchAddress() async {
try {
final coordinates = await getCoordinatesFromAddress(_addressController.text);
mapController?.animateCamera(CameraUpdate.newLatLngZoom(
LatLng(coordinates['latitude']!, coordinates['longitude']!),
15,
));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('주소를 찾을 수 없습니다: $e')),
);
}
}
void _handleTap(LatLng point) async {
try {
final address = await getAddressFromCoordinates(point.latitude, point.longitude);
setState(() {
_selectedAddress = address;
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('주소를 가져올 수 없습니다: $e')),
);
}
}
}
이 예제에서는 사용자가 주소를 입력하고 검색 버튼을 누르면 해당 위치로 지도가 이동합니다. 또한 사용자가 지도에서 위치를 탭하면 해당 위치의 주소가 화면 하단에 표시됩니다.
5.6 성능 및 사용량 고려사항
- API 호출 제한: Google Maps Platform은 일일 및 초당 요청 제한이 있습니다. 대량의 요청이 예상되는 경우 적절한 계획을 세워야 합니다.
- 캐싱: 자주 사용되는 주소나 좌표에 대한 결과를 캐싱하여 API 호출을 줄이고 성능을 향상시킬 수 있습니다.
- 에러 처리: 네트워크 오류, API 한도 초과 등 다양한 오류 상황에 대비한 적절한 에러 처리가 필요합니다.
- 사용자 경험: 지오코딩 과정에서 지연이 발생할 수 있으므로, 로딩 인디케이터 등을 사용하여 사용자에게 피드백을 제공하는 것이 좋습니다.
지오코딩과 역지오코딩 기능은 위치 기반 서비스에서 매우 중요한 역할을 합니다. 재능넷과 같은 플랫폼에서는 이러한 기능을 활용하여 서비스 제공자의 위치를 쉽게 찾거나, 사용자가 선택한 위치의 정확한 주소를 표시하는 등 다양한 방식으로 사용자 경험을 향상시킬 수 있습니다. 또한 이 기능을 통해 위치 기반 검색, 거리 계산, 경로 안내 등 더 복잡한 기능을 구현할 수 있는 기반을 마련할 수 있습니다. 🗺️🏙️
6. 경로 그리기 및 내비게이션 🚗🛣️
Google Maps를 활용한 앱에서 경로 그리기와 내비게이션 기능은 사용자에게 큰 가치를 제공합니다. 이 섹션에서는 Flutter 앱에서 두 지점 사이의 경로를 그리고, 간단한 내비게이션 기능을 구현하는 방법을 알아보겠습니다.
6.1 Google Directions API 설정
경로를 가져오기 위해 Google Directions API를 사용할 것입니다. Google Cloud Console에서 이 API를 활성화해야 합니다:
- Google Cloud Console에 로그인합니다.
- 'API 및 서비스' 대시보드로 이동합니다.
- 'API 및 서비스 사용' 버튼을 클릭합니다.
- 검색창에 'Directions API'를 입력하고 선택합니다.
- '사용' 버튼을 클릭하여 API를 활성화합니다.
6.2 경로 데이터 가져오기
두 지점 사이의 경로 데이터를 가져오는 함수를 만들어 봅시다:
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<List<LatLng>> getRouteCoordinates(LatLng origin, LatLng destination) async {
final String apiKey = 'YOUR_API_KEY';
final String url = 'https://maps.googleapis.com/maps/api/directions/json?origin=${origin.latitude},${origin.longitude}&destination=${destination.latitude},${destination.longitude}&key=$apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final decodedResponse = json.decode(response.body);
final List<LatLng> routeCoordinates = [];
if (decodedResponse['status'] == 'OK') {
final route = decodedResponse['routes'][0]['overview_polyline']['points'];
routeCoordinates.addAll(_decodePolyline(route));
} else {
throw Exception('Failed to fetch route');
}
return routeCoordinates;
} else {
throw Exception('Failed to fetch route');
}
}
List<LatLng> _decodePolyline(String encoded) {
List<LatLng> poly = [];
int index = 0, len = encoded.length;
int lat = 0, lng = 0;
while (index < len) {
int b, shift = 0, result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.codeUnitAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
int dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1));
lng += dlng;
LatLng p = LatLng((lat / 1E5).toDouble(), (lng / 1E5).toDouble());
poly.add(p);
}
return poly;
}
6.3 지도에 경로 그리기
이제 가져온 경로 데이터를 지도에 그려봅시다:
class _MapScreenState extends State<MapScreen> {
GoogleMapController? mapController;
Set<Polyline> _polylines = {};
LatLng _origin = LatLng(37.5665, 126.9780); // 서울
LatLng _destination = LatLng(35.1796, 129.0756); // 부산
@override
void initState() {
super.initState();
_getRoute();
}
void _getRoute() async {
final coordinates = await getRouteCoordinates(_origin, _destination);
setState(() {
_polylines.add(Polyline(
polylineId: PolylineId('route'),
points: coordinates,
color: Colors.blue,
width: 5,
));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('경로 안내')),
body: GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: _origin,
zoom: 7,
),
polylines: _polylines,
markers: {
Marker(markerId: MarkerId('origin'), position: _origin),
Marker(markerId: MarkerId('destination'), position: _destination),
},
),
);
}
}
이 코드는 서울에서 부산까지의 경로를 지도에 파란색 선으로 그립니다.
6.4 턴바이턴 내비게이션 구현
실제 내비게이션 앱처럼 턴바이턴 안내를 구현해 봅시다:
class _MapScreenState extends State<MapScreen> {
GoogleMapController? mapController;
Set<Polyline> _polylines = {};
LatLng _origin = LatLng(37.5665, 126.9780);
LatLng _destination = LatLng(35.1796, 129.0756);
List<String> _instructions = [];
int _currentStep = 0;
@override
void initState() {
super.initState();
_getRouteWithInstructions();
}
void _getRouteWithInstructions() async {
final String apiKey = 'YOUR_API_KEY';
final String url = 'https://maps.googleapis.com/maps/api/directions/json?origin=${_origin.latitude},${_origin.longitude}&destination=${_destination.latitude},${_destination.longitude}&key=$apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final decodedResponse = json.decode(response.body);
if (decodedResponse['status'] == 'OK') {
final List<LatLng> routeCoordinates = [];
final route = decodedResponse['routes'][0]['overview_polyline']['points'];
routeCoordinates.addAll(_decodePolyline(route));
final steps = decodedResponse['routes'][0]['legs'][0]['steps'];
final List<String> instructions = [];
for (var step in steps) {
instructions.add(step['html_instructions']);
}
setState(() {
_polylines.add(Polyline(
polylineId: PolylineId('route'),
points: routeCoordinates,
color: Colors.blue,
width: 5,
));
_instructions = instructions;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('턴바이턴 내비게이션')),
body: Column(
children: [
Expanded(
child: GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: _origin,
zoom: 7,
),
polylines: _polylines,
markers: {
Marker(markerId: MarkerId('origin'), position: _origin),
Marker(markerId: MarkerId('destination'), position: _destination),
},
),
),
Container(
height: 100,
child: Column(
children: [
Text(_instructions.isNotEmpty ? _instructions[_currentStep] : ''),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
if (_currentStep > 0) {
setState(() {
_currentStep--;
});
}
},
child: Text('이전'),
),
ElevatedButton(
onPressed: () {
if (_currentStep < _instructions.length - 1) {
setState(() {
_currentStep++;
});
}
},
child: Text('다음'),
),
],
),
],
),
),
],
),
);
}
}
이 코드는 경로의 각 단계에 대한 안내 지침을 가져와 화면 하단에 표시합니다. 사용자는 '이전'과 '다음' 버튼을 사용하여 단계별로 안내를 볼 수 있습니다.
6.5 실시간 위치 추적 및 경로 업데이트
실제 내비게이션 앱처럼 사용자의 실시간 위치를 추적하고 경로를 업데이트하려면, 이전에 다룬 위치 추적 기능과 결합해야 합니다:
class _MapScreenState extends State<MapScreen> {
GoogleMapController? mapController;
Set<Polyline> _polylines = {};
LatLng _origin = LatLng(37.5665, 126.9780);
LatLng _destination = LatLng(35.1796, 129.0756);
List<String> _instructions = [];
int _currentStep = 0;
LatLng? _currentPosition;
StreamSubscription<Position>? _positionStreamSubscription;
@override
void initState() {
super.initState();
_getRouteWithInstructions();
_startLocationUpdates();
}
void _startLocationUpdates() {
const LocationSettings locationSettings = LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10,
);
_positionStreamSubscription = Geolocator.getPositionStream(locationSettings: locationSettings).listen(
(Position position) {
setState(() {
_currentPosition = LatLng(position.latitude, position.longitude);
});
_updateRouteIfNeeded();
}
);
}
void _updateRouteIfNeeded() {
if (_currentPosition != null) {
// 현재 위치가 경로에서 크게 벗어났는지 확인
// 필요한 경우 _getRouteWithInstructions()를 다시 호출하여 경로 업데이트
}
}
@override
void dispose() {
_positionStreamSubscription?.cancel();
super.dispose();
}
// ... (이전 코드와 동일)
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('실시간 내비게이션')),
body: Column(
children: [
Expanded(
child: GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: _origin,
zoom: 7,
),
polylines: _polylines,
markers: {
Marker(markerId: MarkerId('origin'), position: _origin),
Marker(markerId: MarkerId('destination'), position: _destination),
if (_currentPosition != null)
Marker(markerId: MarkerId('current'), position: _currentPosition!),
},
myLocationEnabled: true,
myLocationButtonEnabled: true,
),
),
// ... (이전 코드와 동일)
],
),
);
}
}
이 코드는 사용자의 실시간 위치를 추적하고 지도에 표시합니다. 또한 필요한 경우 경로를 다시 계산할 수 있는 기반을 제공합니다.
경로 그리기와 내비게이션 기능은 위치 기반 서비스의 핵심 요소입니다. 재능넷과 같은 플랫폼에서는 이러한 기능을 활용하여 서비스 제공자의 위치로 안내하거나, 이벤트 장소까지의 경로를 제공하는 등 사용자 경험을 크게 향상시킬 수 있습니다. 또한 이 기능은 배달 서비스, 여행 계획 앱, 부동산 투어 등 다양한 분야에서 활용될 수 있습니다. 🚗🗺️
7. 성능 최적화 및 베스트 프랙티스 🚀💡
Google Maps를 Flutter 앱에 통합할 때, 성능 최적화와 베스트 프랙티스를 따르는 것이 중요합니다. 이를 통해 앱의 반응성을 높이고 사용자 경험을 개선할 수 있습니다. 다음은 몇 가지 주요 최적화 기법과 베스트 프랙티스입니다.
7.1 마커 클러스터링
많은 수의 마커를 표시해야 할 때, 마커 클러스터링을 사용하면 성능을 크게 향상시킬 수 있습니다. flutter_map_marker_cluster
패키지를 사용하여 구현할 수 있습니다:
import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
// ...
MarkerClusterLayerWidget(
options: MarkerClusterLayerOptions(
maxClusterRadius: 120,
size: Size(40, 40),
fitBoundsOptions: FitBoundsOptions(
padding: EdgeInsets.all(50),
),
markers: _markers,
builder: (context, markers) {
return FloatingActionButton(
child: Text(markers.length.toString()),
onPressed: null,
);
},
),
)
7.2 지연 로딩 및 페이징
대량의 데이터를 처리할 때는 지연 로딩과 페이징을 사용하세요. 예를 들어, 사용자가 지도를 이동할 때마다 해당 영역의 데이터만 로드할 수 있습니다:
GoogleMap(
onCameraMove: (CameraPosition position) {
// 새로운 영역의 데이터 로드
_loadMarkersInVisibleRegion(position.target, position.zoom);
},
// ...
)
void _loadMarkersInVisibleRegion(LatLng center, double zoom) {
// 서버에서 해당 영역의 마커 데이터를 가져오는 로직
// ...
}
7.3 캐싱 전략
자주 사용되는 데이터는 로컬에 캐시하여 네트워크 요청을 줄이세요. shared_preferences
또는 hive
패키지를 사용할 수 있습니다:
import 'package:shared_preferences/shared_preferences.dart';
Future<void> cacheMapData(String key, String data) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(key, data);
}
Future<String?> getCachedMapData(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}
7.4 이미지 최적화
커스텀 마커 아이콘을 사용할 때는 이미지 크기를 최적화하세요. 큰 이미지는 메모리 사용량을 증가시키고 렌더링 성능을 저하시킬 수 있습니다:
BitmapDescriptor.fromAssetImage(
ImageConfiguration(size: Size(48, 48)),
'assets/marker_icon.png',
).then((icon) {
// 최적화된 크기의 아이콘 사용
});
7.5 비동기 처리
무거운 작업은 비동기적으로 처리하여 UI 스레드를 차단하지 않도록 하세요:
Future<void> _loadMapData() async {
setState(() {
_isLoading = true;
});
try {
final data = await compute(parseMapData, await fetchMapData());
setState(() {
_mapData = data;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
7.6 메모리 관리
큰 데이터 세트를 다룰 때는 메모리 사용량에 주의하세요. 필요하지 않은 데이터는 적절히 해제하고, 가능한 경우 스트림을 사용하여 대량의 데이터를 처리하세요:
class _MapScreenState extends State<MapScreen> {
StreamSubscription? _dataSubscription;
@override
void initState() {
super.initState();
_dataSubscription = streamMapData().listen((data) {
// 데이터 처리
});
}
@override
void dispose() {
_dataSubscription?.cancel();
super.dispose();
}
// ...
}
7.7 에러 처리 및 폴백 메커니즘
네트워크 오류나 API 한도 초과 등의 상황에 대비한 적절한 에러 처리와 폴백 메커니즘을 구현하세요:
try {
final result = await getMapData();
// 결과 처리
} catch (e) {
if (e is NetworkError) {
// 네트워크 오류 처리
showOfflineMap();
} else if (e is QuotaExceededError) {
// API 한도 초과 처리
useAlternativeMapProvider();
} else {
// 기타 오류 처리
showErrorMessage(e.toString());
}
}
7.8 테스트 및 모니터링
정기적으로 성능 테스트를 실시하고, 실제 사용자 환경에서의 성능을 모니터링하세요. Firebase Performance Monitoring과 같은 도구를 사용할 수 있습니다:
import 'package:firebase_performance/firebase_performance.dart';
final Trace myTrace = FirebasePerformance.instance.newTrace("map_load_trace");
await myTrace.start();
// 측정하고자 하는 작업 수행
await loadMapData();
await myTrace.stop();
이러한 최적화 기법과 베스트 프랙티스를 적용하면, Google Maps를 사용하는 Flutter 앱의 성능을 크게 향상시킬 수 있습니다. 특히 재능넷과 같이 많은 사용자와 데이터를 다루는 플랫폼에서는 이러한 최적화가 필수적입니다. 사용자들에게 부드럽고 반응성 좋은 지도 경험을 제공함으로써, 앱의 전반적인 품질과 사용자 만족도를 높일 수 있습니다. 🚀📱