Search packages, repositories and app categories

GitHub repositoryjohyunchol/kakao_map_plugin

kakao_map_plugin

johyunchol/kakao_map_plugin open-source repository details.

kakao_map_plugin project cover
Stars
39
Forks
27
Last push (UTC)
Sep 11, 2026
Project status
Active

Original README

English project snapshot. Visit GitHub for the latest content.

kakao_map_plugin

한국어 | English

pub package

카카오 지도 를 구동할 수 있는 Flutter 플러그인 입니다.

네이티브 라이브러리를 사용한 것이 아닌 Javascript 라이브러리를 이용하여 제작한 플러그인 입니다.

Android, iOS 는 webview_flutter 로, Web 은 iframe 으로 동작합니다. 모바일은 최소 버전 확인이 필요합니다.

Android iOS Web
Support SDK 19+ or 20+ 9.0+ 지원 (사이트 도메인 등록 필요, 아래 참고)

시작하기

공통

카카오 개발자센터 에서 javascript key 를 발급받아야 합니다.

pubspec.yaml에 dependencies에 작성

dependencies:
  kakao_map_plugin: [최신버전]
  1. javascript key 등록
  • Singleton 으로 되어 있어서 KakaoMap 위젯이 호출 되기 전에만 initialize 하면 됩니다. 여기서는 main 함수에서 호출하도록 했습니다.
  • example 에서는 flutter_dotenv 라이브러리를 사용하였습니다. 바로 실행해 보시려면 example/assets/env/.env.sample을 복사하여 example/assets/env/.env로 만들어주시고 .env 파일 내부에 APP_KEY=뒤에 본인의 javascript key 를 넣으시면 됩니다.
  • 키워드로 장소검색하기, 카테고리로 장소 검색, 주소로 장소 표시, 좌표로 주소를 얻어오기, 좌표 변환하기 와 같은 services 기능을 사용하려면 baseUrl 을 추가 해야 합니다.
  • .env에 BASE_URL=뒤에 본인의 baseUrl 주소를 넣으시면 됩니다.
void main() {
  AuthRepository.initialize(appKey: 'javascript key');
}

or

void main() {
  AuthRepository.initialize(appKey: 'javascript key', baseUrl: 'http://localhost');
}
Android

AndroidManifest.xml 에 INTERNET 권한 및 usesCleartextTraffic="true" 설정

<manifest>
    <!-- webview_flutter 에서 인터넷 접속을 위한 권한을 선언합니다 -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
    android:usesCleartextTraffic="true">
        ...
    </application>
</manifest>
iOS

Info.plist 에 NSAppTransportSecurity 권한 및 io.flutter.embedded_views_preview 설정

<dict>
    <key>NSAppTransportSecurity</key>
      <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
        <key>NSAllowsArbitraryLoadsInWebContent</key>
        <true/>
      </dict>
    <key>io.flutter.embedded_views_preview</key>
    <true/>
</dict>
Web

web 에서는 WebView 대신 iframe 으로 지도를 그립니다. 별도 설정 없이 flutter run -d chrome 으로 실행되지만, 카카오 콘솔에 사이트 도메인을 등록해야 지도가 표시됩니다.

  1. Kakao Developers → 내 애플리케이션 → 플랫폼 → Web → 사이트 도메인에 앱이 서비스되는 origin 을 추가합니다. 포트까지 정확히 비교하므로 개발 중에는 http://localhost:포트 를 그대로 등록하세요. (예: flutter run -d chrome --web-port=8080 이면 http://localhost:8080)
  2. 등록되지 않은 도메인에서는 카카오 SDK 가 401 을 돌려주고 브라우저 콘솔에 domain mismatched! caller=... 오류가 찍힙니다.

web 에서 다른 점:

  • 지도 위에 겹쳐 놓은 Flutter 위젯은 KakaoMapPointerInterceptor 로 감싸세요. web 에서 지도는 iframe 이라 그 위에 Stack 으로 올린 버튼·카드가 탭을 받지 못합니다. 이 위젯이 자식 영역의 포인터 이벤트를 Flutter 로 돌려주며, Android/iOS 에서는 자식을 그대로 반환하므로 플랫폼 구분 없이 쓰면 됩니다. Scaffold 의 floatingActionButton 처럼 지도와 겹치는 것도 포함됩니다.

    Stack(
      children: [
        KakaoMap(onMapCreated: (c) => mapController = c),
        Positioned(
          top: 16,
          right: 16,
          child: KakaoMapPointerInterceptor(
            child: ElevatedButton(onPressed: () {}, child: const Text('현재 위치')),
          ),
        ),
      ],
    )
    
  • AuthRepository.initialize(baseUrl:) 은 무시됩니다. 도메인 검사는 실제 페이지 origin 으로 이뤄집니다.

  • gestureRecognizers 는 쓰이지 않습니다. iframe 안의 포인터 이벤트는 브라우저가 직접 처리합니다.

  • KakaoMapController.webViewController 는 web 에서 StateError 를 던집니다(WebView 가 없습니다). 플랫폼에 관계없이 지도 문서 안에서 JavaScript 를 직접 실행하려면 controller.runJavaScript() / controller.evaluateJavaScript() 를 사용하세요.

    // 플러그인이 아직 감싸지 않은 SDK 기능을 직접 호출할 때 (Android / iOS / Web 공통)
    await mapController.runJavaScript('map.setCopyrightPosition(kakao.maps.CopyrightPosition.BOTTOMRIGHT);');
    final raw = await mapController.evaluateJavaScript('JSON.stringify(map.getLevel())');
    
  • 그 외 지도·오버레이·로드뷰·Drawing·검색·타일셋 API 는 모바일과 동일하게 동작합니다.

플랫폼별 차이
항목 Android iOS Web
gestureRecognizers ✅ ✅ 무시 (iframe 이 직접 처리)
AuthRepository.initialize(baseUrl:) ✅ ✅ 무시 (실제 origin 검사)
controller.webViewController ✅ ✅ StateError → runJavaScript / evaluateJavaScript 사용
지도 위에 겹친 Flutter 위젯 탭 ✅ ✅ KakaoMapPointerInterceptor 로 감싸야 함
reload() 후 HTML 재실행 ✅ ❌ (WKWebView 제약, 위젯 재생성 권장) ✅
scrollwheel, keyboardShortcuts 해당 없음 해당 없음 ✅
hover 콜백 (onMarkerMouseOver 등) ❌ ❌ ✅ 마우스가 있을 때 (supportsHover())
onMapLongPress ✅ ✅ ✅ (우클릭 포함)
카카오 지도 API 로 지원되지 않는 것

카카오 JavaScript API 자체에 없는 기능이라 이 플러그인으로도 제공할 수 없습니다.

  • 지도 회전(bearing)·기울기(tilt)·3D 건물·실내 지도
  • 지도 스타일 JSON / 다크 모드 기본 지도 (커스텀 타일셋으로 직접 만든 타일만 가능)
  • 타일에 그려진 POI(상호명 라벨) 탭 이벤트
  • 기본 지도 타일의 오프라인 캐싱
  • 지도 스냅샷(이미지 캡처) — 타일이 교차 출처라 캔버스에서 읽을 수 없습니다. 정적 지도가 필요하면 KakaoStaticMap 을 사용하세요

0.x → 1.0.0 마이그레이션

1.0.0 은 첫 안정 버전입니다. 제거된 API 는 없습니다. 시그니처는 모두 그대로이고, 아래 항목만 동작이 달라졌으므로 해당 코드가 있는지 확인하세요.

동작이 바뀐 API
이전 (0.x) 1.0.0 이렇게 바꾸세요
controller.clearMarker() 가 클러스터러 마커까지 제거 일반 마커만 제거 클러스터러 마커도 지우려면 clearMarkerClusterer() 를 함께 호출
controller.clear() 후 클러스터 표시가 남을 수 있었음 클러스터러 객체와 마커까지 모두 제거 클러스터러만 남기려면 clear() 대신 clearPolyline() 등 개별 clearXxx() 조합
controller.clearMarkerClusterer() 가 마커를 남김 클러스터러를 해제하고 소속 마커도 목록에서 제거 마커를 다시 보이려면 addMarker() 재호출
addMarker(markers: []), KakaoMap(markers: []) 는 무시됨 다른 오버레이와 같이 기존 일반 마커를 모두 제거 기존 마커를 유지하려면 [] 대신 null 전달
같은 ID 마커를 다시 추가하면 무시됨 내용이 바뀌었으면 갱신 유지가 목적이면 같은 ID 로 재호출하지 않기
위젯 속성(markers: 등)으로 넘긴 오버레이를 onMapCreated 에서 직접 그려야 했음 지도 준비 시 자동으로 그려짐 onMapCreated 안의 중복 addMarker() 등 제거
검색 실패(오류 상태, null 결과)가 성공처럼 반환 Future 가 에러로 완료 try/catch 로 감싸기
같은 종류 검색을 겹쳐 호출하면 이전 xxxResult() 대기가 영구 대기 이전 대기가 StateError 로 종료 controller.keywordSearch() 반환값을 사용하거나 try/catch
인포윈도우·커스텀 오버레이 안의 <a href> 를 탭하면 WebView 가 이동해 지도가 사라짐 이동을 가로채고 onLinkTap 으로 알림 (콜백 없으면 무시) KakaoMap(onLinkTap: (url) => launchUrl(url))
지도 문서에서 텍스트 선택·탭 하이라이트·롱프레스 메뉴·페이지 핀치 줌 가능 앱처럼 모두 꺼짐 선택이 필요한 요소에 class="kmp-selectable"
// 예: 전체 초기화가 목적이던 코드
await controller.clearMarker();            // 0.x: 클러스터러 마커까지 사라짐
// 1.0.0
await controller.clearMarker();
await controller.clearMarkerClusterer();   // 클러스터러 마커까지 지우려면 추가

// 예: 검색
try {
  final result = await controller.keywordSearch(KeywordSearchRequest(keyword: '카페'));
} catch (e) {
  // 1.0.0 부터 실패는 예외로 옵니다.
}
Deprecated (계속 동작하며 2.0.0 에서 제거 예정)
Deprecated 대체
MapType.roadMap MapType.normal (값이 같습니다)
controller.setStyle(width, height) 컨테이너 크기는 Flutter 위젯으로 제어하고, 필요하면 controller.relayout()
controller.getDraggable() / getZoomable() isDraggable() / isZoomable() (플랫폼과 무관하게 bool 반환)
Marker.markerImageSrc Marker.icon + MarkerIcon.network(url) 등 (기존 문자열 방식도 계속 동작)
KakaoMapPluginPlatform, MethodChannelKakaoMapPlugin 사용하지 않는 템플릿 클래스입니다. 참조를 제거하세요 (KakaoMapPluginWeb 은 Web 등록용으로 유지)
새로 추가된 것 (기존 코드 영향 없음)
  • Web 지원 — 카카오 개발자 콘솔에 사이트 도메인 등록이 필요합니다. 아래 Web 절 참고.
  • 의존성 pointer_interceptor 추가 — Web 에서 지도 위 Flutter 위젯이 탭을 받기 위한 것으로, 모바일 동작에는 영향이 없습니다.
  • AuthRepository.initialize(libraries:) 로 불러올 카카오 라이브러리를 줄일 수 있습니다. 지정하지 않으면 이전과 같이 전부 불러옵니다.
  • 그 밖의 새 위젯·API 는 CHANGELOG 를 참고하세요.

예제

Kakao maps api 사이트에 있는 예제를 기준으로 샘플을 만들었습니다.

  • 기본 지도 생성

    Scaffold(
      body: KakaoMap(),
    );
    
  • 맵 생성 callback

    Scaffold(
      body: KakaoMap(
        onMapCreated: ((controller) {
          mapController = controller;
        }),
    
      ),
    );
    
  • 마커 생성 - 지도가 생성되면 마커 추가되는 예제

    Set<Marker> markers = {}; // 마커 변수
    
    Scaffold(
      body: KakaoMap(
        onMapCreated: ((controller) async {
          mapController = controller;
    
          markers.add(Marker(
            markerId: UniqueKey().toString(),
            latLng: await mapController.getCenter(),
          ));
    
          setState(() { });
        }),
        markers: markers.toList(),
        center: LatLng(37.3608681, 126.9306506),
      ),
    );
    
  • 마커 클러스터 생성 - 지도가 생성되면 마커 추가되는 예제 (마커와 클러스터 함께 사용하지 마세요. 클러스터 안에 마커를 넣어서 사용하세요.)

    Clusterer? clusterer;
    
    Scaffold(
      body: KakaoMap(
        onMapCreated: ((controller) async {
          mapController = controller;
    
          Set<Marker> markers = {};
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.27943075229118, 127.01763998406159)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.55915668706214, 126.92536526611102)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.13854258261161, 129.1014781294671)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.55518388656961, 126.92926237742505)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.20618517638034, 129.07944301057026)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.561110808242056, 126.9831268386891)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.86187129655063, 127.7410250820423)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.47160156778542, 126.62818064142286)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.10233410927457, 129.02611815856181)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.10215562270429, 129.02579793018205)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.475423012251106, 128.76666923366042)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.93282824693927, 126.95307628834287)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.33884892276137, 127.393666019664)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.520412849636, 126.9742764161581)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.155139675209675, 129.06154773758374)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.816041994696576, 127.11046706211324)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(38.20441110638504, 128.59038671285234)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.586112739308916, 127.02949148517999)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.50380641844987, 127.02130716617751)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.55155704387368, 126.92161115892036)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.55413060051369, 126.92207472929526)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.362321615174835, 127.35000483225389)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.55227862908755, 126.92280546294998)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.490413948014606, 127.02079678472444)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.172358507549596, 126.90545394866643)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.15474103200252, 129.11827889154455)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.516081250973485, 127.02369057166361)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.80711722863776, 127.14020346037576)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.28957415752673, 127.00103752005424)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.83953896766896, 128.7566880321854)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.51027412948879, 127.08227718124704)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.493581783270294, 126.72541955660554)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.135291862962795, 129.10060911448775)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.174574933144065, 126.91389980787773)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.795887691878654, 127.10660416587146)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.59288687521181, 126.96560524627377)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.45076411130452, 127.14593003749792)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.86008337557079, 127.1263912488061)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.23773491330953, 129.08371037429578)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.524297321304886, 127.05018281937049)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.33386658021849, 127.4461721466889)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.72963747546802, 128.27079056365005)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.02726828142973, 129.37257233594056)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.0708030360945, 129.0593185494088)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.86835862950247, 128.59755089175871)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(33.51133264696746, 126.51852347452322)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.61284289586752, 127.03120547238589)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.851696038722466, 128.59092937125666)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.59084695083232, 127.01872773588882)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.52114874288784, 129.33573629945764)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.362326407439845, 127.33577420148076)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.28941189110747, 127.00446132665141)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.32049801117398, 129.1810343576788)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.53338631541601, 127.00615481678061)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.413461468258156, 126.67735680840826)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.920390371093205, 128.54411720249956)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.65489374054824, 127.48374816871991)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.49491987110441, 127.01493134206048)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.64985695608336, 127.14496345268074)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.55686770317417, 127.16927880543041)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.37014007589146, 127.10614330185591)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.5350236507627, 126.96157681184789)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.40549630594667, 126.8980581820004)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(34.867950544005744, 128.69069690081176)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.16317059543225, 128.98452978748048)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.607484825953186, 127.48520451195111)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.651724785213986, 126.58306748337554)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.86059690063427, 128.59193087665244)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.25685847585025, 128.59912605060455)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(33.509258155694496, 126.5109451464813)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.64366155701157, 126.63255039247507)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.82667262227336, 127.1030670574823)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.82003554991111, 127.14810974062483)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.097485195649455, 128.99486181862338)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.32204249590605, 127.95591893585816)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.50535127272031, 127.1047465440526)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.99081407156533, 127.09338324956647)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.323486640444834, 127.12285239871076)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.78973089440451, 127.13644319545601)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.641373953578196, 129.35463220719618)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.47423127310911, 126.97625029161996)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.84357192991226, 128.61143720719716)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.180974984085736, 128.20294526341132)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.57895718642583, 126.9316897337244)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(33.49077253755052, 126.49314817000993)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.42175925330255, 128.67409133225766)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.46405540570109, 126.7153544119173)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.594758776232126, 127.10099917489818)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.7239966558994, 127.0478671731854)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.86680171505329, 128.5923738376741)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.560573727266785, 126.81239107485251)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.78692224857484, 126.98966010341789)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.10368644802913, 129.0206862606022)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.063839948992644, 127.06856523030079)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.34344643728643, 127.94382181350932)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.512521267219064, 127.40054805648133)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.15286653837983, 126.90419903971498)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.173238445546296, 129.176082844468)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.082394201323524, 129.40330471725923)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.51043665598106, 127.03974070036524)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.627816673285054, 127.44969866021904)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.59194624756919, 127.01817545576053)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.387147045560866, 127.1253365438929)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.89948383848115, 128.60809550730653)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.555316235235324, 127.14038447894715)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.09622092762977, 128.43314679004078)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.582855922985544, 126.91907857008522)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.516000983841586, 128.72798872032757)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.48429363675198, 127.0379630203579)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.54502575965604, 126.95429338245707)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.236247173046394, 128.8677618015292)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.40157536691968, 127.11717457214067)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.95191038001258, 127.91064040877527)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.491526492971346, 126.85463749525812)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(36.124356479753196, 128.09517052346138)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.15715169307048, 128.15853461363773)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.5808156608605, 126.95109705510639)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.46931787249714, 126.89904775044873)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.52195614910054, 129.3209904841746)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.58625703195563, 126.9496035206742)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.28463639199199, 126.85984474757359)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.534169458631226, 129.31169021536095)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.553341234194285, 127.15481222237025)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(37.62293367990081, 126.83445005122417)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.5272027005698, 127.72953798950101)));
          markers.add(Marker(
              markerId: '${markers.length + 1}',
              latLng: LatLng(35.180032285898854, 128.06954509175367)));
    
          clusterer = Clusterer(
            markers: markers.toList(),
            minLevel: 6,
            gridSize: 45,
            calculator: [30, 60],
            texts: ['적음', '보통', '많음'],
            styles: [
              ClustererStyle(
                width: 50,
                height: 50,
                background: Colors.blue.withOpacity(0.8),
                borderRadius: 25,
                color: Colors.white,
                textAlign: 'center',
                lineHeight: 60,
              ),
              ClustererStyle(
                width: 50,
                height: 50,
                background: Colors.red.withOpacity(0.8),
                borderRadius: 25,
                color: Colors.yellow,
                textAlign: 'center',
                lineHeight: 60,
              ),
              ClustererStyle(
                width: 50,
                height: 50,
                background: Colors.purple.withOpacity(0.8),
                borderRadius: 25,
                color: Colors.white,
                textAlign: 'center',
                lineHeight: 60,
              ),
            ],
          );
    
          setState(() { });
        }),
        clusterer: clusterer,
        center: LatLng(37.3608681, 126.9306506),
      ),
    );
    
  • Circle, Polyline, Polygon, Rectangle 예제

    Set<Circle> circles = {};
    Set<Polyline> polylines = {};
    Set<Polygon> polygons = {};
    Set<Rectangle> rectangles = {};
    
    Scaffold(
      appBar: AppBar(
        title: Text(widget.title ?? selectedTitle),
      ),
      body: KakaoMap(
        onMapCreated: ((controller) async {
          mapController = controller;
    
          circles.add(
            Circle(
              circleId: circles.length.toString(),
              center: LatLng(33.450701, 126.570667),
              strokeWidth: 5,
              strokeColor: Colors.red,
              strokeOpacity: 0.5,
              strokeStyle: StrokeStyle.longDashDotDot,
              fillColor: Colors.black,
              fillOpacity: 0.7,
              radius: 50,
            ),
          );
    
          polylines.add(
            Polyline(
              polylineId: 'polyline_${polylines.length}',
              points: [
                LatLng(33.452344169439975, 126.56878163224233),
                LatLng(33.452739313807456, 126.5709308145358),
                LatLng(33.45178067090639, 126.5726886938753)
              ],
              strokeColor: Colors.purple,
            ),
          );
    
          polygons.add(
            Polygon(
              polygonId: 'polygon_${polygons.length}',
              points: [
                LatLng(33.45133510810506, 126.57159381623066),
                LatLng(33.44955812811862, 126.5713551811832),
                LatLng(33.449986291544086, 126.57263296172184),
                LatLng(33.450682513554554, 126.57321034054742),
                LatLng(33.451346760004206, 126.57235740081413)
              ],
              strokeWidth: 4,
              strokeColor: Colors.blue,
              strokeOpacity: 1,
              strokeStyle: StrokeStyle.shortDashDot,
              fillColor: Colors.black,
              fillOpacity: 0.3,
            ),
          );
    
          rectangles.add(
            Rectangle(
              rectangleId: 'rectangle_${rectangles.length}',
              rectangleBounds: LatLngBounds(
                LatLng(33.42133510810506, 126.53159381623066),
                LatLng(33.44955812811862, 126.5713551811832),
              ),
              strokeWidth: 6,
              strokeColor: Colors.blue,
              strokeOpacity: 1,
              strokeStyle: StrokeStyle.dot,
              fillColor: Colors.black,
              fillOpacity: 0.7,
            ),
          );
    
          setState(() {});
        }),
        circles: circles.toList(),
        polylines: polylines.toList(),
        polygons: polygons.toList(),
        rectangles: rectangles.toList(),
        center: LatLng(33.450701, 126.570667),
      ),
    );
    
  • 불러올 확장 라이브러리 선택 - 기본값은 전체(services, clusterer, drawing)입니다. 쓰지 않는 라이브러리를 빼면 지도 생성이 조금 빨라집니다.

    // 앱 전역 기본값
    AuthRepository.initialize(
      appKey: 'YOUR_JAVASCRIPT_KEY',
      libraries: {KakaoMapLibrary.services},
    );
    
    // 위젯 단위로 덮어쓰기 (clusterer 를 쓰면 자동으로 포함됩니다)
    KakaoMap(
      libraries: const {KakaoMapLibrary.services, KakaoMapLibrary.drawing},
    );
    
  • 로드뷰 생성 - onRoadviewCreated 로 받은 KakaoRoadviewController 로 파노라마 이동과 시점을 제어합니다.

    KakaoRoadviewController? roadviewController;
    
    Scaffold(
      body: KakaoRoadMap(
        center: LatLng(33.450701, 126.570667),
        radius: 50,
        viewpoint: const Viewpoint(pan: 90, tilt: 0, zoom: 0),
        markers: [
          Marker(
            markerId: 'm1',
            latLng: LatLng(33.450701, 126.570667),
            altitude: 5,   // 로드뷰에서 마커가 놓일 높이(m)
            range: 100,    // 마커가 보이는 반경(m)
          ),
        ],
        onRoadviewCreated: (controller) => roadviewController = controller,
        onViewpointChange: (viewpoint) => print('pan ${viewpoint.pan}'),
        onRoadviewNotFound: (latLng) => print('이 지점에는 로드뷰가 없습니다.'),
      ),
    );
    
    // 다른 위치의 가장 가까운 파노라마로 이동
    await roadviewController?.setPanoIdNear(LatLng(37.566826, 126.9786567));
    await roadviewController?.setViewpoint(const Viewpoint(pan: 180, tilt: 0, zoom: 1));
    
  • 지도와 로드뷰 함께 쓰기 - 한 화면에서 지도 클릭으로 로드뷰를 옮기고, 지도 위 동동이(MapWalker)가 로드뷰 시점 방향을 따라갑니다.

    KakaoMapRoadviewController? linkController;
    
    Scaffold(
      body: KakaoMapRoadviewView(
        center: LatLng(33.450701, 126.570667),
        initialViewMode: RoadviewViewMode.split, // map / roadview / split
        splitRatio: 50,                          // split 일 때 지도 비율(%)
        showRoadviewOverlay: true,               // 로드뷰 가능 도로 표시
        useMapWalker: true,
        onCreated: (controller) => linkController = controller,
      ),
    );
    
    await linkController?.toggleRoadview(LatLng(33.450701, 126.570667));
    await linkController?.setViewMode(RoadviewViewMode.roadview);
    
  • Drawing Library - 사용자가 지도 위에 마커·선·다각형·원 등을 직접 그리고, 그린 결과를 데이터로 가져옵니다.

    late KakaoMapController mapController;
    
    Scaffold(
      body: KakaoMap(
        onMapCreated: ((controller) async {
          mapController = controller;
    
          await mapController.createDrawingManager(
            options: const DrawingOptions(
              drawingMode: [
                DrawingOverlayType.marker,
                DrawingOverlayType.polyline,
                DrawingOverlayType.polygon,
              ],
              polylineStyle: DrawingStyle(strokeColor: Colors.blue, strokeWidth: 3),
            ),
          );
          await mapController.showDrawingToolbox(); // 카카오 기본 툴박스 UI (선택)
          await mapController.selectDrawingMode(DrawingOverlayType.polyline);
        }),
        onDrawingEnd: (type) async {
          final data = await mapController.getDrawingData();
          for (final line in data.polylines) {
            print('선 좌표 ${line.points.length}개');
          }
        },
      ),
    );
    
    // 되돌리기 / 다시 실행 / 그리던 도형 취소
    await mapController.undoDrawing();
    await mapController.redoDrawing();
    await mapController.cancelDrawing();
    
  • 커스텀 타일셋 - 직접 만든 타일 이미지를 기본 지도로 쓰거나 기존 지도 위에 겹칩니다.

    // 1) 주소 템플릿 ({x} {y} {z} 치환)
    await mapController.addTileset(const Tileset(
      id: 'MY_TILES',
      urlTemplate: 'https://tiles.example.com/{z}/{y}/{x}.png',
      copyright: [TilesetCopyright('© Example')],
    ));
    await mapController.setTileset('MY_TILES');         // 기본 지도로 사용
    await mapController.setMapTypeId(MapType.normal);   // 일반 지도로 복귀
    
    // 2) DOM 타일 (JavaScript 함수 원문을 그대로 전달)
    await mapController.addTileset(const Tileset(
      id: 'TILE_NUMBER',
      tileFunction: '''
        function (x, y, z) {
          var div = document.createElement('div');
          div.innerHTML = x + ', ' + y + ', ' + z;
          div.style.border = '1px dashed #ff5050';
          return div;
        }
      ''',
    ));
    await mapController.addOverlayTileset('TILE_NUMBER');    // 지도 위에 겹치기
    await mapController.removeOverlayTileset('TILE_NUMBER');
    

    urlFunction / tileFunction 은 WebView 안에서 그대로 실행되므로 앱이 직접 작성한 문자열만 넘기세요.

  • 앱 느낌으로 만들기 - 인포윈도우 스타일, 테마, Flutter 컨트롤, 위젯 마커

    카카오 SDK 가 직접 그리는 UI(인포윈도우, 줌/지도타입 컨트롤, 클러스터, 기본 마커)는 웹페이지 느낌이 납니다. 아래 기능으로 앱 디자인에 맞출 수 있습니다. 지정하지 않으면 기존 모양 그대로입니다.

    // 1) 인포윈도우 — 마커별 또는 테마로 지도 전체 기본값
    Marker(
      markerId: 'm1',
      latLng: LatLng(37.5665, 126.9780),
      infoWindowContent: '<b>서울시청</b><br>02-120',
      infoWindowStyle: const InfoWindowStyle.material(),   // .cupertino(), .dark(), 또는 직접 지정
    );
    
    // 2) 테마 — 전역(AuthRepository.initialize(theme:)) 또는 지도별
    KakaoMap(
      theme: const KakaoMapTheme(
        infoWindowStyle: InfoWindowStyle.material(),
        backgroundColor: Color(0xFFEFF3F6),   // 타일 로딩 전 배경(SDK 기본 회색 격자 대신)
        fontFamily: 'Pretendard, sans-serif', // 기본은 시스템 글꼴 스택
      ),
      copyrightPosition: CopyrightPosition.bottomLeft, // 오른쪽 아래 버튼과 겹치지 않게
    );
    
    // 3) Flutter 컨트롤 — SDK 컨트롤 대신 Stack 으로 올리기 (web 에서도 눌립니다)
    Stack(children: [
      KakaoMap(onMapCreated: (c) => setState(() => controller = c)),
      if (controller != null) KakaoMapControls(controller: controller!, showMapType: true),
    ]);
    KakaoDrawingToolbar(controller: controller!, modes: const [DrawingOverlayType.polyline, DrawingOverlayType.polygon]);
    
    // 4) 마커 — 색만 바꾼 핀, 또는 Flutter 위젯을 그대로 그린 마커
    Marker(markerId: 'p', latLng: latLng, icon: MarkerIcon.pin(color: Colors.red), width: 28, height: 40, offsetX: 14, offsetY: 40);
    final tag = await MarkerIcon.fromWidget(PriceTag('12,000원'), logicalSize: const Size(96, 44));
    Marker(markerId: 't', latLng: latLng, icon: tag, width: 96, height: 44, offsetX: 48, offsetY: 44);
    
    // 5) 클러스터 — 원형 Material 프리셋
    Clusterer(markers: markers, styles: [ClustererStyle.material(Colors.indigo, size: 48)]);
    
  • Flutter 위젯 오버레이 - 진짜 Flutter 위젯을 지도 좌표에 붙이기

    KakaoMap(
      widgetOverlays: [
        KakaoMapWidgetOverlay(
          id: 'cafe',
          position: LatLng(37.5665, 126.9780),
          anchor: Alignment.bottomCenter,           // 위젯의 아래 가운데를 좌표에 맞춤
          child: GestureDetector(
            onTap: () => showModalBottomSheet(...), // 탭하면 앱 UI 로 이어지는 패턴
            child: Card(child: Padding(padding: EdgeInsets.all(8), child: Text('카페 · 4,500원'))),
          ),
        ),
      ],
    )
    

    지도를 움직이면 JS 가 픽셀 좌표를 보내 위젯이 따라옵니다(프레임당 1회). web 에서도 눌립니다. 수백 개 이상이면 CustomOverlay 를 쓰세요.

  • 오버레이 이벤트와 길게 누르기

    KakaoMap(
      onPolylineTap: (id, latLng, level) {},   // 선 / 원 / 사각형 탭 (다각형은 onPolygonTap)
      onCircleTap: (id, latLng, level) {},
      onRectangleTap: (id, latLng, level) {},
      onMapLongPress: (latLng) {},             // 0.5초 이상 누르기 (마우스 환경은 우클릭도)
    )
    
  • 마우스 hover - 마우스 포인터가 있는 환경(데스크톱 브라우저) 전용

    터치 기기에서는 호출되지 않으므로 탭 콜백을 함께 처리하세요. 실행 환경은 await controller.supportsHover() 로 확인할 수 있습니다.

    KakaoMap(
      onMarkerMouseOver: (id, latLng, level) {},   // onMarkerMouseOut
      onPolygonMouseOver: (id, latLng, level) {},  // onPolygonMouseMove(프레임당 1회) / onPolygonMouseOut
      onMarkerTap: (id, latLng, level) {},         // 터치 대체
    )
    
  • 마커 옵션과 부분 갱신

    Marker(
      markerId: 'bus', latLng: latLng,
      opacity: 0.6, clickable: false, title: '툴팁(web)', visible: true,
      // 스프라이트 시트에서 잘라 쓰기
      markerImageSrc: 'https://…/sprite.png', width: 36, height: 37,
      spriteOrigin: Point(0, 46), spriteWidth: 36, spriteHeight: 691,
    );
    await controller.setMarkerPosition('bus', newLatLng); // 재생성 없이 이동 (실시간 위치)
    await controller.setMarkerVisible('bus', false);
    await controller.showInfoWindow('bus');                 // 목록 탭 → 지도 인포윈도우 열기
    await controller.hideInfoWindow('bus');
    
  • 검색 페이지 정보

    final r = await controller.keywordSearch(KeywordSearchRequest(keyword: '카페', size: 15, page: 1));
    if (r.pagination?.hasNextPage == true) {
      final next = await controller.keywordSearch(KeywordSearchRequest(keyword: '카페', size: 15, page: 2));
    }
    
  • 카카오맵 앱으로 연결하기 (길찾기·장소·로드뷰) - URL 만 만들고 실행은 url_launcher 로

    // 웹 링크: 앱이 있으면 앱으로, 없으면 모바일 웹으로 열립니다 (권장)
    final uri = KakaoMapLinks.web.route(to: LatLng(37.5665, 126.9780), toName: '서울시청', mode: KakaoMapRouteMode.transit);
    await launchUrl(uri, mode: LaunchMode.externalApplication);
    // 앱 스킴: kakaomap:// (iOS 는 LSApplicationQueriesSchemes 에 kakaomap 추가, 없으면 KakaoMapLinks.storeUrl())
    KakaoMapLinks.app.route(to: LatLng(37.5665, 126.9780), mode: KakaoMapRouteMode.car);
    
  • 정적 지도 - 움직이지 않는 지도 이미지가 필요할 때 (목록 썸네일, 공유 미리보기 등)

    KakaoStaticMap(
      center: LatLng(33.450701, 126.570667),
      currentLevel: 3,
      markers: [
        Marker(markerId: 'm1', latLng: LatLng(33.450701, 126.570667), infoWindowContent: '카카오'),
      ],
    )
    
  • 카메라 제어와 측정

    // 중심 + 레벨을 한 번에, 애니메이션 시간 지정
    await mapController.jump(LatLng(37.5665, 126.9780), 5, animate: true, duration: const Duration(milliseconds: 400));
    // 영역이 보이도록 이동 (여백 px)
    await mapController.panToBounds(LatLngBounds(LatLng(37.55, 126.96), LatLng(37.58, 127.0)), padding: 48);
    await mapController.fitBounds(points, padding: 48);
    // 바텀시트가 열릴 때 지도를 위로 밀어 올리기
    await mapController.panBy(0, -150);
    // 확대 범위 제한 (rebuild 시 KakaoMap(minLevel:, maxLevel:) 변경도 반영됩니다)
    await mapController.setMinLevel(2);
    await mapController.setMaxLevel(10);
    
    // SDK 가 계산한 길이(m)·면적(㎡)
    final meters = await mapController.getPolylineLength('route');
    final squareMeters = await mapController.getPolygonArea('area');
    
  • 지도 생성 옵션과 이벤트

    KakaoMap(
      mapTypeId: MapType.skyView,        // 처음부터 스카이뷰로
      disableDoubleClickZoom: true,      // 더블탭 확대 끄기
      onMapTypeChanged: (type) => print('지도 타입: $type'),
      // 인포윈도우·커스텀 오버레이 안의 링크는 WebView 이동 대신 이 콜백으로 옵니다.
      onLinkTap: (url) => launchUrl(url, mode: LaunchMode.externalApplication),
    )
    

더 많은 카카오지도 샘플소스는 여기 에서 확인하실 수 있습니다.


실행화면

example

Web

같은 코드가 브라우저에서 그대로 동작합니다. (Chrome, flutter run -d chrome)

지도 지도 + 로드뷰(동동이) Drawing Library
web map web roadview web drawing
Jo Hyun Chol GitHub avatar
GitHub author

Jo Hyun Chol ↗

LanguagesDartRubyHTMLKotlinSwiftCMakeObjective-CShellCC++

Packages published by this repository

Dependencies used

Dependency list 12 items

  • flutter{"sdk":"flutter"}
  • flutter_web_plugins{"sdk":"flutter"}
  • plugin_platform_interface^2.1.8
  • pointer_interceptor^0.10.1
  • web^1.0.0
  • webview_flutter^4.12.0
  • webview_flutter_android^4.7.0
  • webview_flutter_wkwebview^3.22.0
  • flutter_testDevelopment{"sdk":"flutter"}
  • flutter_lintsDevelopment^5.0.0
  • lintsDevelopment^5.1.1
  • webview_flutter_platform_interfaceDevelopment^2.10.0

How do I choose a repository?

Start with your use case, compare repository activity, and check the original project before adopting it.

  1. Search by repository name, owner or description, then narrow the results by topic.
  2. Compare stars, forks and recent pushes. Stars show community interest, not a guarantee of quality.
  3. Read the README, license and issues on GitHub before using a project.

Sources and maintenance

“A repository is the most basic element of GitHub.”

GitHub Docs — About repositories ↗