kakao_map_plugin
한국어 | English
카카오 지도 를 구동할 수 있는 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: [최신버전]
- 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 으로 실행되지만, 카카오 콘솔에 사이트 도메인을 등록해야 지도가 표시됩니다.
- Kakao Developers → 내 애플리케이션 → 플랫폼 → Web → 사이트 도메인에 앱이 서비스되는 origin 을 추가합니다. 포트까지 정확히 비교하므로 개발 중에는
http://localhost:포트를 그대로 등록하세요. (예:flutter run -d chrome --web-port=8080이면http://localhost:8080) - 등록되지 않은 도메인에서는 카카오 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), )
더 많은 카카오지도 샘플소스는 여기 에서 확인하실 수 있습니다.
실행화면

Web
같은 코드가 브라우저에서 그대로 동작합니다. (Chrome, flutter run -d chrome)
| 지도 | 지도 + 로드뷰(동동이) | Drawing Library |
|---|---|---|
![]() |
![]() |
![]() |


