Flutter

구글 Map API 사용

whs5758 2025. 8. 20. 17:20

Google Maps API 키 발급

Google Cloud Console에서 API 키를 발급받는 과정은 다음과 같습니다.

  1. Google Cloud Console(https://console.cloud.google.com/)에 접속하여 로그인합니다.
  2. 새 프로젝트를 생성하고, Maps SDK for Android와 Maps SDK for iOS활성화합니다.
  3. 사용자 인증 정보 메뉴에서 API 키를 생성하고 복사해둡니다.

1. Google Maps API

  • google_maps_flutter 패키지: 이 패키지는 Flutter에서 Google Maps Platform을 편리하게 사용할 수 있도록 도와주는 '다리(Bridge)' 역할을 합니다.
  • 작동 방식: 이 패키지는 Dart 코드를 Android의 네이티브 Google Maps SDK for Android와 iOS의 네이티브 Google Maps SDK for iOS로 변환하여 실행합니다.

2. AGP 오류가 발생한다면 수정하기

gradle-wrapper.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
settings.gradle
plugins {
    id "dev.flutter.flutter-plugin-loader" version "1.0.0"
    id "com.android.application" version "8.5.0" apply false
    id "org.jetbrains.kotlin.android" version "1.9.0" apply false
}
AndroidManifest.xml
    <application
        android:label="dem_google_map"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

        <meta-data android:name="com.google.android.geo.API_KEY"
            android:value="AIzaSyCX0k01nqnZyCU9EKJ8fhtYVnkyt6YizgY"/>
yml
  cupertino_icons: ^1.0.8
  google_maps_flutter: ^2.5.0    # 구글 맵
  shared_preferences: ^2.2.2     # 로컬 저장

models/saved_marker.dart
// 저장시킬 마커 정보를 담는 클래스 (추후 확장 가능)

class SavedMaker {
  final String id;
  final String name;
  final double latitude;
  final double longitude;

  // _ <-- private <-- 외부에서 생성자 호출 방법이 없다.
  SavedMaker._internal({
    required this.id,
    required this.name,
    required this.latitude,
    required this.longitude,
  });

  // 1. static 저장소(캐시) 준비
  static final Map<String, SavedMaker> _cache = <String, SavedMaker>{};

  //  팩토링 생성자
  factory SavedMaker.fromMap(Map<String, dynamic> map) {
    String id = map["id"] as String;

    if (_cache.containsKey(id)) {
      print('캐시에 저장된 객체 리턴 및 활용');
      // map 구조에 있는 객체를 반환
      return _cache[id]!;
    }

    final newMarker = SavedMaker._internal(
      id: id,
      name: map["name"],
      latitude: map["latitude"],
      longitude: map["longitude"],
    );
    _cache[id] = newMarker;
    return newMarker;
  }

  // SavedMarker 를 SharedPreferences에 저장할 형태로 사용하기 위해 설계
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'latitude': latitude,
      'longitude': longitude,
    };
  }

  // Map<String, dynamic> map = SavedMarker().toMap();
}
screens/map_screen.dart
import 'package:class_google_map_v1/models/saved_maker.dart';
import 'package:class_google_map_v1/screens/marker_list_screen.dart';
import 'package:class_google_map_v1/services/storage_service.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';

class MapScreen extends StatefulWidget {
  const MapScreen({super.key});

  @override
  State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  // 구글맵 사용해보기 sdk 버전
  // 1단계 - 지도를 조작하기 위한 리모컨 필요 - GoogleMapController
  GoogleMapController? _controller;

  // 특정 위치로 부드럽게 이동 처리
  // 현재 줌 레벨 조정 1 ~ 20 (확대, 축소)
  // 1 - 전세계 지도. 15 - 도시 레벨
  // 화면의 중심 좌표 가져오기

  // 2 - 중심 좌표 객체 생성
  static const LatLng _initCenter = LatLng(35.162439, 129.062293);

  Set<Marker> _markerSet = {};
  final StorageService _storageService = StorageService();

  // SP에 저장되어 있던 데이터를 담을 자료구조
  List<SavedMaker> _savedMarkers = [];

  @override
  void initState() {
    super.initState();
    _loadMarkers();
  }

  /// 저장된 마커들을 불러오기
  Future<void> _loadMarkers() async {
    try {
      final List<SavedMaker> loadedMakers = await _storageService.loadMarkers();

      setState(() {
        _savedMarkers = loadedMakers;
        _markerSet.clear(); // 기존에 있던 마커를 다 제거

        for (SavedMaker m in loadedMakers) {
          _markerSet.add(
            Marker(
              markerId: MarkerId(m.id),
              position: LatLng(m.latitude, m.longitude),
              infoWindow: InfoWindow(
                title: m.name,
                snippet: "나의 장소",
              ),
            ),
          );
        }
      });
    } catch (e) {
      _showMessage('마커를 불러오는데 실패');
    }
  }

  // 스낵바 메세지 만들어보기
  void _showMessage(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        duration: const Duration(seconds: 1),
      ),
    );
  }

  void _onMapTapped(LatLng position) {
    final nameController = TextEditingController();
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('새 장소 추가'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: nameController,
                decoration: InputDecoration(
                  labelText: '장소 이름',
                  hintText: '장소 이름을 입력하세요',
                  border: OutlineInputBorder(),
                ),
                autofocus: true,
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.pop(context);
              },
              child: Text('취소'),
            ),
            TextButton(
              onPressed: () {
                Navigator.pop(context);
                _addMarker(position, nameController.text);
              },
              child: Text('추가'),
            )
          ],
        );
      },
    );
  }

  void _addMarker(LatLng position, String name) {
    // 고유 ID 생성
    final markerId = DateTime.now().millisecond.toString();

    // 새로운 SavedMarker 객체 생성
    final newMarker = SavedMaker.fromMap(
      {
        'id': markerId,
        'name': name,
        'latitude': position.latitude,
        'longitude': position.longitude,
      },
    );
    setState(() {
      // 새롭게 생성한 객체를 마커 List 자료구조에 넣기
      _savedMarkers.add(newMarker);
      // 지도에 마커 추가
      _markerSet.add(
        Marker(
            markerId: MarkerId(markerId),
            position: position,
            infoWindow: InfoWindow(
              title: name,
              snippet: '저장된 장소',
            )),
      );
    });
    // 로컬에 저장
    _saveMarkers();
  }

  Future<void> _saveMarkers() async {
    try {
      final success = await _storageService.saveMakers(_savedMarkers);
      if (success) {
        _showMessage("마커 저장 완료");
      } else {
        _showMessage("마커 저장 실패");
      }
    } catch (e) {
      _showMessage("마커 저장 실패");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.blue[400],
        foregroundColor: Colors.white,
        title: Text(
          '처음 만나는 구글 맵',
          style: TextStyle(
            fontWeight: FontWeight.bold,
          ),
        ),
        actions: [
          IconButton(
            onPressed: () {
              _showMarkerList();
            },
            icon: Icon(Icons.list),
          ),
          IconButton(
            onPressed: () async {
              await StorageService().clearAllMakers();
              setState(() {
                _markerSet = {};
                _savedMarkers = [];
              });
            },
            icon: Icon(Icons.delete_forever),
          ),
        ],
      ),
      // 3 - GoogleMap 위젯 사용
      // GoogleMap - 지도의 모든 것을 담을 수 있는 그릇
      body: GoogleMap(
        onMapCreated: (controller) {
          // controller 새롭게 생성된 지도 안에 생성한 GoogleController
          // 내 코드에서 사용하기 위해 멤버 변수로 선언
          // 주소값을 연결 시켜 두었다.
          _controller = controller;
        },
        initialCameraPosition: CameraPosition(
          target: _initCenter,
          zoom: 15,
        ),
        markers: _markerSet,
        onTap: _onMapTapped,
      ),
      bottomNavigationBar: Container(
        color: Colors.blue[200],
        padding: EdgeInsets.all(16.0),
        child: Text(
          '마커된 장소 ${_savedMarkers.length} 개',
          style: TextStyle(
            fontSize: 18.0,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    );
  }

  // 마커 목록 화면 표시 메서드
  void _showMarkerList() async {
    if (_savedMarkers.isEmpty) {
      _showMessage('저장된 장소가 없습니다');
    }

    /// 중요!! 화면 이동 후 코드가 여기서 멈춰져 있다. await
    // 저장된 마커들이 있다면 화면을 stack 구조로 올릴 예정
    // <> 타입 안정성을 위해 제네릭 사용
    final result =
        await Navigator.of(context).push<SavedMaker>(MaterialPageRoute(
      builder: (context) {
        return MarkerListScreen();
      },
    ));

    if (result != null) {
      // 리스트 화면에서 선택된 마커로 포커스를 이동하는 기능
      await _moveToMarker(result);
    }

    // 목록에서 돌아온 후 마커를 다시 로드 (새로고침 - 삭제된 마커가 있다면 반영)
    await _loadMarkers();
  }

  // 메서드 추가
  /// 코드로 특정 마커 위치로 이동하는 기능
  Future<void> _moveToMarker(SavedMaker marker) async {
    // 구글 맵에서 특정 포지션으로 이동시키는 기능은 _controller 담당
    if (_controller != null) {
      _controller!.animateCamera(
        CameraUpdate.newLatLngZoom(
            LatLng(marker.latitude, marker.longitude), 16.0),
      );
    }
  }
}
screens/market_list_screen.dart
import 'package:class_google_map_v1/models/saved_maker.dart';
import 'package:class_google_map_v1/services/storage_service.dart';
import 'package:flutter/material.dart';

class MarkerListScreen extends StatefulWidget {
  const MarkerListScreen({super.key});

  @override
  State<MarkerListScreen> createState() => _MarkerListScreenState();
}

class _MarkerListScreenState extends State<MarkerListScreen> {
  // 데이터가 필요
  List<SavedMaker> _markers = [];
  final StorageService _storageService = StorageService();

  @override
  void initState() {
    super.initState();
    // 저장된 마커 불러 오기
    _loadMarkers();
  }

  // 1.
  Future<void> _loadMarkers() async {
    final markers = await _storageService.loadMarkers();
    setState(() {
      _markers = markers;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('저장된 장소 ${_markers.length} 개'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),
      body: ListView.builder(
        itemCount: _markers.length,
        itemBuilder: (context, index) {
          final marker = _markers[index];
          return Card(
            margin: const EdgeInsets.all(12),
            child: ListTile(
              leading: Icon(Icons.place, color: Colors.blue),
              title: Text(marker.name),
              subtitle: Text('위도:${marker.latitude}\n경도:${marker.longitude}'),
              trailing: IconButton(
                onPressed: () async {
                  print('삭제');
                  final success = await _storageService.deleteMarker(marker.id);
                  if (success) {
                    _loadMarkers();
                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                        content: Text(
                      '${marker.id}가 삭제되었습니다',
                    )));
                  }
                },
                icon: Icon(
                  Icons.delete,
                  color: Colors.red,
                ),
              ),
              onTap: () {
                print('선택된 마커로 자동 이동 처리');
                // 화면 스택 구조 구글맵에서 --> 저장된 마커 리스트가 올라온 상태
                // 화면을 제거하고 선택된 마커값을 뒤 화면에 전달 시키는 기능
                Navigator.of(context).pop(marker);
              },
            ),
          );
        },
      ),
    );
  }
}
services/storage_service.dart
// SharedPreferences 를 가공해서 관리하는 서비스 클래스
import 'dart:convert';
import 'package:class_google_map_v1/models/saved_maker.dart';
import 'package:shared_preferences/shared_preferences.dart';

class StorageService {
  // SharedPreferences - 저장 키
  static const String _markerKey = "saved_markers";

  // C R U D
  /// SavedMarker 객체를 저장하는 기능 설계
  Future<bool> saveMakers(List<SavedMaker> markers) async {
    try {
      final prefs = await SharedPreferences.getInstance();

      // [SavedMarker(), SavedMarker(), SavedMarker()]
      // dart 에서 json 형식을 만들 때 map 구조에서 변경하는 것이 가장 편하다.
      // {'key1' : 1, 'key2' : "abc"} --> jsonEncode(map) --> {"key1" : 1, "key2" : "abc"}
      // 자료구조에서 map 메서드는 (내부 형태를 다른 형태로 바꿀 때 편하게 사용하는 메서드)
      final markersMapList = markers.map((e) => e.toMap()).toList();
      // {'id' : "1"}, {'id' : "3"}]; --> List<Map<>>
      // List<Map> 형태를 json 형식으로 변환
      final jsonString = jsonEncode(markersMapList);
      return await prefs.setString(_markerKey, jsonString); // true
    } catch (e) {
      return false;
    }
  }

  /// 저장된 마커 불러오기
  Future<List<SavedMaker>> loadMarkers() async {
    try {
      final prefs = await SharedPreferences.getInstance();

      // 저장되어 있는 JSON 형식의 문자열 가져오기
      final markerJson = prefs.getString(_markerKey);
      if (markerJson != null) {
        final List<dynamic> markersList = jsonDecode(markerJson);
        // [{'id' : "1"}, {'id' : "2"}]
        // 내부에 데이터 타입(형태를 변경할 때 쓰는 것)
        // e --> {'id' : "1"}
        return markersList.map((e) => SavedMaker.fromMap(e)).toList();
        // [ SavedMarker(), SavedMarker() ] 변경 됨
      }
      return [];
    } catch (e) {
      return [];
    }
  }

  /// 특정 id의 마커 삭제
  Future<bool> deleteMarker(String markerId) async {
    try {
      final markers = await loadMarkers();
      final updatedMarkers =
          // list.where()
          // --> 조건을 만족하는 요소들로만 새로운 컬렉션을 만들어주는 메서드
          markers.where((marker) => marker.id != markerId).toList();
      return await saveMakers(updatedMarkers);
    } catch (e) {
      print('마커 삭제 오류 $e');
      return false;
    }
  }

  /// 모든 SavedMarker 를 삭제하는 기능 추가
  Future<bool> clearAllMakers() async {
    try {
      final prefs = await SharedPreferences.getInstance();

      // "saved_markers" : "[{'id' : "1"}, {'id' : "2"}]"
      return await prefs.remove(_markerKey);
    } catch (e) {
      return false;
    }
  }

  /// 마커 객체 하나만 추가 하는 코드 (중복체크)
  Future<bool> addMarker(SavedMaker newMarker) async {
    try {
      // 1. {..} 데이터를 가져와야 한다.
      // 2. {..} + {..} 기존에 있던 데이터에 새롭게 추가 한다.
      // 3. SP에 다시 리스트 저장하기

      /// List 객체가 새롭게 추가 된 상태
      final List<SavedMaker> markers = await loadMarkers();

      /// 중복 체크 Stream API를 활용 하면 편리 하다.
      // 자료구조에 any() 메서드 사용 해보기
      /// markers.any() <-- 조건을 만족하는 요소가 하나라도 있으면 true 반환
      if (markers.any((marker) => marker.id == newMarker.id)) {
        print('이미 존재 하는 마커 입니다');
        return false;
      }
      // 새 마커를 리스트 자료구조에 추가 하는 코드
      markers.add(newMarker);

      // 리스트 객체를 다시 SharedPreferences에 저장 하는 코드
      return await saveMakers(markers);
    } catch (e) {
      return false;
    }
  }
}

 

main.dart
import 'package:class_google_map_v1/screens/map_screen.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '나의 지도',
      debugShowCheckedModeBanner: false,
      home: const MapScreen(),
    );
  }
}

 

'Flutter' 카테고리의 다른 글

구글 맵 3단계 마커 활용  (0) 2025.08.20
SharedPreferences  (0) 2025.08.20
카메라 및 이미지 다루기 4단계  (0) 2025.08.20
카메라 다루기 3단계  (0) 2025.08.20
카메라 다루기 2단계  (0) 2025.08.20