Leaflet은 오픈 소스 자바스크립트 라이브러리로, 주로 OSM(OpenStreetMap) 같은 무료 지도 데이터를 사용하거나, 타일 기반의 지도 데이터를 불러와 사용할 수 있다. 기본적인 사용 방법만 익히면 별도의 API KEY 설정 없이 간단하게 사용 가능하고, 비용 지불의 걱정이 없다. react-leaflet 같은 라이브러리를 통해 React에서도 만나볼 수 있는데, 이에 대해 포스트에서 다루어 보았다.
OpenStreetMap
위 사진은 React에서 구현한 Leaflet 지도 샘플이다. 이 지도들은 OpenStreetMap의 데이터를 사용한다. OpenStreetMap(OSM)은 전 세계의 누구나 자유롭게 편집하고 사용할 수 있는 오픈 소스 지리 정보 데이터베이스이다. OSM은 Google Map과 같은 전통적인 지도 서비스와 달리 커뮤니티 기반으로 운영되며, 사용자가 지도를 편집하고 업데이트할 수 있도록 허용하는 방식으로 만들어졌다.
OpenStreetMap 데이터는 Leaflet 같은 라이브러리와 함께 사용되어 웹에서 다양한 형태의 지도 서비스를 만들 수 있다.
Leaflet 기본 설정
먼저 React에서 react-leaflet, leaflet 두 개의 패키지를 설치한다. react-leaflet가 React 환경에서 Leaflet을 사용할 수 있도록 컴포넌트화된 라이브러리지만, leaflet에 latLngBounds 등의 기타 관리 기능이 포함되어 있기 때문에 두 개 다 설치되어야 한다.
$ npm install leaflet react-leaflet
Leaflet의 기본 구조는 MapContainer, TileLayer로 구성된다. 여기서 MapContainer는 지도를 렌더링하는 기본 컨테이너 역할을 하고, TileLayer는 지도 타일을 렌더링하는 역할을 한다.
간단한 예제로, 기본값의 지도 컴포넌트를 구현해 보았다.
import { MapContainer, TileLayer } from "react-leaflet"
import L from "leaflet"
import "leaflet/dist/leaflet.css"
// 초기 중심 위치 설정
const position = [36.17, 127.83]
// 최대 경계 설정
const bounds = L.latLngBounds(
[32.5, 123.5], // 남서 좌표 (제주 남서쪽)
[39.0, 132.0] // 북동 좌표 (강원도 북동쪽)
)
function App() {
return (
<div>
<MapContainer
center={position} // 초기 중심 좌표
zoom={8.0} // 초기 줌 레벨
zoomSnap={0.5} // 줌 레벨 스냅
maxBounds={bounds} // 최대 경계 설정
maxBoundsViscosity={1.0} // 경계의 견고 정도 제어 (1.0일 경우 완전히 견고해져 경계 밖으로 드래그 불가)
style={{ width: "100vw", height: "100vh" }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"
maxZoom={20} // 최대 줌 레벨
minZoom={8.0} // 최소 줌 레벨
/>
</MapContainer>
</div>
)
}
export default App
초기 중심 위치로 대한민국 중심 부근을 임의 지정하였고, 국내 이외의 지역은 확인이 되지 않도록 최대 경계를 설정해 두었다. 경계를 지정하지 않으면 해외까지 자유자재로 이동이 가능하다. 그리고 style 설정을 보면 100vw, 100vh의 길이를 설정한 것을 확인할 수 있는데, 이처럼 크기를 명시해야 지도가 렌더링되는 것을 볼 수 있다.
마커 추가하기
지도에 표시할 중요한 정보는 주로 특정 위치에 마커로 표시하는 경우가 있다. Leaflet에서는 Marker 컴포넌트를 사용하여 이러한 마커를 표시할 수 있다.
import { MapContainer, TileLayer, Marker } from "react-leaflet"
import L from "leaflet"
import "leaflet/dist/leaflet.css"
import markerIcon from "leaflet/dist/images/marker-icon.png"
// 초기 중심 위치 설정
const position = [36.17, 127.83]
// 최대 경계 설정
const bounds = L.latLngBounds(
[32.5, 123.5], // 남서 좌표 (제주 남서쪽)
[39.0, 132.0] // 북동 좌표 (강원도 북동쪽)
)
// 서울과 대구의 좌표 설정
const seoulPosition = [37.5665, 126.978] // 서울
const daeguPosition = [35.8722, 128.6025] // 대구
function App() {
return (
<div>
<MapContainer
center={position} // 초기 중심 좌표
zoom={8.0} // 초기 줌 레벨
zoomSnap={0.5} // 줌 레벨 스냅
maxBounds={bounds} // 최대 경계 설정
maxBoundsViscosity={1.0} // 경계의 견고 정도 제어 (1.0일 경우 완전히 견고해져 경계 밖으로 드래그 불가)
style={{ width: "100vw", height: "100vh" }}
>
<TileLayer
url="https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"
maxZoom={18} // 최대 줌 레벨
minZoom={8.0} // 최소 줌 레벨
/>
{/* 서울 마커 */}
<Marker
position={seoulPosition}
icon={L.icon({ iconUrl: markerIcon })}
/>
{/* 대구 마커 */}
<Marker
position={daeguPosition}
icon={L.icon({ iconUrl: markerIcon })}
/>
</MapContainer>
</div>
)
}
export default App
서울, 대구의 좌표를 설정하고 각각 표시해 보았다. Marker 컴포넌트는 위치를 position 속성으로 전달받아 해당 위치에 마커를 표시한다.
커스텀 마커 스타일
Leaflet에서는 기본적으로 제공되는 마커 스타일이 있지만, 사용자 정의 마커를 사용할 수도 있다. L.divIcon을 이용해 커스텀 마커를 생성할 수 있으며, 예를 들어 이미지를 포함한 마커를 만들 수 있다.
import L from "leaflet"
const customIcon = L.divIcon({
className: "custom-marker",
html: `
<div style="display: flex; flex-direction: column; align-items: center;">
<img src="path-to-your-icon.png" style="width: 25px; height: 41px;" />
</div>
`,
})
위와 같은 방식으로 customIcon을 Marker에 적용하면 커스텀 스타일을 가진 마커를 지도에 표시할 수 있다. 하지만 직관적으로 알맞게 커스텀이 되지는 않기 때문에, 실제로 적용할 경우 보다 세밀한 스타일 조정이 필요해 보인다.
Leaflet은 Marker 외에도 Vector layers, Tooltips 등 다양한 옵션을 제공한다.
다양한 유형의 OpenStreetMap
Leaflet 라이브러리의 최대 장점은 바로 다양한 유형의 지도를 적용 가능하다는 것이다. 앞선 예제 기반으로 다른 지도 유형을 적용해 보았다.
import { MapContainer, TileLayer } from "react-leaflet"
import L from "leaflet"
import "leaflet/dist/leaflet.css"
// 초기 중심 위치 설정
const position = [36.17, 127.83]
// 최대 경계 설정
const bounds = L.latLngBounds(
[32.5, 123.5], // 남서 좌표 (제주 남서쪽)
[39.0, 132.0] // 북동 좌표 (강원도 북동쪽)
)
function App() {
return (
<div style={{ display: "flex" }}>
<MapContainer
center={position} // 초기 중심 좌표
zoom={8.0} // 초기 줌 레벨
zoomSnap={0.5} // 줌 레벨 스냅
maxBounds={bounds} // 최대 경계 설정
maxBoundsViscosity={1.0} // 경계의 견고 정도 제어 (1.0일 경우 완전히 견고해져 경계 밖으로 드래그 불가)
style={{ width: "50vw", height: "100vh" }}
>
<TileLayer url="https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png" />
</MapContainer>
<MapContainer
center={position} // 초기 중심 좌표
zoom={8.0} // 초기 줌 레벨
zoomSnap={0.5} // 줌 레벨 스냅
maxBounds={bounds} // 최대 경계 설정
maxBoundsViscosity={1.0} // 경계의 견고 정도 제어 (1.0일 경우 완전히 견고해져 경계 밖으로 드래그 불가)
style={{ width: "50vw", height: "100vh" }}
>
<TileLayer
url="https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.{ext}"
ext="jpg" // 확장자를 jpg로 설정
/>
</MapContainer>
</div>
)
}
export default App
서로 다른 두 개의 지도를 띄워 보았다. ext 설정과 같이 지도 유형에 따라 설정 사항, 표시 요소의 형태 등이 다를 수 있으므로 주의하여 적용해야 한다. 이 외에도 다른 다양한 지도 유형과 코드 정보는 Leaflet Provider Demo에서 찾아볼 수 있다.