Flutter ์ฑ์์ ์ง๋ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๊ฒ์ ์๊ฐ๋ณด๋ค ๊ฐ๋จํฉ๋๋ค. ์ค๋์ Google Maps API ํค ์ค์ ๋ถํฐ ์ค์ ์ง๋ ๊ตฌํ๊น์ง ์ฐจ๊ทผ์ฐจ๊ทผ ์์๋ณด๊ฒ ์ต๋๋ค.

1. Google Cloud Console์์ API ํค ๋ฐ๊ธ๋ฐ๊ธฐ
1-1. Google Cloud Console ์ ์
- Google Cloud Console์ ์ ์ํฉ๋๋ค
- ์ ํ๋ก์ ํธ๋ฅผ ์์ฑํ๊ฑฐ๋ ๊ธฐ์กด ํ๋ก์ ํธ๋ฅผ ์ ํํฉ๋๋ค
1-2. Maps API ํ์ฑํ
- ์ข์ธก ๋ฉ๋ด์์ "API ๋ฐ ์๋น์ค" → "๋ผ์ด๋ธ๋ฌ๋ฆฌ" ํด๋ฆญ
- "Maps SDK for Android" ๊ฒ์ ํ ํ์ฑํ
- "Maps JavaScript API"๋ ํจ๊ป ํ์ฑํ (์น์์๋ ์ฌ์ฉํ ๊ฒฝ์ฐ)
1-3. API ํค ์์ฑ
- "API ๋ฐ ์๋น์ค" → "์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด" ํด๋ฆญ
- "+ ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด ๋ง๋ค๊ธฐ" → "API ํค" ์ ํ
- ์์ฑ๋ API ํค๋ฅผ ๋ณต์ฌํด ๋ก๋๋ค
1-4. API ํค ์ ํ ์ค์ (์ ํ์ฌํญ)
๋ณด์์ ์ํด API ํค ์ฌ์ฉ์ ์ ํํ ์ ์์ต๋๋ค.
- ์์ฑ๋ API ํค ํด๋ฆญ
- "์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ฌํญ"์์ "Android ์ฑ" ์ ํ
- ํจํค์ง ์ด๋ฆ๊ณผ SHA-1 ์ธ์ฆ์ ์ง๋ฌธ ์ถ๊ฐ
2. Flutter ํ๋ก์ ํธ ์ค์
2-1. ์์กด์ฑ ์ถ๊ฐ
pubspec.yaml ํ์ผ์ ํ์ํ ํจํค์ง๋ฅผ ์ถ๊ฐํฉ๋๋ค.
dependencies:
flutter:
sdk: flutter
google_maps_flutter: ^2.5.0
geolocator: ^10.1.0
permission_handler: ^11.0.1
2-2. Android ๊ถํ ์ค์
android/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" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:label="your_app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity>
<!-- ๊ธฐ์กด activity ์ค์ -->
</activity>
<!-- Google Maps API ํค ์ถ๊ฐ - ์ค์! -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_API_KEY_HERE" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
3. ๊ธฐ๋ณธ ์ง๋ ํ๋ฉด ๊ตฌํ
3-1. ๊ธฐ๋ณธ MapScreen ์์ฑ
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
CameraPosition? _userPosition;
Set<Marker> _storeMarkers = {};
GoogleMapController? _mapController;
bool _isLoading = true;
@override
void initState() {
super.initState();
_initializeMap();
}
Future<void> _initializeMap() async {
try {
// ์์น ๊ถํ ํ์ธ
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
// ํ์ฌ ์์น ๊ฐ์ ธ์ค๊ธฐ
CameraPosition userPosition = await _getUserLocation();
Set<Marker> storeMarkers = await _getStoreMarkers(userPosition);
setState(() {
_userPosition = userPosition;
_storeMarkers = storeMarkers;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
print('์ง๋ ์ด๊ธฐํ ์คํจ: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('์ง๋๋ก ์ฐพ๊ธฐ'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _userPosition == null
? const Center(child: Text('์์น๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค'))
: GoogleMap(
initialCameraPosition: _userPosition!,
mapType: MapType.normal,
myLocationEnabled: true,
myLocationButtonEnabled: true,
markers: _storeMarkers,
onMapCreated: (GoogleMapController controller) {
_mapController = controller;
},
),
);
}
Future<CameraPosition> _getUserLocation() async {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
return CameraPosition(
target: LatLng(position.latitude, position.longitude),
zoom: 15,
);
}
Future<Set<Marker>> _getStoreMarkers(CameraPosition position) async {
Set<Marker> markers = {};
// ํ
์คํธ์ฉ ๋ง์ปค๋ค ์ถ๊ฐ
for (int i = 0; i < 5; i++) {
markers.add(
Marker(
markerId: MarkerId('store_$i'),
position: LatLng(
position.target.latitude + i * 0.001,
position.target.longitude + i * 0.001,
),
infoWindow: InfoWindow(
title: '๊ฐ๊ฒ ${i + 1}',
snippet: '๋ง์๋ ์์์ ',
),
),
);
}
return markers;
}
}
4. ์ง๋ ๋์์ธ ํ์ด์ง ๊ตฌํ
4-1. MapScreen ์์ฑ
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_fonts/google_fonts.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> with TickerProviderStateMixin {
CameraPosition? _userPosition;
Set<Marker> _storeMarkers = {};
GoogleMapController? _mapController;
bool _isLoading = true;
String _statusMessage = 'ํ์ฌ ์์น๋ฅผ ์ฐพ๋ ์ค...';
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
));
_initializeMap();
}
Future<void> _initializeMap() async {
try {
setState(() {
_statusMessage = '์์น ๊ถํ์ ํ์ธํ๋ ์ค...';
});
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
throw '์์น ๊ถํ์ด ๊ฑฐ๋ถ๋์์ต๋๋ค';
}
}
setState(() {
_statusMessage = 'ํ์ฌ ์์น๋ฅผ ๊ฐ์ ธ์ค๋ ์ค...';
});
CameraPosition userPosition = await _getUserLocation();
Set<Marker> storeMarkers = await _getStoreMarkers(userPosition);
setState(() {
_userPosition = userPosition;
_storeMarkers = storeMarkers;
_isLoading = false;
});
_animationController.forward();
} catch (e) {
setState(() {
_isLoading = false;
_statusMessage = '์ค๋ฅ: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFFF8F0),
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFFFFF3E0),
Color(0xFFFFE0B2),
Color(0xFFFFCC80),
],
),
),
child: SafeArea(
child: Column(
children: [
_buildCustomAppBar(),
Expanded(child: _buildMapContent()),
],
),
),
),
floatingActionButton: _buildFloatingButtons(),
);
}
Widget _buildCustomAppBar() {
return Container(
padding: const EdgeInsets.all(20),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.arrow_back_ios_new, size: 20),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'์ง๋๋ก ์ฐพ๊ธฐ',
style: GoogleFonts.jua(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.brown[700],
),
),
if (!_isLoading)
Text(
'์ฃผ๋ณ ${_storeMarkers.length}๊ฐ ๋งค์ฅ',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
),
],
),
);
}
Widget _buildMapContent() {
if (_isLoading) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(_statusMessage),
],
),
),
);
}
return FadeTransition(
opacity: _fadeAnimation,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: GoogleMap(
initialCameraPosition: _userPosition!,
mapType: MapType.normal,
myLocationEnabled: true,
myLocationButtonEnabled: false,
markers: _storeMarkers,
onMapCreated: (GoogleMapController controller) {
_mapController = controller;
},
),
),
),
);
}
Widget _buildFloatingButtons() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: "location",
onPressed: _moveToUserLocation,
backgroundColor: Colors.white,
child: const Icon(Icons.my_location),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: "zoom_in",
onPressed: () => _mapController?.animateCamera(CameraUpdate.zoomIn()),
backgroundColor: Colors.white,
mini: true,
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: "zoom_out",
onPressed: () => _mapController?.animateCamera(CameraUpdate.zoomOut()),
backgroundColor: Colors.white,
mini: true,
child: const Icon(Icons.remove),
),
],
);
}
Future<CameraPosition> _getUserLocation() async {
Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
return CameraPosition(
target: LatLng(position.latitude, position.longitude),
zoom: 15,
);
}
Future<Set<Marker>> _getStoreMarkers(CameraPosition position) async {
Set<Marker> markers = {};
for (int i = 0; i < 5; i++) {
markers.add(
Marker(
markerId: MarkerId('store_$i'),
position: LatLng(
position.target.latitude + i * 0.001,
position.target.longitude + i * 0.001,
),
infoWindow: InfoWindow(
title: '๊ฐ๊ฒ ${i + 1}',
snippet: '๋ง์๋ ์์์ ',
),
),
);
}
return markers;
}
void _moveToUserLocation() {
if (_userPosition != null && _mapController != null) {
_mapController!.animateCamera(
CameraUpdate.newCameraPosition(_userPosition!),
);
}
}
@override
void dispose() {
_animationController.dispose();
_mapController?.dispose();
super.dispose();
}
}
5. ์ถ๊ฐ ๊ธฐ๋ฅ ๊ตฌํ
5-1. ์ปค์คํ ๋ง์ปค ์์ด์ฝ
Future<BitmapDescriptor> _getCustomMarkerIcon() async {
return BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueOrange);
}
5-2. ์ง๋ ์คํ์ผ ์ ์ฉ
// Google Maps ์คํ์ผ JSON์ ์ ์ฉํ์ฌ ์ง๋ ์์์ ์ปค์คํฐ๋ง์ด์งํ ์ ์์ต๋๋ค
String _mapStyle = '''
[
{
"elementType": "geometry",
"stylers": [{"color": "#f5f5f5"}]
}
]
''';
// GoogleMap ์์ ฏ์ ์ ์ฉ
GoogleMap(
style: _mapStyle,
// ๋ค๋ฅธ ์ค์ ๋ค...
)
6. ๋ฌธ์ ํด๊ฒฐ ํ
6-1. ๊ถํ ์ฒ๋ฆฌ
์์น ๊ถํ์ ์์ ํ๊ฒ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ
Future<bool> _handleLocationPermission() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('์์น ์๋น์ค๊ฐ ๋นํ์ฑํ๋์ด ์์ต๋๋ค. ์์น ์๋น์ค๋ฅผ ํ์ฑํํด์ฃผ์ธ์.')));
return false;
}
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('์์น ๊ถํ์ด ๊ฑฐ๋ถ๋์์ต๋๋ค')));
return false;
}
}
if (permission == LocationPermission.deniedForever) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('์์น ๊ถํ์ด ์๊ตฌ์ ์ผ๋ก ๊ฑฐ๋ถ๋์ด ์ค์ ์์ ์ง์ ํ์ฉํด์ผ ํฉ๋๋ค.')));
return false;
}
return true;
}
6-2. ๋๋ฒ๊น ๋ก๊ทธ ํ์ฉ
๊ฐ๋ฐ ์ค์ ์ ์ฉํ ๋๋ฒ๊น ์ฝ๋
if (kDebugMode) {
print('์ง๋ ์ด๊ธฐํ ์์');
print('ํ์ฌ ์์น: ${position.latitude}, ${position.longitude}');
print('๋ง์ปค ์: ${markers.length}');
}
7. ์ฑ๋ฅ ์ต์ ํ
7-1. ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ
@override
void dispose() {
_mapController?.dispose();
_animationController.dispose();
super.dispose();
}
7-2. ๋ง์ปค ์ต์ ํ
๋ง์ ๋ง์ปค๋ฅผ ํ์ํ ๋๋ ํด๋ฌ์คํฐ๋ง์ ๊ณ ๋ คํ๋๊ฒ ์ข๋ค๊ณ ํฉ๋๋ค.
// google_maps_cluster_manager ํจํค์ง ์ฌ์ฉ
dependencies:
google_maps_cluster_manager: ^3.0.0
๋ง๋ฌด๋ฆฌ
Flutter์์ Google Maps๋ฅผ ์ฐ๋ํ๋ ๊ณผ์ ์ ๋จ๊ณ๋ณ๋ก ์ดํด๋ณด์์ต๋๋ค. ํต์ฌ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Google Cloud Console์์ ์ฌ๋ฐ๋ฅธ API ํค ์ค์
- AndroidManifest.xml์ API ํค์ ๊ถํ ์ถ๊ฐ
- ์ ์ ํ ํจํค์ง ์ค์น ๋ฐ ๊ถํ ์ฒ๋ฆฌ
- ์ฌ์ฉ์ ์นํ์ ์ธ UI/UX ๊ตฌํ
์ด ๊ฐ์ด๋๋ฅผ ๋ฐ๋ผํ๋ฉด ๊ธฐ๋ณธ์ ์ธ ์ง๋ ๊ธฐ๋ฅ๋ถํฐ ๋ชจ๋ํ ๋์์ธ๊น์ง ๋ชจ๋ ๊ตฌํํ ์ ์์ต๋๋ค. ์ง๋ ๊ธฐ๋ฅ์ ๋ฐฐ๋ฌ์ฑ, ์์น ๊ธฐ๋ฐ ์๋น์ค ๋ฑ ๋ค์ํ ์ฑ์์ ํต์ฌ์ ์ธ ์ญํ ์ ํ๋ฏ๋ก, ํ ๋ฒ ์ตํ๋๋ฉด ๋งค์ฐ ์ ์ฉํ ๊ฒ ๊ฐ์ต๋๋ค!