Google Map 관련 필수 플러그인
google_maps_flutter
Flutter에서 Google 지도 기능을 앱 내에 직접 통합할 수 있게 해주는 공식 플러그인입니다. Android와 iOS 플랫폼 모두에서 사용할 수 있으며, Flutter 앱에 지도 위젯을 삽입하고 다양한 지도 상호작용을 구현할 수 있습니다.
- 플러그인 이름: google_maps_flutter
- 플랫폼 지원: Android, iOS
- 공식 문서: https://pub.dev/packages/google_maps_flutter
google_maps_flutter | Flutter package
A Flutter plugin for integrating Google Maps in iOS and Android applications.
pub.dev
- Google API Console에서 Maps SDK for Android/iOS 를 반드시 활성화해야 합니다.
- 위치 권한은 직접 별도 패키지(geolocator, permission_handler)로 요청해야 합니다.
- Web 플랫폼은 지원하지 않습니다. Flutter Web용 지도는 flutter_map이나 WebView 방식 등으로 대체해야 합니다.
기능 | 설명 |
지도 위젯 | GoogleMap 위젯을 통해 지도 화면을 앱에 직접 표시 |
마커 | 원하는 위치에 Marker 객체를 사용해 핀 표시 가능 |
카메라 조작 | CameraUpdate로 지도 위치/줌을 애니메이션과 함께 이동 |
지도 스타일링 | JSON 기반 커스텀 지도 스타일 적용 가능 |
지도 타입 | 일반, 위성, 지형 등 다양한 타입 지원 |
사용자 위치 | 현재 위치 표시 및 사용자 위치 중심 이동 (단, location 권한 필요) |
제스처 지원 | 확대/축소, 회전, 드래그 등의 제스처 자동 지원 |
폴리라인/폴리곤/서클 | 지도 위에 선·영역을 그릴 수 있음 |
주요 위젯과 클래스
GoogleMap
지도를 표시하는 메인 위젯입니다.
GoogleMap(
initialCameraPosition: CameraPosition(
target: LatLng(37.5665, 126.9780), // 서울
zoom: 14,
),
onMapCreated: _onMapCreated,
markers: _markers,
polylines: _polylines,
)
CameraPosition / CameraUpdate
카메라 위치 초기화 및 이동용.
CameraUpdate.newLatLng(LatLng(37.0, 127.0))
Marker
지도에 위치 핀을 추가합니다.
Marker(
markerId: MarkerId('id'),
position: LatLng(37.0, 127.0),
infoWindow: InfoWindow(title: '위치 이름'),
)
Polyline, Polygon, Circle
지도 위에 도형을 그릴 때 사용합니다.
geolocator
Flutter에서 기기의 위치(위도/경도)를 가져오고, 위치 서비스 상태를 확인하고, 거리 계산까지 도와주는 강력한 위치 추적 플러그인입니다. 실시간 위치 추적, 권한 요청, 거리 계산 등에 자주 사용됩니다.
- 플러그인 이름: geolocator
- 지원 플랫폼: Android, iOS, Web, macOS, Windows, Linux
- 용도: 위치 정보 가져오기, 위치 권한 요청, 거리 계산 등
- 위치를 사용하려면 사용자의 명시적 권한 허용이 필요합니다.
- Android 12+에서는 권한 요청 흐름이 더 복잡하므로 permission_handler와 함께 사용하는 것을 권장합니다.
- 위치 정보는 민감한 정보이므로 프라이버시 정책도 함께 안내해야 합니다.
기능 | 설명 |
현재 위치 가져오기 | getCurrentPosition() 사용 |
위치 스트리밍 (실시간 추적) | getPositionStream() 사용 |
권한 상태 확인 및 요청 | checkPermission(), requestPermission() |
위치 서비스 활성 상태 확인 | isLocationServiceEnabled() |
거리 계산 | 두 지점 간 거리 계산: Geolocator.distanceBetween() |
속도, 고도 등 정보 포함 | 위치 객체에 포함되어 있음 |
주요 메서드와 예제
현재 위치 가져오기
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
위치 스트리밍 (이동할 때마다 콜백)
StreamSubscription<Position> positionStream = Geolocator.getPositionStream().listen(
(Position position) {
print(position.latitude);
},
);
권한 확인 및 요청
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
거리 계산
double distanceInMeters = Geolocator.distanceBetween(
37.5665, 126.9780, // 서울시청
35.1796, 129.0756 // 부산
);
활용 사례
- 실시간 배달 위치 추적
- 사용자의 현재 위치 기반 날씨 정보 제공
- 거리 기반 검색 (예: 주변 맛집, 주변 매장 등)
- 이동 경로 기록 및 거리 측정
flutter_google_maps_webservices
Flutter 앱에서 Google Maps Web Services API들을 쉽게 사용할 수 있도록 래핑해주는 Dart 패키지입니다.
이 패키지를 이용하면 Flutter에서 지도 화면 없이도 Google Places, Directions, Distance Matrix, Timezone 등의 다양한 지도 관련 정보를 HTTP로 직접 요청하여 활용할 수 있습니다.
- 패키지 이름: flutter_google_maps_webservices
- 의존 대상: Google Maps Platform Web Service APIs
- 지원 API 종류:
- Places API
- Directions API
- Distance Matrix API
- Timezone API
- Geocoding API (위도/경도 ↔ 주소 변환)
- 이 플러그인은 지도 UI를 포함하지 않습니다.
→ google_maps_flutter과 함께 사용하면 지도 UI + 경로 + 장소 검색 통합 가능 - Google Maps Web Services는 유료 정책(월 무료 할당량 초과 시 과금)임을 반드시 고려해야 합니다.
- 결과 언어는 language 파라미터를 통해 ko, en 등으로 지정 가능
주요 기능별 설명
1. Places API
- 주소 자동완성 (Autocomplete)
- 장소 ID로 상세 정보 가져오기
- 주변 장소 탐색 (Nearby Search)
final places = GoogleMapsPlaces(apiKey: 'YOUR_API_KEY');
final response = await places.autocomplete('강남역');
final detail = await places.getDetailsByPlaceId(response.predictions.first.placeId!);
2. Directions API
- A → B 간 최적 경로 정보 요청
final directions = GoogleMapsDirections(apiKey: 'YOUR_API_KEY');
final result = await directions.directionsWithLocation(
Location(37.5665, 126.9780), // 서울
Location(35.1796, 129.0756), // 부산
);
3. Distance Matrix API
- 여러 지점 간 거리/소요시간 계산
final matrix = GoogleDistanceMatrix(apiKey: 'YOUR_API_KEY');
final result = await matrix.distanceWithLocation(
[Location(37.5665, 126.9780)], // origin
[Location(35.1796, 129.0756)], // destination
);
4. Timezone API
- 위도/경도로 시간대 정보 조회
final timezone = GoogleMapsTimezone(apiKey: 'YOUR_API_KEY');
final tz = await timezone.getByLocation(Location(37.5665, 126.9780), DateTime.now());
Google Maps API Key 신청
Flutter에서 사용할 Google Maps API Key를 신청하려면 아래 단계대로 따라하시면 됩니다. Android/iOS 모두 동일한 Google Cloud Console에서 발급받으며, 발급 후 플랫폼에 따라 키를 등록해 줘야 합니다.
1. Google Cloud Console 접속
https://console.cloud.google.com/
Google 클라우드 플랫폼
로그인 Google 클라우드 플랫폼으로 이동
accounts.google.com
2. 새 프로젝트 만들기 (또는 기존 프로젝트 선택)
- 오른쪽 상단 프로젝트 선택 클릭
- [새 프로젝트] 클릭
- 프로젝트 이름 입력 → [만들기]
3. 필요한 API 활성화
Flutter 앱에서 지도를 표시하고 장소를 검색하려면 다음 API들을 활성화해야 합니다:
사용 기능 | 필요한 API 이름 |
지도 표시 | Maps SDK for Android, Maps SDK for iOS |
주소 검색 | Places API |
경로 표시 | Directions API |
거리/시간 계산 | Distance Matrix API |
위도/경도 ↔ 주소 | Geocoding API |
API 활성화 방법
- 좌측 메뉴 [API 및 서비스] → [라이브러리] 클릭
- 위 검색창에 API 이름 입력 (예: “Maps SDK for Android”)
- 클릭 후 [사용] 버튼 클릭
→ 모든 필요한 API에 대해 반복
API Key 생성
- 좌측 메뉴 [사용자 인증 정보] 클릭
- 상단 [사용자 인증 정보 만들기] → [API 키] 클릭
- 새 키가 생성되며 복사 가능함
Google Map App 예제 코드
pubspec.yaml
dependencies:
flutter:
sdk: flutter
google_maps_flutter: ^2.5.0
geolocator: ^11.0.0 # 위치 정보 가져오기용
permission_handler: ^11.3.0 # 위치 권한 요청
flutter_google_maps_webservices: ^1.1.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_launcher_icons: ^0.13.1
flutter_icons:
android: true
ios: false
image_path: "assets/app_icon.png"
app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 위치 권한 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="Andrew Google Map"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="GOOGLE API KEY"/>
...
</application>
</manifest>
main.dart
import 'package:flutter/material.dart';
import 'package:google_map/screen/map_screen.dart';
void main() {
runApp(const MapApp());
}
class MapApp extends StatelessWidget {
const MapApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Andrew Google Map',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MapScreen(),
);
}
}
/screen/map_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_google_maps_webservices/places.dart' as places_ws;
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
/// Google 지도 + 현재 위치 + 주소 검색
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => MapScreenState();
}
class MapScreenState extends State<MapScreen> {
late places_ws.GoogleMapsPlaces _places; // Google Places API 클라이언트
GoogleMapController? _mapController; // Google Map 컨트롤러
final TextEditingController _searchController = TextEditingController(); // 주소 입력 필드 컨트롤러
final Set<Marker> _markers = {}; // 지도에 표시할 마커들
static const String _currentMarkerId = 'current';
static const String _searchMarkerId = 'search';
// 앱 실행 시 초기 카메라 위치 (서울시청 기준)
static const CameraPosition _initialCameraPosition = CameraPosition(
target: LatLng(37.5665, 126.9780),
zoom: 12,
);
@override
void initState() {
super.initState();
_places = places_ws.GoogleMapsPlaces(apiKey: 'AIzaSyD6Spl76hr4f1UpFW3qiUQz773PuVxWMTo');
}
@override
void dispose() {
_searchController.dispose();
_places.dispose();
super.dispose();
}
void _onMapCreated(GoogleMapController controller) {
_mapController = controller;
}
/// 현재 위치로 카메라 이동 및 마커 추가
Future<void> _goToCurrentLocation() async {
if (!await Geolocator.isLocationServiceEnabled()) return;
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) permission = await Geolocator.requestPermission();
if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) return;
final pos = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final latLng = LatLng(pos.latitude, pos.longitude);
_mapController?.animateCamera(CameraUpdate.newLatLngZoom(latLng, 15));
setState(() {
_markers.removeWhere((m) => m.markerId.value == _currentMarkerId);
_markers.add(Marker(
markerId: const MarkerId(_currentMarkerId),
position: latLng,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure),
infoWindow: const InfoWindow(title: '현재 위치'),
));
});
}
/// 주소 자동완성 검색 및 결과 목록 표시
Future<void> _searchLocation() async {
final input = _searchController.text.trim();
if (input.isEmpty) return;
try {
final response = await _places.autocomplete(input);
if (response.isOkay && response.predictions.isNotEmpty) {
final preds = response.predictions;
if (preds.length == 1) {
_moveToPlace(preds.first.placeId!); // 하나일 경우 바로 이동
} else {
// 여러 개일 경우 리스트로 표시
showModalBottomSheet(
context: context,
builder: (_) => ListView.builder(
itemCount: preds.length,
itemBuilder: (ctx, i) {
final p = preds[i];
return ListTile(
title: Text(p.description ?? ''),
onTap: () {
Navigator.pop(ctx);
_moveToPlace(p.placeId!);
},
);
},
),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('검색 실패: ${response.errorMessage}')),
);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('오류 발생: $e')),
);
}
}
/// 장소 ID로 상세 조회 후 지도 이동 및 마커 표시
Future<void> _moveToPlace(String placeId) async {
try {
final detail = await _places.getDetailsByPlaceId(placeId);
if (!detail.isOkay) return;
final loc = detail.result.geometry!.location;
final latLng = LatLng(loc.lat, loc.lng);
_searchController.text = detail.result.name ?? '';
_mapController?.animateCamera(CameraUpdate.newLatLngZoom(latLng, 15));
setState(() {
_markers.removeWhere((m) => m.markerId.value == _searchMarkerId);
_markers.add(Marker(
markerId: const MarkerId(_searchMarkerId),
position: latLng,
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueViolet),
infoWindow: InfoWindow(title: detail.result.name),
));
});
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('장소 정보를 가져오는 중 오류 발생: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: '주소 검색',
hintStyle: TextStyle(color: Colors.black45),
border: InputBorder.none,
filled: true,
fillColor: Colors.white,
),
style: const TextStyle(color: Colors.black),
textInputAction: TextInputAction.search,
onSubmitted: (_) => _searchLocation(),
),
),
actions: [
IconButton(
icon: const Icon(Icons.search, color: Colors.black),
onPressed: _searchLocation,
)
],
),
body: GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _initialCameraPosition,
markers: _markers,
),
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: 'loc',
tooltip: '현재 위치',
onPressed: _goToCurrentLocation,
child: const Icon(Icons.my_location),
),
],
),
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
);
}
}
web/index.html
<!DOCTYPE html>
<html>
<head>
...
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY"></script>
</head>
...
</html>
Flutter 웹 사용시 JavaScript API 추가
Flutter Web 빌드 시 사용하는 web/index.html 파일 <head> 안에 아래 <script> 태그를 넣어 주세요.
YOUR_API_KEY는 Google Cloud Console에서 발급받은 웹용 API 키로 교체하세요.
'Flutter for Beginners' 카테고리의 다른 글
Flutter Gemini 앱 (0) | 2025.04.22 |
---|---|
Flutter 포토 스티커 (0) | 2025.04.21 |
Flutter 영상통화 (0) | 2025.04.15 |
Flutter 비디오 플레이어 (0) | 2025.04.10 |
Flutter 디지털 시계에 주사위 굴리기 추가 (0) | 2025.04.03 |