ํ”„๋กœ์ ํŠธ/์ž์œจ์ฃผํ–‰

[Flutter] ํ”Œ๋Ÿฌํ„ฐ์—์„œ Google Maps ์—ฐ๋™ํ•˜๋Š” ๋ฐฉ๋ฒ• ์ •๋ฆฌ

THE NICOLE 2025. 9. 27. 21:59

Flutter ์•ฑ์—์„œ ์ง€๋„ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์€ ์ƒ๊ฐ๋ณด๋‹ค ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋Š˜์€ Google Maps API ํ‚ค ์„ค์ •๋ถ€ํ„ฐ ์‹ค์ œ ์ง€๋„ ๊ตฌํ˜„๊นŒ์ง€ ์ฐจ๊ทผ์ฐจ๊ทผ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ง์ ‘ ๊ตฌํ˜„ํ•œ ํ”Œ๋Ÿฌํ„ฐ UI์—์„œ์˜ ์ง€๋„ ํ™”๋ฉด!

1. Google Cloud Console์—์„œ API ํ‚ค ๋ฐœ๊ธ‰๋ฐ›๊ธฐ

1-1. Google Cloud Console ์ ‘์†

  1. Google Cloud Console์— ์ ‘์†ํ•ฉ๋‹ˆ๋‹ค
  2. ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ๊ธฐ์กด ํ”„๋กœ์ ํŠธ๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค

1-2. Maps API ํ™œ์„ฑํ™”

  1. ์ขŒ์ธก ๋ฉ”๋‰ด์—์„œ "API ๋ฐ ์„œ๋น„์Šค" → "๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ" ํด๋ฆญ
  2. "Maps SDK for Android" ๊ฒ€์ƒ‰ ํ›„ ํ™œ์„ฑํ™”
  3. "Maps JavaScript API"๋„ ํ•จ๊ป˜ ํ™œ์„ฑํ™” (์›น์—์„œ๋„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ)

1-3. API ํ‚ค ์ƒ์„ฑ

  1. "API ๋ฐ ์„œ๋น„์Šค" → "์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด" ํด๋ฆญ
  2. "+ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด ๋งŒ๋“ค๊ธฐ" → "API ํ‚ค" ์„ ํƒ
  3. ์ƒ์„ฑ๋œ API ํ‚ค๋ฅผ ๋ณต์‚ฌํ•ด ๋‘ก๋‹ˆ๋‹ค

1-4. API ํ‚ค ์ œํ•œ ์„ค์ • (์„ ํƒ์‚ฌํ•ญ)

๋ณด์•ˆ์„ ์œ„ํ•ด API ํ‚ค ์‚ฌ์šฉ์„ ์ œํ•œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  1. ์ƒ์„ฑ๋œ API ํ‚ค ํด๋ฆญ
  2. "์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ œํ•œ์‚ฌํ•ญ"์—์„œ "Android ์•ฑ" ์„ ํƒ
  3. ํŒจํ‚ค์ง€ ์ด๋ฆ„๊ณผ 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๋ฅผ ์—ฐ๋™ํ•˜๋Š” ๊ณผ์ •์„ ๋‹จ๊ณ„๋ณ„๋กœ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ํ•ต์‹ฌ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. Google Cloud Console์—์„œ ์˜ฌ๋ฐ”๋ฅธ API ํ‚ค ์„ค์ •
  2. AndroidManifest.xml์— API ํ‚ค์™€ ๊ถŒํ•œ ์ถ”๊ฐ€
  3. ์ ์ ˆํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜ ๋ฐ ๊ถŒํ•œ ์ฒ˜๋ฆฌ
  4. ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ UI/UX ๊ตฌํ˜„

 

์ด ๊ฐ€์ด๋“œ๋ฅผ ๋”ฐ๋ผํ•˜๋ฉด ๊ธฐ๋ณธ์ ์ธ ์ง€๋„ ๊ธฐ๋Šฅ๋ถ€ํ„ฐ ๋ชจ๋˜ํ•œ ๋””์ž์ธ๊นŒ์ง€ ๋ชจ๋‘ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ง€๋„ ๊ธฐ๋Šฅ์€ ๋ฐฐ๋‹ฌ์•ฑ, ์œ„์น˜ ๊ธฐ๋ฐ˜ ์„œ๋น„์Šค ๋“ฑ ๋‹ค์–‘ํ•œ ์•ฑ์—์„œ ํ•ต์‹ฌ์ ์ธ ์—ญํ• ์„ ํ•˜๋ฏ€๋กœ, ํ•œ ๋ฒˆ ์ตํ˜€๋‘๋ฉด ๋งค์šฐ ์œ ์šฉํ•  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค!