
마커(Marker)란
구글 맵에서 중요한 장소나 관심있는 위치를 표시하는 용도로 사용합니다.
- 위치 표시: 정확한 위도, 경도로 특정 장소를 가리킴
- 정보 제공: 마커를 탭하면 그 장소에 대한 정보를 보여줄 수 있음.
- 개인화: 사용자가 직접 원하는 곳에 마커를 추가하고 저장할 수 있습니다.
Marker 객체의 구조
Marker(
markerId: MarkerId('unique_id'), // 마커 구분용 고유 ID
position: LatLng(37.5665, 126.9780), // 마커가 표시될 위치
infoWindow: InfoWindow( // 마커를 탭했을 때 나타나는 정보창
title: '서울시청',
snippet: '서울특별시의 중심',
),
)
여러 개의 마커를 관리하려면 Set<Marker> 타입을 권장
Set<Marker> _markers = {}; // 마커들을 담을 집합
왜 List가 아닌 Set일까?
- 중복 방지: 같은 ID의 마커가 여러 개 생기는 걸 막아줌.
- 효율적인 관리: 마커 추가/삭제가 빠름.
1단계 예제에 마커 객체만 올려 보기

1단계 - 모델링 과 캐시 기능 추가해보기 , 팩토리 생성자 사용해보기
// 저장 시킬 마커 정보를 담는 클래스 (추후 확장 가능)
class SavedMaker {
final String id;
final String name;
final double latitude;
final double longitude;
// GSP 좌표 정보 ...
// 생성자에는 ?? if 리터키워드가 없다.
// _ <-- 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;
}
// SavedMaker 를 SharedPreferences 에 저장 할 형태로 사용하기 위해 설계
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'latitude': latitude,
'longitude': longitude,
};
}
// Map<String, dynamic> map = SavedMaker().toMap();
}
StorageService 서비스 클래스 구현 - 마커 CRUD 담당
// SharedPreferences 를 가공해서 관리하는 서비스 클래스
import 'dart:convert';
import 'package:class_google_map_v1/models/saved_marker.dart';
import 'package:shared_preferences/shared_preferences.dart';
class StorageService {
// SharedPreferences - 저장 키
static const String _markerKey = "saved_markers";
// C R U D
/// SavedMarker 들을 객체를 저장하는 기능 설계
Future<bool> saveMarkers(List<SavedMaker> markers) async {
try {
final prefs = await SharedPreferences.getInstance();
// [ SavedMarker(),SavedMarker(), SavedMarker(), ]
// "key1" : 1, "key2" : "abc", "key3" : true, "key4" : SavedMarker (x)
// 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"}, {'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 markersJson = prefs.getString(_markerKey);
if (markersJson != null) {
final List<dynamic> markersList = jsonDecode(markersJson);
// [ {'id' : "1"}, {'id' : "2"} ]
// 내부에 데이터 타입(형태를 변경할 때 쓰는 녀석)
return markersList.map((e) => SavedMaker.fromMap(e)).toList();
// [ SavedMaker(), SavedMaker() ] 변경 됨
}
return [];
} catch (e) {
return [];
}
}
/// 모든 SavedMarker 를 삭제하는 기능 추가
Future<bool> clearAllMarkers() async {
try {
final prefs = await SharedPreferences.getInstance();
// "saved_markers" : "[ {'id' : "1"}, {'id' : "3"}, {'id' : "3"},]"
return await prefs.remove(_markerKey);
} catch (e) {
return false;
}
}
/// 1단계 마커 객체 하나는 추가하는 코드를 작성해 보시요 (중복 체크)
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 saveMarkers(markers);
} catch (e) {
return false;
}
}
/// 특정 ID 값으로 마커객체 하나를 삭제하는 코드를 만들어 보시오.
}
저장시 데이터 구조의 이해 확인
// SavedMarker 리스트 형태
List<SavedMarker> markers = [
SavedMarker(
id: 'marker_001',
title: '스타벅스 강남점',
latitude: 37.4979,
longitude: 127.0276,
),
SavedMarker(
id: 'marker_002',
title: '롯데월드타워',
latitude: 37.5125,
longitude: 127.1025,
),
];
---------------------------------------------
// markers.map((marker) => marker.toMap()).toList()
// SavedMarker 객체를 map 형태로 변환하고 그 map 를 리스로 감싼는 형태의 자료 구조
[
{
'id': 'marker_001',
'title': '스타벅스 강남점',
'latitude': 37.4979,
'longitude': 127.0276,
},
{
'id': 'marker_002',
'title': '롯데월드타워',
'latitude': 37.5125,
'longitude': 127.1025,
}
]
----------------------------------------------------
// jsonEncode(....) 값 부분을 문자열 형태로 변환(json 형식)
"saved_markers" : "[{\"id\":\"marker_001\",\"title\":\"스타벅스 강남점\"...}]"
2단계 - 마커 저장 및 불러오기 및 전체 삭제 기능 추가


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),
);
}
}
}
'Flutter' 카테고리의 다른 글
| 블로그 프로젝트 - 디자인 시안, 구조, 주소 수정 (0) | 2025.08.20 |
|---|---|
| MVVM 패턴과 상태 관리 (0) | 2025.08.20 |
| SharedPreferences (0) | 2025.08.20 |
| 구글 Map API 사용 (0) | 2025.08.20 |
| 카메라 및 이미지 다루기 4단계 (0) | 2025.08.20 |