@jykim0507/hd-map
v2.0.2
Published
**HD 정밀지도를 Vue 3 화면에 띄워 주는 라이브러리.** 도로 중심선, 신호등, 횡단보도, 안전표지 같은 정밀지도 요소를 `addLinkLayer()` 처럼 **함수 한 줄로 켜고 끌 수 있다.**
Readme
@jykim0507/hd-map
HD 정밀지도를 Vue 3 화면에 띄워 주는 라이브러리.
도로 중심선, 신호등, 횡단보도, 안전표지 같은 정밀지도 요소를 addLinkLayer() 처럼 함수 한 줄로 켜고 끌 수 있다.
내부적으로는 MapLibre GL JS 로 지도를 그리고, 지도 데이터는 PMTiles 파일 하나에 담겨 있다.
| 항목 | 내용 |
|-----------|-------------------------------------------------------------------------------|
| 지원 지역 | 화성시만 (경도 126.72 ~ 126.85 / 위도 37.16 ~ 37.29) |
| 줌 범위 | 10 ~ 20 (정밀지도 레이어는 대부분 줌 14 이상에서만 보인다) |
| 필요 환경 | Vue 3 · MapLibre GL JS v5 |
| 좌표계 | WGS84 (EPSG:4326) — 일반적인 [경도, 위도] |
💡 다른 지역은 추후 추가 예정이며 지원 방식은 아직 정해지지 않았다.
pmtilesUrl로 다른 타일 파일을 넘길 수는 있지만, 패키지에 들어 있는 원본 GeoJSON·표지판 데이터는 화성시 기준이다.
목차
| 구분 | 장 | 한 줄 요약 |
|------|----|-----------|
| 필수 | 0. 먼저 알아둘 용어 | 지도·소스·레이어가 뭔지 |
| 필수 | 1. 설치 | npm 한 줄 |
| 필수 | 2. 지도 띄우기 | 복사해서 붙이면 지도가 뜬다 |
| 필수 | 3. 레이어 켜고 끄기 | 도로·신호등·표지판 표시 |
| 선택 | 4. 로딩 상태 표시 | onProgress · onMapReady |
| 선택 | 5. 색과 테마 | 다크/라이트, 레이어 색 바꾸기 |
| 선택 | 6. 마커 | 차량 아이콘 하나 꽂기 |
| 선택 | 7. 내 레이어 얹기 | 직접 만든 데이터 그리기 |
| 선택 | 8. 원본 데이터로 계산하기 | 보호구역 진입 판정 등 |
| 선택 | 9. 정리 (cleanup) | 지도 파기 |
| 참고 | 10. 자주 하는 실수 | 안 될 때 여기부터 |
| 참고 | 부록. MapLibre 기본 사용 | 카메라·팝업 등 |
1 ~ 3장까지만 읽으면 지도가 뜬다. 나머지는 필요할 때 찾아 읽으면 된다.
0. 먼저 알아둘 용어
지도 라이브러리를 처음 쓴다면 이 네 단어만 알면 된다.
| 용어 | 뜻 | 비유 |
|------|----|------|
| 지도(map) | 화면에 그려지는 MapLibre 지도 객체 | 도화지 |
| 소스(source) | 지도에 그릴 데이터 (좌표 묶음) | 물감 |
| 레이어(layer) | 소스를 어떻게 그릴지 (선·면·점, 색, 굵기) | 붓질 한 번 |
| PMTiles | 지도 데이터를 한 파일에 압축해 둔 형식 (hwaseong.pmtiles) | 물감 통 |
이 라이브러리는 소스 등록은 알아서 해 주고, 레이어는 addXxxLayer() 함수로 켜고 removeXxxLayer() 로 끄게 해 준다.
1. 설치
npm install @jykim0507/hd-map maplibre-gl pmtiles설치가 끝나면 지도 데이터 파일 hwaseong.pmtiles (약 32MB) 가 내 프로젝트의 public/ 폴더로 자동 복사된다.
⚠️ 프로젝트에
public/폴더가 없으면 설치 중ENOENT에러가 난다. 폴더를 먼저 만들고 설치할 것. ⚠️public/hwaseong.pmtiles가 안 보이면 지도가 빈 화면으로 뜬다. 설치 후 파일이 있는지 꼭 확인.
2. 지도 띄우기
아래 코드를 컴포넌트에 그대로 붙여 넣으면 된다.
<template>
<!-- 지도가 그려질 영역. 높이가 꼭 있어야 한다 -->
<div id="map" style="width: 100%; height: 100vh;" />
</template>
<script setup>
import { onMounted } from 'vue'
import { useMap } from '@jykim0507/hd-map'
import '@jykim0507/hd-map/style'
// ① 라이브러리 준비 (아직 지도는 안 그려짐)
const mapLib = useMap({ pmtilesUrl: '/hwaseong.pmtiles' })
onMounted(async () => {
// ② 지도 생성 — 'map' 은 위 div 의 id
await mapLib.initMap('map', { theme: 'light' })
// ③ 레이어 켜기 — 반드시 initMap 이 끝난 뒤에
mapLib.addLinkLayer() // 도로 중심선
mapLib.addC1Layer() // 신호등 전체
await mapLib.createSignLayer() // 안전표지 (create* 는 await)
})
</script>흐름 한눈에 보기
useMap() → initMap() → add*Layer()
준비 지도 생성 레이어 켜기
(await 필수)| 단계 | 하는 일 | 주의 |
|------|--------|------|
| useMap() | 설정만 저장. 지도는 아직 없다 | 보통 <script setup> 최상단. Pinia 스토어 안에서도 가능 (9장 참고) |
| initMap() | 지도 생성 + 도로 밑바탕 자동 추가 | onMounted 안에서, await 필수 |
| add*Layer() | 원하는 레이어 표시 | initMap 이 끝난 뒤에만 동작 |
⚠️ 지도 영역(div)에 높이가 없으면 지도가 안 보인다. ⚠️
initMap을await하지 않고 레이어를 추가하면Style is not done loading에러가 난다. (initMap을 아예 부르기 전이면 에러 없이 무시된다) 💡 도로 밑바탕(addRoadBaseLayer)은initMap이 자동으로 추가한다.
mapLib 과 mapLib.map 의 차이
const mapLib = useMap({ ... }) // 이 라이브러리의 함수 모음 (레이어 켜고 끄기, 마커 등)
const map = mapLib.map // MapLibre 원본 지도 객체 (카메라 이동, 이벤트, 직접 레이어 추가)| 쓰고 싶은 것 | 사용 |
|-------------|------|
| 도로·신호등 등 정밀지도 레이어 켜고 끄기 | mapLib.addXxxLayer() |
| 카메라 이동, 클릭 이벤트, 내 레이어 추가 | mapLib.map.xxx() (부록) |
⚠️
mapLib.map을 Piniastate나reactive에 넣을 때는markRaw()로 감쌀 것. 감싸지 않으면 Vue 가 지도 객체 내부 전체를 추적해서 느려지거나 오작동할 수 있다.state.map = markRaw(mapLib.map)
useMap 옵션
const mapLib = useMap({ pmtilesUrl: '/hwaseong.pmtiles', zoom: 16 })| 옵션 | 타입 | 기본값 | 설명 |
|--------------|----------|-----------------------|---------------------------------------------------|
| pmtilesUrl | String | 필수 | PMTiles 파일 경로 (public/ 기준) |
| center | Array | [126.8312, 37.1995] | 처음 보여줄 중심 [경도, 위도] |
| zoom | Number | 18 | 처음 줌 |
| pitch | Number | 0 | 기울기 (0 ~ 85) |
| bearing | Number | 0 | 회전 각도 (0 ~ 360) |
| onMapReady | Function | - | 지도 준비 완료 시 호출 (map) => void (4장) |
| onProgress | Function | - | 로딩 중이면 true, 끝나면 false 로 호출 (isLoading) => void (4장) |
initMap 옵션
await mapLib.initMap('map', { theme: 'dark', minZoom: 12 })| 옵션 | 타입 | 기본값 | 설명 |
|-----------------------|--------|------------|------------------------|
| theme | String | 'dark' | 'dark' 또는 'light' |
| center | Array | useMap 값 | 처음 중심 |
| zoom | Number | useMap 값 | 처음 줌 |
| pitch / bearing | Number | useMap 값 | 기울기 / 회전 |
| minZoom / maxZoom | Number | 0 / 22 | 줌 제한 |
⚠️
theme은initMap에만 넣는다.useMap에 넣으면 조용히 무시된다. 가장 자주 하는 실수. 💡center·zoom·pitch·bearing은 양쪽 다 넣을 수 있고, 둘 다 넣으면initMap값이 우선이다.
3. 레이어 켜고 끄기
mapLib.addLinkLayer() // 켜기
mapLib.removeLinkLayer() // 끄기규칙은 세 가지뿐이다.
await initMap()이 끝난 뒤에 호출한다.- 이미 켜진 레이어를 또 켜거나, 없는 레이어를 꺼도 에러가 나지 않는다. (안심하고 호출해도 됨)
- 끈 레이어를 다시 보려면
add*를 다시 호출한다.
💡 줌 13 이하에서는 노드 · 도로 밑바탕 · 도로 중심선 · 노면 차선만 보인다. 나머지는 켜 두어도 줌 14 이상에서 나타난다. 마커(
addIcon)와 직접 추가한 레이어(7장)는 줌과 상관없이 항상 보인다.
이름 규칙 — add 는 바로, create 는 await
| 이름으로 시작 | 동작 | 끄는 함수 |
|--------------|------|-----------|
| add* | 호출 즉시 표시 (동기) | remove* |
| create* | 이미지를 불러오느라 시간이 걸림 → await 필요 | delete* |
현재 create* 는 createSignLayer 하나뿐이다. 끄는 deleteSignLayer() 는 바로 동작하므로 await 가 필요 없다.
await mapLib.createSignLayer() // 레이어 호출 순서의 맨 마지막에 두면 신경 쓸 일이 없다💡
await를 빼도 표지판은 잠시 뒤 뜬다. 하지만 바로 뒤에서 레이어 존재를 확인하거나, 표지판 위에 다른 레이어를 얹거나, 실패를 잡아야 한다면await가 필요하다.await를 쓸 수 없는 자리라면 최소한.catch(e => console.error(e))를 붙일 것.
A. 도로 네트워크
| 켜기 | 끄기 | 보이는 것 | 원본 데이터 |
|----------------------|--------------------------|------------------------------|-----------------------|
| addNodeLayer() | removeNodeLayer() | 노드 (도로 연결점) | A1_NODE |
| addRoadBaseLayer() | removeRoadBaseLayer() | 도로 밑바탕 (자동 추가됨) | A2_LINK |
| addLinkLayer() | removeLinkLayer() | 도로 중심선 | A2_LINK |
| addKind1Layer() | removeKind1Layer() | 특수 주행 구간 (터널, 교량 등) | A3_DRIVEWAYSECTION |
| addKind7Layer() | removeKind7Layer() | 자율주행 금지 구간 | A3_DRIVEWAYSECTION |
| addSidewalkLayer() | removeSidewalkLayer() | 보도 | A4_SUBSIDIARYSECTION |
| addBicycleLayer() | removeBicycleLayer() | 자전거 도로 | A4_SUBSIDIARYSECTION |
💡
addKind7Layer()는 자율주행 금지 구간에 마우스를 올리면 팝업(🚫 + 구간 설명)이 자동으로 뜬다.
B. 교통 표지 · 노면 표시
| 켜기 | 끄기 | 보이는 것 | 원본 데이터 |
|-----------------------------|----------------------------|-----------------|--------------------|
| await createSignLayer() | deleteSignLayer() | 안전표지 아이콘 | B1_SAFETYSIGN |
| addSurfaceLineLayer() | removeSurfaceLineLayer() | 노면 차선 | B2_SURFACELINEMARK |
| addCrosswalkLayer() | removeCrosswalkLayer() | 횡단보도 | B3_SURFACEMARK |
C. 도로 시설물
| 켜기 | 끄기 | 보이는 것 | 원본 데이터 |
|-----------------------------|--------------------------------|---------------------|----------------------------|
| addVehicleLightLayer() | removeVehicleLightLayer() | 차량용 신호등 | C1_TRAFFICLIGHT |
| addPedestrianLightLayer() | removePedestrianLightLayer() | 보행자/자전거 신호등 | C1_TRAFFICLIGHT |
| addEtcLightLayer() | removeEtcLightLayer() | 기타 신호등 | C1_TRAFFICLIGHT |
| addGuardrailLayer() | removeGuardrailLayer() | 차량방호안전시설 | C3_VEHICLEPROTECTIONSAFETY |
| addSpeedbumpLayer() | removeSpeedbumpLayer() | 과속방지턱 | C4_SPEEDBUMP |
| addHeightBarrierLayer() | removeHeightBarrierLayer() | 높이장애물 | C5_HEIGHTBARRIER |
| addPostPointLayer() | removePostPointLayer() | 지주 | C6_POSTPOINT |
| addStationLayer() | removeStationLayer() | 버스 정류장 | TB_MS_STATION |
묶음으로 한 번에 켜기
| 켜기 | 끄기 | 포함되는 레이어 |
|----------------|-------------------|---------------------------------------------------|
| addA2Layer() | removeA2Layer() | 도로 밑바탕 + 도로 중심선 |
| addA3Layer() | removeA3Layer() | 특수 주행 구간 + 자율주행 금지 구간 |
| addA4Layer() | removeA4Layer() | 보도 + 자전거 도로 |
| addC1Layer() | removeC1Layer() | 차량용 + 보행자 + 기타 신호등 |
⚠️ 묶음 함수에 색을 넘기면 묶인 레이어 전부 같은 색이 된다. 예:
addA2Layer('#f00')→ 도로 밑바탕과 중심선 둘 다 빨강. 레이어마다 다른 색을 주려면 개별 함수를 따로 부를 것.
체크박스로 켜고 끄기 예시
<template>
<label><input type="checkbox" v-model="showLink" /> 도로 중심선</label>
<div id="map" style="height: 100vh;" />
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { useMap } from '@jykim0507/hd-map'
import '@jykim0507/hd-map/style'
const mapLib = useMap({ pmtilesUrl: '/hwaseong.pmtiles' })
const showLink = ref(true)
onMounted(async () => {
await mapLib.initMap('map')
mapLib.addLinkLayer()
})
watch(showLink, (on) => {
on ? mapLib.addLinkLayer() : mapLib.removeLinkLayer()
})
</script>4. 로딩 상태 표시 (onProgress · onMapReady)
지도가 준비되는 동안 "불러오는 중" 화면을 띄우고 싶을 때 쓴다.
| 콜백 | 언제 호출되나 | 받는 값 |
|------|-------------|--------|
| onProgress | initMap 시작할 때 true, 준비가 끝나면 false | isLoading (Boolean) |
| onMapReady | 지도 준비가 끝났을 때 한 번 | map (MapLibre 인스턴스) |
호출 순서
initMap() 호출
└ onProgress(true) ← 로딩 시작
... 지도 생성, 데이터 연결 ...
└ onProgress(false) ← 로딩 끝
└ onMapReady(map) ← 이제 지도 사용 가능<template>
<div v-if="isLoading" class="loading">지도 불러오는 중...</div>
<div id="map" style="width: 100%; height: 100vh;" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useMap } from '@jykim0507/hd-map'
import '@jykim0507/hd-map/style'
const isLoading = ref(false)
const mapLib = useMap({
pmtilesUrl: '/hwaseong.pmtiles',
onProgress: (loading) => { isLoading.value = loading }, // true / false
onMapReady: (map) => { console.log('지도 준비 완료', map) },
})
onMounted(() => mapLib.initMap('map'))
</script>
<style scoped>
.loading {
position: fixed; inset: 0; z-index: 100;
display: flex; align-items: center; justify-content: center;
background: rgba(0, 0, 0, 0.6); color: #fff;
}
</style>💡
onProgress는 퍼센트(%)가 아니라 true/false 만 알려준다. "로딩 중인지 아닌지" 만 확인하는 용도. 💡 지도 생성에 실패해도onProgress(false)가 호출되므로 로딩 화면이 영원히 남지 않는다. 💡initMap을 다시 부르면onProgress(true)부터 다시 시작한다. 💡await mapLib.initMap()뒤에 코드를 쓰는 것과onMapReady안에 쓰는 것은 같은 시점이다. 편한 쪽을 쓰면 된다.
5. 색과 테마
테마 바꾸기 (dark / light)
await mapLib.initMap('map', { theme: 'light' }) // 처음 테마
mapLib.changeTheme('dark') // 실행 중 변경 — 이미 그려진 레이어에도 바로 반영⚠️
changeTheme()을 부르면add*('#색')으로 직접 지정한 색이 테마 색으로 덮어써진다. 필요하면 테마를 바꾼 뒤remove*()→add*('#색')으로 다시 칠할 것.
레이어 하나의 색 바꾸기
mapLib.addLinkLayer('#ff0000') // 문자열: 그 레이어의 대표 색 하나만 교체
mapLib.addKind7Layer({ kind7Fill: '#ff00ff' }) // 객체: 색상 키를 직접 지정 (아래 표 참고)| 넘기는 값 | 동작 | 언제 |
|----------|------|------|
| 문자열 '#ff0000' | 그 레이어의 대표 색 교체 | 대부분 이걸로 충분 |
| 객체 { 키: 색 } | 색상 키를 직접 지정. 여러 개 가능 | 한 레이어가 색을 2개 이상 쓸 때 |
⚠️ 색은 레이어를 새로 만들 때만 적용된다. 이미 켜진 레이어에
add*('#색')을 다시 불러도 안 바뀐다 →remove*()후 다시add*('#색'). ⚠️ 도로 밑바탕은initMap이 이미 만들었으므로removeRoadBaseLayer()→addRoadBaseLayer('#색')순서로.
레이어별 기본 색
| 함수 | 색상 키 | dark (기본) | light |
|-----------------------------|--------------|-------------|-----------|
| addNodeLayer() | node | #6a8dc1 | #BBBBBB |
| addRoadBaseLayer() | roadBase | #232b3b | #FCFCFC |
| addLinkLayer() | roadLink | #1565c0 | #62abff |
| addKind1Layer() | kind1Fill | #66bb6a | #66bb6a |
| addKind7Layer() | kind7Fill | #ff7043 | #ff7043 |
| addSidewalkLayer() | subsidiary | #1f2533 | #F5F5F5 |
| addBicycleLayer() | bicycle | #242b3b | #eeeeee |
| addVehicleLightLayer() | vehicle | #ef5350 | #ef5350 |
| addPedestrianLightLayer() | pedestrian | #66bb6a | #66bb6a |
| addEtcLightLayer() | etc | #ffd54f | #ffd54f |
| addGuardrailLayer() | guardrail | #8b9baf | #BBBBBB |
| addHeightBarrierLayer() | barrier | #9b8365 | #CCCCCC |
| addPostPointLayer() | postpoint | #ffffff | #999999 |
| addStationLayer() | station | #FFFFFF | #E0E0E0 |
색을 바꿀 수 없는 레이어
아래 레이어에 색을 넘기면 에러 없이 무시된다.
| 함수 | 이유 | 바꾸려면 |
|-------------------------|----------------------------------------------------------------------|--------------------|
| createSignLayer() | PNG 아이콘 | 아이콘 이미지 교체 |
| addCrosswalkLayer() | 줄무늬 패턴 이미지 (crosswalk 색이 이미지에 들어감) | changeTheme() |
| addSpeedbumpLayer() | 고정 패턴 (노랑/검정) | 패턴 생성 코드 수정 |
| addSurfaceLineLayer() | 차선 종류별 고정 색 (황색 #FFD700 / 백색 #E0E0E0 / 청색 #42A5F5) | 라이브러리 수정 |
배경(background)과 하늘(sky) 색은 테마 전용이라 changeTheme() 로만 바뀐다.
6. 마커
이미지 하나를 원하는 좌표에 꽂는다. (예: 차량 한 대 위치 표시)
import carIcon from '@/assets/car.png'
await mapLib.addIcon(carIcon, {
id: 'car-1', // 필수 — 나중에 지울 때 쓰는 이름
lngLat: [126.8312, 37.1995], // 필수 — [경도, 위도]
size: 0.3, // 선택 — 기본 0.25
anchor: 'bottom', // 선택 — 기본 'bottom' (이미지 아래쪽이 좌표에 닿음)
})
mapLib.removeIcon('car-1') // 하나 삭제
mapLib.removeAllIcons() // 전부 삭제반환값: { imageId, sourceId, layerId } → 각각 icon-image-{id}, icon-source-{id}, icon-layer-{id}
⚠️
addIcon은id·lngLat이 없거나initMap전에 부르면 에러 없이 콘솔 경고만 남기고undefined를 반환한다.
마커를 움직이려면 (실시간 차량 위치 등)
removeIcon → addIcon 을 반복하면 깜빡인다. 좌표만 바꿀 때는 소스 데이터를 갱신한다.
const moveCar = (lng, lat) => {
mapLib.map.getSource('icon-source-car-1').setData({
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: { type: 'Point', coordinates: [lng, lat] },
properties: { id: 'car-1' },
}],
})
}💡 같은 아이콘을 여러 지점에 쓰거나 속성에 따라 아이콘을 바꾸려면 → 7장 아이콘 레이어
7. 내 레이어 얹기
여기부터는 MapLibre 원본 객체 mapLib.map 을 직접 쓴다.
방법 1. 정밀지도 데이터를 골라서 그리기 (소스 추가 불필요)
라이브러리가 이미 등록해 둔 소스 'martin-hd-map' 에서 원하는 것만 필터해서 그린다.
// 예: 어린이보호구역만 빨갛게
mapLib.map.addLayer({
id: 'kids-zone', // 내가 정하는 이름 (중복 불가)
type: 'fill', // 면으로 그리기
source: 'martin-hd-map', // 라이브러리가 등록해 둔 소스
'source-layer': 'a3_drivewaysection', // 아래 표에서 선택
filter: ['==', ['get', 'Remark'], '어린이보호구역'],
paint: { 'fill-color': '#ff0000', 'fill-opacity': 0.4 },
})| source-layer | type | 주요 필터 필드 |
|------------------------------|--------|----------------------|
| a1_node | circle | - |
| a2_link | line | LinkID, MaxSpeed |
| a2_roadbase_buffer | fill | - |
| a3_drivewaysection | fill | Kind, Remark |
| a4_subsidiarysection | fill | SubType |
| b1_safetysign | fill | Type, Remark |
| b2_surfacelinemark | line | Type |
| b3_surfacemark | fill | Type, Kind |
| c1_trafficlight | circle | Kind |
| c3_vehicleprotectionsafety | line | - |
| c4_speedbump | fill | - |
| c5_heightbarrier | line | - |
| c6_postpoint | circle | - |
| tb_ms_station | circle | StationID |
필드 값의 의미(코드표)는 docs/DATA.md 참고.
⚠️
source-layer이름을 틀리면 에러 없이 아무것도 안 그려진다. 안 보이면 오타부터 의심.
방법 2. 내 데이터(GeoJSON) 올리기
const fc = {
type: 'FeatureCollection',
features: [
{ type: 'Feature', geometry: { type: 'Point', coordinates: [126.8312, 37.1995] }, properties: {} },
],
}
// 1) 처음 한 번: 소스 + 레이어 추가
mapLib.map.addSource('my-source', { type: 'geojson', data: fc })
mapLib.map.addLayer({
id: 'my-layer', type: 'circle', source: 'my-source',
paint: { 'circle-radius': 6, 'circle-color': '#ff0000' },
})
// 2) 데이터가 바뀔 때: setData 로 갱신
mapLib.map.getSource('my-source').setData(newFc)⚠️ 갱신할 때
addSource를 다시 부르면already exists에러가 난다. 갱신은 항상setData. ⚠️features오타(featrues)는Cannot read properties of undefined (reading 'length')로 나타난다. 에러에 파일명이 안 나와서 찾기 어렵다.
아이콘을 쓰는 레이어 만들기
같은 아이콘을 여러 지점에 찍을 때는 이미지만 등록하고 symbol 레이어에서 이름으로 가져다 쓴다.
import busIcon from '@/assets/bus.png'
await mapLib.addCustomImage(busIcon, 'bus') // 1) 이미지 등록 — 반드시 await
mapLib.map.addLayer({ // 2) 등록한 이름 'bus' 로 사용
id: 'bus-stops',
type: 'symbol',
source: 'my-source',
layout: {
'icon-image': 'bus',
'icon-size': 0.4,
'icon-allow-overlap': true,
},
})속성에 따라 아이콘을 바꾸려면 이미지를 여러 개 등록하고 표현식으로 고른다.
'icon-image': ['match', ['get', 'type'], 'bus', 'bus', 'stop', 'stop-icon', 'bus']
// type 값이 'bus'면 'bus', 'stop'이면 'stop-icon', 그 외 'bus'| | addIcon (6장) | addCustomImage |
|-----------|---------------------------------|---------------------------|
| 만드는 것 | 이미지 + 소스 + 레이어 한 번에 | 이미지만 |
| 위치 | 좌표 하나 | 내 소스의 모든 지점 |
| 삭제 | removeIcon / removeAllIcons | 지도를 파기하면 같이 사라짐 |
| 이럴 때 | 차량 한 대 같은 개별 마커 | 정류장 여러 개 같은 반복 아이콘 |
⚠️ 이미지 등록 전에 레이어를 추가하면 아이콘이 안 보인다.
addCustomImage를await한 뒤addLayer. ⚠️addCustomImage는 이미지를 불러오지 못해도 에러를 던지지 않고 콘솔에만 남긴다.try/catch로는 잡히지 않으니, 아이콘이 안 보이면 콘솔부터 확인할 것.
8. 원본 데이터로 계산하기
화면에 그리기만 → PMTiles (자동) / 좌표로 계산 → 원본 GeoJSON (직접 불러오기)
지도에 그려진 데이터(PMTiles)는 화면용으로 좌표가 살짝 뭉개져 있다. 보호구역 진입 판정처럼 정확한 좌표가 필요한 계산은 원본 GeoJSON 으로 해야 한다.
import a3Url from '@jykim0507/hd-map/data/a3_drivewaysection.geojson?url'
const a3 = await (await fetch(a3Url)).json()
const kind7 = a3.features.filter(f => String(f.properties.Kind) === '7') // 자율주행 금지 구간만⚠️
fetch('/node_modules/...')는 개발 중에만 되고 빌드 후 깨진다. 반드시?urlimport 로 쓸 것. ⚠️ PMTiles 좌표는 줌 14 기준 약 0.48m 단위로 반올림되고 타일 경계에서 잘린다. 안전 관련 판정은 반드시 원본으로.
| 파일 (@jykim0507/hd-map/data/) | 쓰임새 |
|--------------------------------------|----------------------|
| a1_node.geojson | 노드 좌표 |
| a2_link.geojson | 링크 좌표 · 경로 매칭 |
| a3_drivewaysection.geojson | 보호구역 진입 판정 |
| a4_subsidiarysection.geojson | 보도 · 자전거도로 |
| b1_safetysign.geojson | 안전표지 |
| b2_surfacelinemark.geojson | 노면 차선 |
| b3_surfacemark.geojson | 노면표시 |
| c1_trafficlight.geojson | 신호등 |
| c3_vehicleprotectionsafety.geojson | 차량방호안전시설 |
| c4_speedbump.geojson | 과속방지턱 |
| c5_heightbarrier.geojson | 높이장애물 |
| c6_postpoint.geojson | 지주 |
| tb_ms_station.geojson | 정류장 좌표 |
💡 파일이 수 MB 라서 매번 통째로 받으면 느리다. 필요한 필드만 뽑은 인덱스를 빌드 때 미리 만들어 두는 편이 낫다. 필드와 코드표는 docs/DATA.md.
9. 정리 (cleanup)
mapLib.cleanup()| 상황 | 직접 호출해야 하나? |
|------|-------------------|
| 컴포넌트 setup 안에서 useMap 을 부른 경우, 컴포넌트가 사라질 때 | ❌ 자동 |
| Pinia 스토어 등 컴포넌트 밖에서 useMap 을 부른 경우 | ✅ 직접 호출 (onBeforeUnmount 에서) |
| initMap() 을 다시 부를 때 | ❌ 기존 지도를 자동 정리 후 새로 생성 |
| 컴포넌트는 그대로인데 지도만 없애고 싶을 때 | ✅ 직접 호출 |
정리되는 것: 이벤트 리스너, addIcon 마커, 등록한 이미지, 팝업, 지도 객체.
⚠️ 직접 추가한 레이어·소스(7장)도 지도와 함께 사라진다. 지도를 다시 만들면 다시 추가해야 한다. 💡
?url로 불러둔 원본 데이터(8장)는 내 변수에 있으므로 영향 없다. 다시 fetch 할 필요 없다.
10. 자주 하는 실수
| 증상 | 원인 | 해결 |
|------|------|------|
| 지도가 아예 안 보임 | 지도 div 에 높이 없음 | style="height: 100vh" 등 높이 지정 |
| 지도는 뜨는데 배경만 보임 | public/hwaseong.pmtiles 없음 / 경로 틀림 | 파일 위치와 pmtilesUrl 확인 |
| Style is not done loading 에러 | await initMap() 없이 레이어 추가 | await 후에 add*Layer() |
| 레이어가 안 보임 | 줌이 14 미만 | 줌을 14 이상으로 확대 |
| 테마가 안 바뀜 | theme 을 useMap 에 넣음 | initMap('map', { theme }) 으로 이동 |
| 색이 안 바뀜 | 이미 켜진 레이어에 add*('#색') | remove*() 후 다시 add*('#색') |
| 내 레이어가 안 보임 | source-layer 오타 | 7장 표에서 이름 확인 |
| already exists 에러 | 갱신할 때 addSource 재호출 | getSource(id).setData() 사용 |
| 마커가 깜빡임 | removeIcon → addIcon 반복 | 6장처럼 setData |
| 빌드 후 GeoJSON 404 | fetch('/node_modules/...') | ?url import 사용 |
| 설치 중 ENOENT | 프로젝트에 public/ 폴더 없음 | public/ 폴더 만든 뒤 재설치 |
부록. MapLibre 기본 사용
mapLib.map 은 MapLibre 원본 객체라서 MapLibre 기능을 전부 쓸 수 있다.
공식 문서: https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/
카메라 제어
| 함수 | 기능 | 매개변수 |
|-----------------------|-------------------------------|----------------------------------------------|
| setCenter(center) | 중심 이동 | [lng, lat] |
| getCenter() | 현재 중심 반환 | - |
| setZoom(zoom) | 줌 설정 | Number |
| getZoom() | 현재 줌 반환 | - |
| setPitch(pitch) | 기울기 설정 | Number (0~85) |
| getPitch() | 현재 기울기 반환 | - |
| setBearing(bearing) | 회전 설정 | Number (0~360) |
| getBearing() | 현재 회전 반환 | - |
| flyTo(options) | 날아가듯 이동 (애니메이션) | { center, zoom, bearing, pitch, duration } |
| easeTo(options) | 부드럽게 이동 | { center, zoom, bearing, pitch, duration } |
| jumpTo(options) | 애니메이션 없이 즉시 이동 | { center, zoom, bearing, pitch } |
mapLib.map.flyTo({ center: [126.8312, 37.1995], zoom: 18, duration: 1500 })클릭 이벤트
// 지도 아무 곳이나 클릭
mapLib.map.on('click', (e) => {
console.log('클릭 좌표', e.lngLat)
})
// 특정 레이어를 클릭했을 때만
mapLib.map.on('click', 'my-layer', (e) => {
console.log('클릭한 데이터', e.features[0].properties)
})마커 (MapLibre 기본)
import maplibregl from 'maplibre-gl'
const marker = new maplibregl.Marker({ color: '#ff0000' })
.setLngLat([126.8312, 37.1995])
.addTo(mapLib.map)
marker.remove() // 삭제팝업
import maplibregl from 'maplibre-gl'
new maplibregl.Popup({ closeButton: false })
.setLngLat([126.8312, 37.1995])
.setHTML('<p>팝업 내용</p>')
.addTo(mapLib.map)레이어 / 소스
| 함수 | 기능 |
|--------------------------------------|---------------------------|
| addSource(id, source) | 데이터 소스 추가 |
| removeSource(id) | 데이터 소스 삭제 |
| getSource(id) | 소스 가져오기 (없으면 undefined) |
| addLayer(layer) | 레이어 추가 |
| removeLayer(id) | 레이어 삭제 |
| getLayer(id) | 레이어 가져오기 (없으면 undefined) |
| setLayoutProperty(id, name, value) | 레이어 layout 속성 변경 |
| setPaintProperty(id, name, value) | 레이어 paint 속성 변경 (색 등) |
⚠️ 소스를 지우려면 그 소스를 쓰는 레이어를 먼저 지워야 한다. (
removeLayer→removeSource)
관련 문서
| 문서 | 내용 | |------------------------------|-----------------------------------------------| | docs/DATA.md | 레이어별 속성 필드, 코드표, 데이터 간 연관관계 |
