본문 바로가기
Flutter for Beginners

Flutter Google Map

by Records that rule memory 2025. 4. 18.
728x90

Google Map 관련 필수 플러그인

google_maps_flutter

Flutter에서 Google 지도 기능을 앱 내에 직접 통합할 수 있게 해주는 공식 플러그인입니다. Android와 iOS 플랫폼 모두에서 사용할 수 있으며, 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. 새 프로젝트 만들기 (또는 기존 프로젝트 선택)

  1. 오른쪽 상단 프로젝트 선택 클릭
  2. [새 프로젝트] 클릭
  3. 프로젝트 이름 입력 → [만들기]

3. 필요한 API 활성화

Flutter 앱에서 지도를 표시하고 장소를 검색하려면 다음 API들을 활성화해야 합니다:

사용 기능 필요한 API 이름
지도 표시 Maps SDK for Android, Maps SDK for iOS
주소 검색 Places API
경로 표시 Directions API
거리/시간 계산 Distance Matrix API
위도/경도 ↔ 주소 Geocoding API

API 활성화 방법

  1. 좌측 메뉴 [API 및 서비스] → [라이브러리] 클릭
  2. 위 검색창에 API 이름 입력 (예: “Maps SDK for Android”)
  3. 클릭 후 [사용] 버튼 클릭
    → 모든 필요한 API에 대해 반복

 API Key 생성

  1. 좌측 메뉴 [사용자 인증 정보] 클릭
  2. 상단 [사용자 인증 정보 만들기] → [API 키] 클릭
  3. 새 키가 생성되며 복사 가능함
728x90

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_icon.png
0.21MB

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(),
    );
  }
}

Google Map API 적용

/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 키로 교체하세요.

728x90

'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