npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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 을 Pinia state 나 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()   // 끄기

규칙은 세 가지뿐이다.

  1. await initMap() 이 끝난 뒤에 호출한다.
  2. 이미 켜진 레이어를 또 켜거나, 없는 레이어를 꺼도 에러가 나지 않는다. (안심하고 호출해도 됨)
  3. 끈 레이어를 다시 보려면 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/...') 는 개발 중에만 되고 빌드 후 깨진다. 반드시 ?url import 로 쓸 것. ⚠️ 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 | 레이어별 속성 필드, 코드표, 데이터 간 연관관계 |