lustre_kakaomap
Gleam / Lustre 애플리케이션을 위한 선언적 카카오맵 래퍼 라이브러리.
gleam add lustre_kakaomap@1
시작하기
1. 카카오맵 SDK 로드
API 키는 카카오 개발자사이트에서 JavaScript 키를 발급받으세요.
키를 소스에 직접 노출하지 않도록 .env 파일로 관리하는 것을 권장합니다.
# .env
KAKAO_APP_KEY=발급받은_APP_KEY
HTML 파일에서는 플레이스홀더를 사용하고, dev 서버가 .env의 값을 주입합니다:
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%KAKAO_APP_KEY%"></script>
서비스(장소 검색, 주소 변환)나 클러스터러를 사용하려면 라이브러리를 함께 로드합니다:
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%KAKAO_APP_KEY%&libraries=services,clusterer,drawing"></script>
example/디렉토리에.env.example과dev.mjs(환경 변수 주입 dev 서버)가 포함되어 있습니다.cp .env.example .env후 키를 입력하고node dev.mjs로 실행하세요.
2. Lustre 앱에서 사용
import lustre/effect
import lustre/element/html
import lustre_kakaomap as kakao
import lustre_kakaomap/coords
import lustre_kakaomap/events
import lustre_kakaomap/marker
type Msg {
MapClicked(coords.LatLng)
MapIdle
}
// View - 지도 컨테이너 렌더링
fn view(model) {
html.div([], [
kakao.map(id: "mymap", attributes: []),
])
}
// Init - 지도 초기화 + 이벤트 구독
fn init(_) {
#(model, effect.batch([
kakao.init(id: "mymap", options: [
kakao.center(coords.seoul()),
kakao.level(3),
]),
events.on_click(id: "mymap", handler: MapClicked),
events.on_idle(id: "mymap", handler: fn() { MapIdle }),
]))
}
// Update - 클릭 시 마커 추가
fn update(model, msg) {
case msg {
MapClicked(pos) -> #(
model,
marker.add(id: "mymap", marker_id: "m1", options: [
marker.position(pos),
marker.title("클릭!"),
]),
)
MapIdle -> #(model, effect.none())
}
}
모듈 목록
핵심 (Stage 1)
| 모듈 | 설명 |
|---|---|
lustre_kakaomap | 지도 초기화, 뷰 렌더링, 지도 제어 + 상태 조회 (Getter) |
lustre_kakaomap/coords | LatLng, LatLngBounds, Point, Size + 한국 도시 프리셋 + 거리/방위각 계산 |
lustre_kakaomap/types | MapTypeId, ControlPosition, StrokeStyle 열거형 |
lustre_kakaomap/events | 지도, 마커, 도형 이벤트 구독 + 개별 이벤트 해제 (listen/off) |
lustre_kakaomap/marker | 마커 오버레이 + 선언적 마커 동기화 (marker.sync) |
lustre_kakaomap/info_window | 인포윈도우 팝업 + 타입 안전 콘텐츠 + 선언적 동기화 |
lustre_kakaomap/custom_overlay | HTML 기반 커스텀 오버레이 + 타입 안전 콘텐츠 + 선언적 동기화 |
lustre_kakaomap/preset | 조합 가능한 Map 프리셋 시스템 (파이프라인 API) |
도형 (Stage 2)
모든 도형 모듈은 파이프라인 빌더 API(from |> color |> fill |> draw)와 clear() 함수를 제공합니다.
| 모듈 | 설명 |
|---|---|
lustre_kakaomap/polyline | 폴리라인 (파이프라인 API) |
lustre_kakaomap/polygon | 다각형 (파이프라인 API) |
lustre_kakaomap/circle | 원 (파이프라인 API) |
lustre_kakaomap/ellipse | 타원 (파이프라인 API) |
lustre_kakaomap/rectangle | 사각형 (파이프라인 API) |
고급 기능 (Stage 3)
| 모듈 | 설명 | 필요 라이브러리 |
|---|---|---|
lustre_kakaomap/roadview | 로드뷰 (스트리트뷰) + 상태 조회 Getter | 기본 SDK |
lustre_kakaomap/services/places | 장소 검색 (키워드, 카테고리) + 타입 안전 CategoryCode | services |
lustre_kakaomap/services/geocoder | 주소<->좌표 변환 | services |
lustre_kakaomap/clusterer | 마커 클러스터링 + 이벤트 확장 | clusterer |
유틸리티 (Stage 4)
| 모듈 | 설명 | 필요 라이브러리 |
|---|---|---|
lustre_kakaomap/static_map | 정적 지도 (비인터랙티브 이미지) | 기본 SDK |
lustre_kakaomap/drawing | 그리기 도구 + 데이터 추출 + undo/redo 상태 | drawing |
lustre_kakaomap/geojson | GeoJSON 좌표 변환 헬퍼 (순수 Gleam, FFI 불필요) | 없음 |
lustre_kakaomap/url | 카카오맵 URL 빌더 (순수 Gleam, FFI 불필요) | 없음 |
프리셋 시스템
자주 쓰는 지도 설정을 파이프라인으로 조합합니다.
import lustre_kakaomap/preset
import lustre_kakaomap/coords
// 읽기 전용 지도 (임베드용)
preset.readonly()
|> preset.with_center(coords.jeju())
|> preset.with_level(5)
|> preset.apply(id: "embed_map")
// 사용 가능한 프리셋:
preset.clean_map() // 기본 로드맵
preset.satellite() // 위성 뷰
preset.hybrid() // 하이브리드 (위성 + 도로명)
preset.readonly() // 읽기 전용 (드래그/줌 비활성화)
preset.full_control() // 모든 인터랙션 활성화
파이프라인 친화적 도형 API
모든 도형 모듈은 from |> color |> fill |> draw 파이프라인 패턴을 지원합니다.
import lustre_kakaomap/polyline
import lustre_kakaomap/circle
import lustre_kakaomap/coords
import lustre_kakaomap/types
// 폴리라인 - 경로선
polyline.from([coords.seoul(), coords.daejeon(), coords.busan()])
|> polyline.color("#FF0000")
|> polyline.weight(3)
|> polyline.style(types.Dash)
|> polyline.opacity(0.8)
|> polyline.arrow()
|> polyline.draw(id: "mymap", shape_id: "route")
// 원 - 반경 표시
circle.from(coords.seoul(), radius: 5000.0)
|> circle.color("#3366FF")
|> circle.fill(color: "#CFE7FF", opacity: 0.3)
|> circle.draw(id: "mymap", shape_id: "area")
// 모든 도형 한번에 제거
circle.clear(id: "mymap")
Map 상태 조회 (Getter)
지도의 현재 상태를 콜백으로 읽어올 수 있습니다.
import lustre_kakaomap as kakao
type Msg {
GotCenter(coords.LatLng)
GotLevel(Int)
GotBounds(coords.LatLngBounds)
GotMapType(types.MapTypeId)
}
// update 함수에서:
kakao.get_center(id: "mymap", handler: GotCenter)
kakao.get_level(id: "mymap", handler: GotLevel)
kakao.get_bounds(id: "mymap", handler: GotBounds)
kakao.get_map_type(id: "mymap", handler: GotMapType)
타입 안전한 오버레이 콘텐츠
Lustre Element를 직접 오버레이 콘텐츠로 사용할 수 있습니다. 원시 HTML 문자열 대신 타입 안전한 마크업을 제공합니다.
import lustre/element/html
import lustre/element
import lustre/attribute
import lustre_kakaomap/info_window
import lustre_kakaomap/custom_overlay
// 기존 방식 (raw HTML 문자열)
info_window.content("<div style='padding:5px'>Hello</div>")
// 타입 안전한 방식 (Lustre Element)
info_window.content_element(
html.div([attribute.style("padding", "5px")], [
element.text("Hello"),
])
)
// custom_overlay에도 동일하게 사용
custom_overlay.content_element(
html.div([attribute.style("background", "#fff")], [
element.text("Kakao HQ"),
])
)
이벤트 구독 해제
Named listener로 이벤트를 구독하고 개별적으로 해제할 수 있습니다.
import lustre_kakaomap/events
// Named listener로 이벤트 구독
events.listen(
id: "mymap", event: "click",
listener_id: "my_click",
handler: MapClicked,
)
// 상태 이벤트용
events.listen_simple(
id: "mymap", event: "idle",
listener_id: "my_idle",
handler: fn() { MapIdle },
)
// 개별 리스너 해제
events.off(id: "mymap", listener_id: "my_click")
// 전체 named 리스너 해제
events.off_all(id: "mymap")
선언적 동기화
모델의 리스트를 지도와 자동 동기화합니다. 새 항목 추가, 사라진 항목 제거, 변경된 항목 업데이트를 자동 처리합니다.
import lustre_kakaomap/marker
import lustre_kakaomap/info_window
import lustre_kakaomap/custom_overlay
import lustre_kakaomap/coords
// 마커 동기화
marker.sync(id: "mymap", markers: [
#("seoul", coords.seoul(), "서울시청"),
#("busan", coords.busan(), "부산역"),
])
// 인포윈도우 동기화
info_window.sync(id: "mymap", info_windows: [
#("iw1", "<div>서울</div>", coords.seoul()),
#("iw2", "<div>부산</div>", coords.busan()),
])
// 커스텀 오버레이 동기화
custom_overlay.sync(id: "mymap", overlays: [
#("label1", "<div>Seoul</div>", coords.seoul()),
])
좌표 유틸리티
import lustre_kakaomap/coords
// 한국 도시 프리셋
coords.seoul() // 서울시청
coords.busan() // 부산역
coords.pangyo() // 판교 카카오 본사
// + daegu, incheon, gwangju, daejeon, ulsan, sejong, jeju
// 거리 계산 (Haversine 공식, 미터 단위)
let distance = coords.distance(from: coords.seoul(), to: coords.busan())
// 약 325,000.0 (325km)
// 방위각 계산 (도 단위, 0-360)
let bearing = coords.bearing(from: coords.seoul(), to: coords.busan())
// 약 165도 (남남동)
// 목적지 좌표 계산 (방위각 + 거리)
let dest = coords.destination(from: coords.seoul(), bearing_deg: 90.0, distance_m: 1000.0)
// 서울에서 동쪽으로 1km 지점
// 좌표 오프셋
let shifted = coords.offset(coords.seoul(), lat_offset: 0.01, lng_offset: -0.01)
// Bounds 유틸리티
let bounds = coords.bounds_from_list([coords.seoul(), coords.busan(), coords.jeju()])
let center = coords.bounds_center(bounds)
let overlaps = coords.bounds_overlap(bounds_a, bounds_b)
GeoJSON 좌표 변환
import lustre_kakaomap/geojson
import lustre_kakaomap/polyline
// GeoJSON은 [경도, 위도] 순서 - 이 모듈이 자동으로 LatLng(위도, 경도)로 변환
let path = geojson.line_coords([
#(126.978, 37.566), // 서울 (경도, 위도)
#(129.042, 35.114), // 부산
])
// 바로 폴리라인으로 그리기
geojson.draw_line_string(path,
id: "mymap", shape_id: "route",
options: [polyline.stroke_color("#FF0000")],
)
URL 빌더 (FFI 불필요)
import lustre_kakaomap/url
import lustre_kakaomap/coords
// 지도 바로가기 링크
url.map_link_named(name: "서울시청", position: coords.seoul())
// -> "https://map.kakao.com/link/map/서울시청,37.5665,126.978"
// 자동차 경로 링크
let from = url.named_location(name: "서울역", position: coords.seoul())
let to = url.named_location(name: "부산역", position: coords.busan())
url.route_by(mode: url.Car, from:, to:)
// 지하철 경로
url.subway_route(region: url.SeoulSubway, from: "판교역", to: "강남역")
// 검색 결과
url.search(query: "강남 카페")
장소 검색
import lustre_kakaomap/services/places
import lustre_kakaomap/services/status
// 키워드 검색 (SDK에 &libraries=services 필요)
places.keyword_search(
keyword: "강남 맛집",
options: [places.size(15), places.sort(status.Accuracy)],
handler: fn(search_status, results) { GotPlaces(search_status, results) },
)
// 타입 안전 카테고리 검색 (문자열 코드 대신 CategoryCode 사용)
places.category_search_by(
category: places.Cafe,
options: [places.location(coords.seoul()), places.radius(1000)],
handler: fn(search_status, results) { GotCafes(search_status, results) },
)
17개 카테고리 코드를 타입으로 제공합니다: Mart, ConvStore, School, Academy, Parking, GasStation, Subway, Bank, Culture, Brokerage, PublicInst, Attraction, Lodge, Restaurant, Cafe, Hospital, Pharmacy
주소<->좌표 변환
import lustre_kakaomap/services/geocoder
// 주소 -> 좌표
geocoder.address_search(
address: "서울시 강남구 역삼동",
handler: fn(status, results) { GotAddress(status, results) },
)
// 좌표 -> 주소
geocoder.coord2_address(
position: coords.seoul(),
handler: fn(status, addr, road_addr) { GotReverseGeocode(status, addr, road_addr) },
)
마커 클러스터링
import lustre_kakaomap/clusterer
// 클러스터러 초기화 (SDK에 &libraries=clusterer 필요)
clusterer.init(id: "mymap", clusterer_id: "c1", options: [
clusterer.grid_size(60),
clusterer.min_cluster_size(2),
])
// 마커를 클러스터러에 추가
clusterer.add_marker(id: "mymap", clusterer_id: "c1", marker_id: "m1")
// 클러스터 이벤트
clusterer.on_cluster_click(id: "mymap", clusterer_id: "c1", handler: fn(pos) { ClusterClicked(pos) })
clusterer.on_cluster_over(id: "mymap", clusterer_id: "c1", handler: fn(pos) { ClusterHovered(pos) })
로드뷰
import lustre_kakaomap/roadview
// 로드뷰 초기화
roadview.init(id: "rv", options: [
roadview.init_pan(0.0),
roadview.init_tilt(0.0),
])
// 가장 가까운 파노라마 찾기
roadview.get_nearest_pano_id(
position: coords.seoul(),
radius: 50.0,
handler: fn(pano_id) { GotPanoId(pano_id) },
)
// 현재 상태 조회
roadview.get_pano_id(id: "rv", handler: fn(pid) { GotCurrentPano(pid) })
roadview.get_viewpoint_state(id: "rv", handler: fn(vp) { GotViewpoint(vp) })
roadview.get_position(id: "rv", handler: fn(pos) { GotRvPosition(pos) })
그리기 도구
import lustre_kakaomap/drawing
// 그리기 매니저 초기화 (SDK에 &libraries=drawing 필요)
drawing.init(
map_id: "mymap",
drawing_id: "draw1",
modes: [drawing.DrawPolyline, drawing.DrawPolygon, drawing.DrawCircle],
options: [drawing.stroke_color("#FF0000"), drawing.removable(True)],
)
// 그리기 모드 선택
drawing.select(map_id: "mymap", drawing_id: "draw1", mode: drawing.DrawPolygon)
// 그려진 데이터 추출 (JSON)
drawing.get_data(map_id: "mymap", drawing_id: "draw1", handler: fn(json) { GotDrawingData(json) })
// Undo/Redo 가능 여부 확인
drawing.get_undoable(map_id: "mymap", drawing_id: "draw1", handler: fn(can) { CanUndo(can) })
drawing.get_redoable(map_id: "mymap", drawing_id: "draw1", handler: fn(can) { CanRedo(can) })
자세한 API 문서는 https://hexdocs.pm/lustre_kakaomap에서 확인할 수 있습니다.
개발
gleam deps download # 의존성 다운로드
gleam build # 빌드
gleam test # 테스트 실행
gleam format # 코드 포맷팅
gleam docs build # 문서 생성