본문으로 건너뛰기 Leaflet 완벽 가이드 | 오픈소스 지도·마커·GeoJSON·인터랙션·실전 활용

Leaflet 완벽 가이드 | 오픈소스 지도·마커·GeoJSON·인터랙션·실전 활용

Leaflet 완벽 가이드 | 오픈소스 지도·마커·GeoJSON·인터랙션·실전 활용

이 글의 핵심

Leaflet으로 인터랙티브 지도를 구현하는 완벽 가이드. 마커, 팝업, GeoJSON, 레이어, React/Vue 통합까지 실전 예제로 정리. Leaflet·Maps·GeoJSON 중심으로 설명합니다. Start now.

이 글의 핵심

Leaflet으로 인터랙티브 지도를 구현하는 완벽 가이드입니다. 마커, 팝업, GeoJSON, 레이어, React/Vue 통합까지 실전 예제로 정리했습니다.

실무 경험 공유: Google Maps API를 Leaflet으로 전환하면서, 비용이 100% 절감되고 커스터마이징이 자유로워진 경험을 공유합니다.

들어가며: “지도 API가 비싸요”

실무에서 마주치는 문제들

Google Maps 비용이 높아요

API 호출마다 비용이 듭니다. Leaflet은 무료 오픈소스입니다. 커스터마이징이 제한적이에요

Google Maps는 제한적입니다. Leaflet은 완전히 자유롭습니다. 오프라인 지도가 필요해요

클라우드 의존적입니다. Leaflet은 로컬 타일을 사용할 수 있습니다.

1. Leaflet이란?

핵심 특징

Leaflet은 오픈소스 JavaScript 지도 라이브러리입니다. 주요 장점

  • 무료: 오픈소스
  • 가벼움: 42KB
  • 모바일 친화적: 터치 제스처
  • 플러그인: 수백 개의 플러그인
  • 커스터마이징: 완전한 제어

2. 설치 및 기본 사용

설치

npm install leaflet
npm install -D @types/leaflet

기본 지도

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <style>
      #map { height: 400px; }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script>
      const map = L.map('map').setView([37.5665, 126.9780], 13);
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors',
      }).addTo(map);
    </script>
  </body>
</html>

3. 마커

기본 마커

import L from 'leaflet';
const map = L.map('map').setView([37.5665, 126.9780], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// 마커 추가
const marker = L.marker([37.5665, 126.9780]).addTo(map);
// 팝업
marker.bindPopup('서울특별시').openPopup();
// 커스텀 아이콘
const customIcon = L.icon({
  iconUrl: '/marker.png',
  iconSize: [32, 32],
  iconAnchor: [16, 32],
  popupAnchor: [0, -32],
});
L.marker([37.5665, 126.9780], { icon: customIcon }).addTo(map);

4. GeoJSON

const geojsonData = {
  type: 'FeatureCollection',
  features: [
    {
      type: 'Feature',
      properties: {
        name: 'Location 1',
        category: 'restaurant',
      },
      geometry: {
        type: 'Point',
        coordinates: [126.9780, 37.5665],
      },
    },
  ],
};
L.geoJSON(geojsonData, {
  onEachFeature: (feature, layer) => {
    layer.bindPopup(feature.properties.name);
  },
  pointToLayer: (feature, latlng) => {
    return L.circleMarker(latlng, {
      radius: 8,
      fillColor: '#ff7800',
      color: '#000',
      weight: 1,
      opacity: 1,
      fillOpacity: 0.8,
    });
  },
}).addTo(map);

5. React 통합

React Leaflet

npm install react-leaflet
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
export default function Map() {
  return (
    <MapContainer
      center={[37.5665, 126.9780]}
      zoom={13}
      style={{ height: '400px', width: '100%' }}
    >
      <TileLayer
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        attribution='© OpenStreetMap contributors'
      />
      <Marker position={[37.5665, 126.9780]}>
        <Popup>서울특별시</Popup>
      </Marker>
    </MapContainer>
  );
}

6. Vue 통합

<script setup lang="ts">
import { onMounted, ref } from 'vue';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
const mapContainer = ref<HTMLElement>();
onMounted(() => {
  if (!mapContainer.value) return;
  const map = L.map(mapContainer.value).setView([37.5665, 126.9780], 13);
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
  L.marker([37.5665, 126.9780])
    .addTo(map)
    .bindPopup('서울특별시')
    .openPopup();
});
</script>
<template>
  <div ref="mapContainer" style="height: 400px;"></div>
</template>

7. 이벤트

TypeScript/JavaScript 예제 코드입니다.

map.on('click', (e) => {
  console.log('Clicked at:', e.latlng);
  L.marker(e.latlng).addTo(map).bindPopup('You clicked here!').openPopup();
});
marker.on('click', () => {
  console.log('Marker clicked');
});

8. 플러그인

Leaflet.markercluster

npm install leaflet.markercluster
import L from 'leaflet';
import 'leaflet.markercluster';
const markers = L.markerClusterGroup();
locations.forEach((loc) => {
  const marker = L.marker([loc.lat, loc.lng]);
  markers.addLayer(marker);
});
map.addLayer(markers);

9. 내부 동작과 좌표계(EPSG:3857)

Leaflet은 기본적으로 Web Mercator(EPSG:3857) 타일을 가정합니다. 화면은 픽셀이지만, 지도 내부에서는 줌 레벨 z마다 세계를 2^z × 2^z 타일 격자로 나눕니다. {z}/{x}/{y} URL 패턴은 이 타일 주소를 가리킵니다. 경도·위도(WGS84)LatLng로 다루으며, 타일 계산은 내부적으로 투영 변환을 거칩니다.

실무 함의: GeoJSON은 좌표 순서가 [경도, 위도](RFC 7946)입니다. 실수로 반대로 넣으면 마커가 엉뚱한 해상에 찍힙니다. 한국에서 네이버·카카오 타일을 쓸 때는 각 제공자의 좌표계·URL·사용 약관을 별도로 확인해야 합니다.


10. 프로덕션 패턴

10.1 타일 서버와 사용 정책

OpenStreetMap 공용 타일(tile.openstreetmap.org)은 대량 트래픽·앱 상용 배포에 부적합할 수 있습니다. 서비스 규모가 커지면 자체 타일 서버, MapTiler·Mapbox 등 상용, 또는 정부/기관 제공 WMTS를 검토합니다. CDN 앞에 두면 지연·비용을 동시에 다루기 쉽습니다.

10.2 성능: 수천 개 마커

DOM 마커는 수가 늘면 느려집니다. 클러스터(markercluster)·Canvas 레이어·서버 측 클러스터링(줌별 집계) 중 하나를 선택합니다. 뷰포트 밖 데이터는 아예 요청하지 않도록 bbox 쿼리를 API에 두는 패턴이 흔합니다.

10.3 React Leaflet과 SSR

react-leaflet은 브라우저 전용 API에 의존하므로 Next.js 등 SSR에서는 dynamic(..., { ssr: false })로 지도 컴포넌트를 감싸 하이드레이션 불일치를 피합니다. 지도 높이가 0이면 타일이 안 보이므로 부모에 명시적 height를 주는 것이 첫 트러블슈팅 단계입니다.


11. 트러블슈팅

증상점검
회색 타일만 보임타일 URL·CORS·API 키·줌 범위 minZoom/maxZoom
마커 아이콘이 깨짐webpack/Vite가 default 아이콘 경로를 번들에서 누락 — Icon.Default.imagePath 보정 또는 정적 자산 경로 고정
모바일에서 더블탭 줌이 과함tap 옵션·터치 핸들러와 UI 버튼 충돌 여부
GeoJSON이 한참 떨어진 곳에 표시[lng, lat] 순서·CRS 불일치
메모리가 계속 증가이벤트 리스너·레이어를 remove()하지 않고 라우트만 바꾸는 SPA 패턴 — map.remove() 또는 레이어 정리

정리 및 체크리스트

핵심 요약

  • Leaflet: 오픈소스 지도 라이브러리
  • 무료: 오픈소스
  • 가벼움: 42KB
  • 모바일 친화적: 터치 제스처
  • GeoJSON: 지리 데이터
  • 플러그인: 수백 개

구현 체크리스트

  • Leaflet 설치
  • 기본 지도 구현
  • 마커 추가
  • 팝업 구현
  • GeoJSON 사용
  • React/Vue 통합
  • 플러그인 활용

같이 보면 좋은 글


이 글에서 다루는 키워드

Leaflet, Maps, GeoJSON, Location, React, Vue, Frontend

자주 묻는 질문 (FAQ)

Q. Google Maps와 비교하면 어떤가요?

A. Leaflet이 무료이고 커스터마이징이 자유롭습니다. Google Maps는 더 많은 기능을 제공하지만 비쌉니다.

Q. 한국 지도를 사용할 수 있나요?

A. 네, Kakao Map, Naver Map 타일을 사용할 수 있습니다.

Q. 모바일에서 잘 작동하나요?

A. 네, 터치 제스처에 최적화되어 있습니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 수많은 웹사이트에서 안정적으로 사용하고 있습니다.