6.1 지도 데이터 시각화6.2 Leaflet.js 소개 및 설치6.2.1 CDN으로 불러오기6.2.2 npm으로 설치하기6.2.3 React Leaflet 라이브러리 사용하기1. 생명주기2. 주의사항6.3 간단한 지도 생성하기6.3.1 MapContainer - 지도 영역 만들기6.3.2 TileLayer - 지도 보여주기6.4 지도 위에 표현하기6.4.1 Marker6.4.2 Tooltip, Popup 1. 인스턴스를 생성하여 bind하기2. 바로 bind하기6.5 CSV를 사용한 데이터 시각화6.6 지도 스타일링 및 커스터마이징6.6.1 좌표계 설정하기6.6.2 유용한 메서드 소개6.7 왜 D3로 하지 않고 Leaflet을 사용하는가?6.7.1 D3.js와 Leaflet의 차이점1. 데이터 시각화 vs 지도 생성2. 일반성 vs 특수성3. 코드 복잡성4. 커뮤니티와 문서5. 상호작용과 애니메이션6. 지리공간 기능
6.1 지도 데이터 시각화


지도 데이터 시각화란 위치 정보를 포함한 데이터를 지도 위에 표현하는 시각화 유형이다. 여기에서 위치 정보의 종류에는 위도,경도, 지역명, 주소 등이 있다.
지도 데이터 시각화의 종류는 다음과 같다.
- Dot map(점 지도)
- 빠르고 정확하게 위치를 지정하고 인사이트를 도출할 수 있다. 점의 밀도가 높을수록 정확한 확인이 어려워 확대, 축소가 가능한 인터랙티브 지도를 사용한다.
- Heat Map(히트맵)
- 데이터의 크기에 기초해 색을 달리하여 시각화한 차트. 데이터의 분포를 표현할 수 있다. 전체적인 데이터의 분포 또는 밀집도를 바탕으로 인사이트를 도출한다. ex) 인종 분포, 밀도 및 변동 추세
- Choropleth map(Field map, 단계 구분도)
- 지도 위 지역별(행정구역별) 영역에 따라 색을 달리하며, 수치형 데이터의 크기를 표현한다.
- Connection Map
- 공간과 시간을 모두 표현할 수 있어 다양한 상황에 활용 가치가 높다. ex) 운전 경로, 버스/지하철 노선 분포, 비행기경로 추척
- Flow Map
- 출발지와 목적지 간의 데이터를 선으로 연결한다. 경로(연결) 보다는 흐름에 초점을 두었다. 수치형 데이터의 크기에 따라 흐름을 나타내는 선의 굵기를 다르게 표현한다. ex) 교통 흐름, 인구 이동 경로, 항공 경로 등
- Symbol map(Bubble map, 도형 표현도)
- 데이터의 크기에 따라 도형의 크기를 다르게 표현한다.
- Cartogram(카토그램)
- 지연 실제 크기를 왜곡하여 데이터 크기에 따라 면적을 크고, 작게 표현하기 때문에 직관적으로 지역별 차이를 이해할 수 있다.
- Dorling map(돌링 카토그램)
- 지역의 모양을 도형을 시각화한다. 지역의 모양을 모두 같은 도형으로 바꾸고, 데이터의 수치를 표현하는 데 집중한다. 도형을 기존 지역 위치에 배치한다.
- Tile Grid map(타일 격자 지도)
- 모든 지역을 일정한 크기의 정사각형 모양으로 고정하고, 데이터 수치에 따라 색을 다르게 표현한다. 모든 지역의 크기가 일정해므로 지역의 크기와 관계없이 지역별 데이터를 쉽게 탐색할 수 있다.
6.2 Leaflet.js 소개 및 설치
지도를 처음 불러올 때 지도의 배치가 이상하거나 제대로 렌더링이 안되는 상황이 생긴다. 이는 Leaflet에서 제공하는 stylesheet를 추가하지 않았기 때문이다. 따라서 기본적으로 다음 CSS를 전체적으로 추가해야 한다.
<link rel="stylesheet" href="<https://unpkg.com/leaflet@1.9.4/dist/leaflet.css>" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
6.2.1 CDN으로 불러오기
<script src="<https://unpkg.com/leaflet@1.9.4/dist/leaflet.js>" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
6.2.2 npm으로 설치하기
앞서 개발 환경에서 React를 설치했기 때문에 추가적으로 설치해야 하는 것만 적었다. 이 예제에는
leaflet v 1.9.4
를 사용한다.npm install react-dom leaflet
6.2.3 React Leaflet 라이브러리 사용하기
앞서 Leaflet을 직접 사용할 때와 달리 이미 라이브러리에 Leaflet과 React-dom에 대한 의존성이 있기 때문에 해당 라이브러리만 설치하면 된다.
npm install react-leaflet
타입스크립트를 위한 타입 라이브러리는 다음과 같다.
npm install -D @types/leaflet
해당 라이브러리는 Leaflet 자체에서 렌더링된 결과를 DOM에 HTML요소로 렌더링한다.
처음 렌더링할 때는 Leaflet 인스턴스를 만들 때 넘겨주며, 기본적으로 불변한 객체로 다뤄진다. 인스턴스가 변할 수 있다고 명시적으로 적어주지 않는 한 업데이트되어도 UI를 바꾸진 않는다. 인스턴스의 메서드들을 부를 때 업데이트된 props가 반영된다. 명시하지 않는 한, 라이브러리는 Leaflet 요소 인스턴스나 DOM 요소를 참조한다.
React context API를 사용한다. 그러므로
MapContainer
컴포넌트에 의존적이다. 라이브러리에서 제공하는 React 훅들은 모두 MapContainer
하위 컴포넌트에서 사용해야 한다.1. 생명주기
1)
MapContainer
를 div 태그로 렌더링한다. → placeholder prop을 넘겨줬다면 div 태그 안에 해당 컴포넌트를 먼저 렌더링한다.
2)
props
와 함께 div
태그 내에 Leaflet Map
인스턴스를 만들고, 해당 지도 인스턴스를 포함한 React context
를 생성한다.3) 자식 컴포넌트들을 렌더링한다.
4) 각각의 자식 컴포넌트는
props
와 context
를 바탕으로 필요한 Leaflet 인스턴스
를 만들고, 지도에 추가한다.5) 자식 컴포넌트가 다시 렌더링되면,
props
의 변화를 지도에 적용한다.6) 컴포넌트가 렌더 트리에서 제거되면, 필요에 따라 지도 레이어를 제거한다.
2. 주의사항
Leaflet은 DOM에 직접 호출하기 때문에 SSR에는 적합하지 않다. 또한, DOM 요소가 아닌 Leaflet 레이어를 위한 추상화에 컴포넌트들이 노출된다. 몇몇 추상화들은 직접적으로 Leaflet의 setter를 호출하기 때문에, key prop을 통해 React 알고리즘에 의해 적절하게 처리할 수 있다.
6.3 간단한 지도 생성하기

간단한 지도 구현을 위해서는 React Leaflet을 사용하는 것이 편하다. 그러나 해당 라이브러리는 Leaflet을 완전히 대체할 수 없으므로 Leaflet을 React에 직접 사용하는 방식으로 코드를 작성하였다. 컴포넌트의 이름과 기능 등은 편의를 위해 React Leaflet과 비슷하게 만들기로 하였다.
앞으로 만들 컴포넌트들의 기능들을 먼저 정하고 간다.
<MapContainer />
- 지도 생성 및 Context api 전달.
<TileLayer />
- 실제 지도 구현. WMTS로 구현된 타일 지도를 가져온다.
6.3.1 MapContainer - 지도 영역 만들기
Leaflet에서 지도를 가져올 때 다음과 같은 코드가 필요하다.
const center = [0, 0]; //lat, lng 숫자로 이뤄진 배열 const zoomLevel = 13; //숫자가 클수록 확대된 지도를 얻을 수 있다. const options = {}; const map = L.map("지도를 보여줄 태그의 id"); map.setView(center, zoomLevel); L.tileLayer(url, options).addTo(map)
이는 DOM에 직접 접근하여 id를 가져와야 하기 때문에 useEffect 안에 넣어 컴포넌트가 마운트 된 직후에 실행할 수 있도록 한다.

<MapContainer />
는 Context api를 통해 하위 자식에게 지도와 관련한 props 들을 전달한다. - map: Context api를 사용하는 이유이다. 자세한 내용은
<TileLayer />
에서 알아보자.
- center: lat(위도)와 lng(경도)를 차례로 가진 배열을 주면 된다.
- zoom: 숫자값을 주며, 숫자가 클수록 더 확대된 지도를 얻을 수 있다.
주의할 점은 px, vw, vh 등의 단위로 고정 크기를 주어야 지도가 보인다. 물론 부모 컴포넌트에서 크기를 정해주고 %를 사용할 수도 있다.
컴포넌트의 형태를 잡아보자. Context api를 사용하기 때문에 하나의 Context를 선언해주고, Provider로 앞서 설정한 기본 정보들을 넘겨준다.
//components/MapContainer const MapContainer = ({ children, ...props }) => { return ( <LeafletContext.Provider value={initValue} {...props}> {children} </LeafletContext.Provider> ); }; export default MapContainer;
또한 Context api를 편하게 사용하기 위해
useMap
훅으로 분리한다. 자세한 코드는 예제 사이트에서 확인이 가능하다.여기에서 문제가 있다면, React는 단방향 데이터 통신을 한다. 즉, Context api를 통해 value를 넘겨준다면, 해당 value를 수정하기가 무척 까다롭다는 것이다. useMemo, useCallback 등을 통해 해결할 수 있는 방법을 제공하지만, 유연하게 지도를 다루기 위해 useReducer로 선언한 value와 dispatch를 넘겨줄 것이다.
//components/MapContainer import { useReducer } from "react"; import { LeafletContext } from "../../hooks/useMap"; const initValue = { map: null, center: [0, 0], zoom: 13, }; const reducer = (state, action) => { //..생략.. return state; }; const MapContainer = ({ children, ...props }) => { const [value, dispatch] = useReducer(reducer, initValue); return ( <LeafletContext.Provider value={{ value, dispatch }} {...props}> {children} </LeafletContext.Provider> ); }; export default MapContainer;
이렇게 하면 지도가 나올 수 있는 영역을 잡은 것이다. 앞으로 만들어갈 컴포넌트들이나 훅들은 모두
<MapContainer />
의 자식 컴포넌트에서 쓸 수 있도록 Context api를 적극 사용해야 한다.6.3.2 TileLayer - 지도 보여주기

앞서
<MapContainer />
로 지도의 영역을 만들었다면, 실제 지도는 <TileLayer />
컴포넌트로 가져와야 한다. 이때, Leaflet에서 제공하는
tilelayer
는 WMTS 방식으로 지도를 불러와야 한다.GIS 짧은 상식
- WMS: 이미지는 모서리의 좌표에 의해 정의된다. (Leaflet이 내부적으로 수행하는 계산)
- TMS(Tiled Map Service): 웹지도에 보다 초점을 맞춘 지도 타일링 표준으로, Leaflet이 L.TileLayer에서 기대하는 지도 타일과 매우 유사하다.
- WMTS(Web Map Tile Service): 표준 프로토콜. L.TileLayer에서 직접 사용할 수 있는 맵 타일을 제공한다.
더 많은 내용은 GIS를 공부하는 걸 추천한다. 네이버, 카카오 등 국내 지도 API를 사용하기 위해서는 QGIS라는 툴을 사용하여 TMS for Korea 플러그인을 통해 불러오는 편이 좋다.
다음 사이트에서는 오픈소스 중에서 불러올 수 있는 예시를 확인할 수 있다.
<MapContainer />
컴포넌트를 만들 때 context api를 사용한 걸 적용한다면 다음과 같다.import { useEffect } from "react"; import * as L from "leaflet"; import useMap from "../../hooks/useMap"; import useGeoLocation from "../../hooks/useGeoLocation"; const TileLayer = ({ url, attribution, options, ...props }) => { const { value, dispatch } = useMap(); useEffect(() => { if (!value.map) { const map = L.map("map"); //지도를 보여줄 태그의 id map.setView(value.center, value.zoom); dispatch({ type: "update", value: { center: value.center, map: map } }); L.tileLayer(url, { attribution, ...options, }).addTo(map); } }, []); return ( <div id="map" key={value.toString()} style={{ width: `100%`, height: `100%` }} {...props} ></div> ); }; export default TileLayer;
예제에서 가장 많이 사용하는 지도 오픈 소스를 url로 주어 쉽게 지도를 가져올 수 있다. 이 컴포넌트에서는 필수로 들어가는 prop은 url 하나이다.
이때 url은
"https://{s}.tile.domain.org/{z}/{x}/{y}.png"
형태로 사용한다. s는 서버 서브도메인, z는 level, x는 lat, y는 lng값을 가진다. domain
부분을 바꾸는 형태라면 다른 지도 소스를 가지고 와서 사용할 수도 있다. 물론, GeoJSON 형태의 데이터도 가능하며, 이는 별도의 챕터에서 이어서 설명한다.- attribution
- 지도의 캡션을 보여주며, 생략 가능하다.
<TileLayer />
최종 코드//components/TileLayer.jsx import { useEffect } from "react"; import * as L from "leaflet"; import useMap from "../../hooks/useMap"; const TileLayer = ({ url, attribution, options }) => { const { value, dispatch } = useMap(); useEffect(() => { if (!value.map) return; const tileLayer = L.tileLayer(url, { attribution, ...options, }); dispatch({ type: "create", value: { ...value, tileLayer }, }); }, []); return null; }; export default TileLayer;
6.4 지도 위에 표현하기
<Marker />
- 지도 마커 구현. id값을 기준으로 기존에 있는 마커의 경우와 새로운 마커인 경우를 구분하여 지도 위에 표시되는 마커를 생성한다. 마커의 역할에 따라 자식 컴포넌트를 Popup으로 할지, Tooltip으로 할지 결정한다.
<Popup />
- 지도 팝업 구현. 지도의 특정 위치 혹은 마커를 클릭 시 나오는 팝업을 생성한다.
<Tooltip />
- 지도 툴팁 구현. 특정 위치의 마커에 마우스를 올리면 나오는 툴팁을 생성한다.
6.4.1 Marker

지도 위의 특정 위치에 마커를 추가하거나, 삭제하는 기본적인 내용을 다룬다.
마커를 만들기 위해서 Leaflet은 다음과 같이 코드를 작성한다.
const latlng = L.latLng([lat, lng]); //lat: number, lng: number const options = {/**마커 옵션들*/}; const marker = L.marker(latlng, options).addTo(map);
마커를 생성하고 수정하고 삭제하는 생명주기 관련한 로직과 이벤트까지 다룰 수 있도록 훅으로 분리한다.
//hooks/useMarker.js import { useEffect, useState } from "react"; import * as L from "leaflet"; import useMap from "./useMap"; const useMarker = () => { const { value, dispatch } = useMap(); const currentMarkers = [...value.marker]; const [markers, setMarkers] = useState(currentMarkers); const [marker, setMarker] = useState(null); useEffect(() => { dispatch({ type: "update", value: { ...value, marker: markers }, }); }, [markers, value]); const isIncludeMarker = (markerId) => !currentMarkers.every((marker) => marker.id != markerId); const findMarker = (markerId) => currentMarkers.filter((marker) => marker.id === markerId)[0]; const exceptMarkers = (markerId) => currentMarkers.filter((marker) => marker.id != markerId); const createMarker = (markerId, latlng, options) => { if (isIncludeMarker(markerId)) { console.log("중복된 id의 마커가 있습니다."); return findMarker(markerId); } const newMarker = { id: markerId, marker: L.marker(latlng, options), }; setMarker(newMarker.marker); setMarkers((prev) => [...prev, newMarker]); return newMarker; }; const updateMarker = (markerId, latlng, options) => { if (!marker) return; const targetMarker = findMarker(markerId); const { marker: prevMarker, ...rest } = targetMarker; // 마커의 위치(latlng)를 업데이트 prevMarker.setLatLng(latlng); // 아이콘 업데이트 if (options?.icon) { prevMarker.setIcon(options.icon); } const copyMarker = { ...rest, marker: prevMarker, }; setMarker(copyMarker.marker); const otherMarkers = exceptMarkers(markerId); setMarkers([...otherMarkers, copyMarker]); return copyMarker; }; const deleteMarker = (markerId) => { const targetMarker = findMarker(markerId); const otherMarkers = exceptMarkers(markerId); targetMarker && targetMarker.marker.remove(); setMarkers([...otherMarkers]); setMarker(null); return otherMarkers; }; const useMarkerEvent = (event, callback, array) => { useEffect(() => { if (!marker) return; marker.on(event, callback); return () => { marker.off(event, callback); }; }, [marker, value, ...array]); }; return { createMarker, updateMarker, deleteMarker, useMarkerEvent, isIncludeMarker, findMarker, exceptMarkers, }; }; export default useMarker;
위의 훅을 통해서 간단하게 기본 마커를 만든다. 자식 컴포넌트를 받지 않으면 반환값이
null
이어도 된다. 후에 팝업을 추가하기 위해 여기에서는 children
을 받는다.//components/DefaultMarker.jsx import { useEffect } from "react"; import * as L from "leaflet"; import useMarker from "../../hooks/useMarker"; const DefaultMarker = ({ id, latlng: [lat, lng], options = {}, children, ...props }) => { const { createMarker, updateMarker, deleteMarker, isIncludeMarker } = useMarker(); const Latlng = L.latLng([lat, lng]); useEffect(() => { if (isIncludeMarker(id)) updateMarker(id, Latlng, options); else createMarker(id, Latlng, options); return () => { deleteMarker(id) }; }, []); return ( <b {...props}> {children} </b> ); }; export default DefaultMarker;
6.4.2 Tooltip, Popup

마커나 벡터 등을 사용할 때 정보를 담을 수 있는 툴팁과 팝업을 어떻게 추가하고 삭제할 수 있는지 다룬다.
툴팁이나 팝업을 추가하기 위해서는 두 가지 방법이 있다. 툴팁과 팝업 모두 비슷한 방법이므로 팝업을 예시로 든다.
1. 인스턴스를 생성하여 bind하기
const popup = L.popup() .setLatLng(latlng) .setContent('Hello world!<br />This is a nice tooltip.') .addTo(map); //이때 marker는 이미 지도에 추가되어 있어야 한다. marker.bindPopup(popup).openPopup();
2. 바로 bind하기
//이때 marker는 이미 지도에 추가되어 있어야 한다. marker.bindPopup("content").openPopup();
1번의 경우와 2번의 경우 모두 최종적으로 marker에 bind하는 형태라서 헷갈릴 수 있다. 그러나 1번의 경우 marker에 bind하지 않더라도 이미 자체적으로 latlng 값을 가지고 있으며, 지도에 추가했으므로 해당 좌표에서 팝업을 띄울 수 있다.
즉, 지도 위에 바로 툴팁과 팝업을 띄우고 싶다면 1번으로, 마커나 벡터 등과 연결하고 싶다면 2번을 사용하는 것이 좋다.
위의 내용을 토대로 Marker도 같이 수정하면 다음과 같다.
//components/CustomMarker.jsx import { useEffect, useRef, useState } from "react"; import * as L from "leaflet"; import useMapEvent from "../../hooks/useMapEvent"; import useMarker from "../../hooks/useMarker"; const CustomMarker = ({ latlng, options, children, id, tooltip, //tooltip을 props로 받으면서 popup과 구분 openPopup, //마커에 bind된 popup의 초기 상태 결정 onClick, //별도의 Marker 클릭 이벤트 핸들러 추가 ...props }) => { const { createMarker, updateMarker, deleteMarker, isIncludeMarker, useMarkerEvent, } = useMarker(); const markerRef = useRef(null); const [currentMarker, setCurrentMarker] = useState(null); const [showPopup, setShowPopup] = useState(openPopup); //아이콘 커스터마이징 const { iconUrl, iconSize, ...rest } = options; const Icon = L.icon({ iconUrl, iconSize }); const markerOptions = iconUrl ? { ...rest, icon: Icon } : rest ? { ...rest } : undefined; const Latlng = L.latLng(latlng); useEffect(() => { if (isIncludeMarker(id)) updateMarker(id, Latlng, .markerOptions); else createMarker(id, Latlng, markerOptions); return () => { deleteMarker(id); }; }, [Latlng]); useEffect(() => { if (currentMarker && markerRef.current) { currentMarker.bindPopup(markerRef.current); } }, [currentMarker, markerRef]); useMarkerEvent( "mouseover", (e) => { tooltip && e.target.bindTooltip(tooltip); }, [tooltip] ); useMarkerEvent( "click", (e) => { setCurrentMarker(e.target); setShowPopup(true); if (showPopup) e.target.openPopup(); onClick && onClick(e); }, [Latlng] ); useMapEvent( "popupclose", (e) => { setShowPopup(false); e.target._popup.closePopup(); }, [Latlng] ); return ( <dialog {...props} ref={markerRef}> {children} </dialog> ); }; export default CustomMarker;
팝업의 경우 툴팁보다 많은 정보를 받으므로 팝업을 children으로, 툴팁을 props로 받는다.
주의할 점은, children을 바로 받아서 넣을 수 없어 ref를 사용해 HTML 형태로 넣어준다.
이제 자체적으로 지도 위에 있는 popup을 만들어보자. marker와 마찬가지로 popup도 훅을 사용하여 생명주기에 따라 다뤄줄 것이다.
import { useEffect, useRef } from "react"; import usePopup from "../../hooks/usePopup"; const Popup = ({ id, latlng, children, popupoptions = {}, open, ...props }) => { const { createPopup, updatePopup, deletePopup, isIncludePopup } = usePopup(); const popupRef = useRef(null); useEffect(() => { if (!popupRef.current) return; const content = popupRef.current; if (isIncludePopup(id)) updatePopup(id, latlng, content, popupoptions, open); else createPopup(id, latlng, content, popupoptions, open); return () => { deletePopup(id); }; }, [open, id, popupRef]); return ( <dialog {...props} ref={popupRef}> {children} </dialog> ); }; export default Popup;
usePopup
훅의 경우 useMarker
와 비슷하니 직접 만들어보자😆6.5 CSV를 사용한 데이터 시각화

csv, 엑셀 데이터를 사용해서 지도에 마커를 표시하도록 한다. 이를 이용해 후에 지도를 활용한 분산 그래프 등을 그릴 수 있다.
간단하게 sheetJs(xlsx)를 사용하여 엑셀 파일을 가져오고 보여주고, 내보내는 컴포넌트 세 가지를 만든다. 이후 해당 데이터를 토대로 마커의 위치를 특정하고, 팝업 내용이 별도로 있다면 해당 내용을 마커의 자식요소로 추가할 수 있다.
엑셀 데이터를 binary 형식으로 읽어온 결과인 sheet를 부모 컴포넌트로 올려보낸다.
//component/FileInput.jsx import * as XLSX from "xlsx-js-style"; const FileInput = ({ setData }) => { const handleFileUpload = (e) => { const file = e.target.files[0]; const reader = new FileReader(); reader.onload = (event) => { const workbook = XLSX.read(event.target.result, { type: "binary" }); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; setData(sheet); }; reader.readAsBinaryString(file); }; return ( <form> <input type="file" onChange={handleFileUpload} /> </form> ); } export default FileInput;
최종적으로 수정한 파일을 엑셀파일로 내보낸다. 예제에서는 별도의 수정을 하지 않고 더미 데이터를 내보낸다.
//component/FileExport.jsx import { useState } from "react"; import { saveAs } from "file-saver"; import * as XLSX from "xlsx-js-style"; const FileExport = ({ data }) => { const [value, setValue] = useState("map"); //파일을 내보내는 이벤트 const exportToExcel = (e) => { e.preventDefault(); //데이터는 json으로 부터 sheet 형식으로 만들기때문에 배열값을 가져와야 한다. const worksheet = XLSX.utils.json_to_sheet(data); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "Sheet1"); const excelBuffer = XLSX.write(workbook, { bookType: "xlsx", type: "array", }); const blob = new Blob([excelBuffer], { type: "application/octet-stream" }); saveAs(blob, `${value}.xlsx`); }; return ( <form onSubmit={exportToExcel}> <input type="text" name="" id="" value={value} onChange={(e) => setValue(e.target.value)} /> <button type="submit">Export to Excel</button> </form> ); }; export default FileExport;
가져온 파일은 표 형식으로 사용자에게 보여준다.
import { createContext, useContext } from "react"; import * as XLSX from "xlsx-js-style"; //테이블을 구성하는 컴포넌트들은 기능상 큰 관여를 하지 않기 때문에 생략한다. //기존의 행 위치를 잡아주기 위한 함수 const fitRowNum = (sheetData, newData, maxRows) => { const newData = Array(maxRows).fill([]); sheetData.forEach((data) => { const currentRow = data.__rowNum__; newData[currentRow] = data; }); return newData; }; const XlsxSheet = ({ data, ...props }) => { const sheetData = XLSX.utils.sheet_to_json(data); const maxRows = sheetData[sheetData.length - 1] ? sheetData[sheetData.length - 1]["__rowNum__"] : 0; const maxCols = maxRows ? Math.max( ...sheetData.map((d) => { return Object.values(d).length; }) ) : 0; const value = { maxRows, maxCols}; const newData = fitRowNum(sheetData, maxRows); const filtedData = newData.filter((d) => isObject(d)); return ( <TableContext.Provider value={value}> <table {...props}> <Head data={filtedData[0]} /> <tbody> {filtedData.map((item, idx) => ( <Body key={"td" + idx} data={item} /> ))} </tbody> </table> </TableContext.Provider> ); }; export default XlsxSheet;
다음은 위의 컴포넌트들을 지도와 함께 사용하여 마커들을 표현하는 예제 코드이다.
//사용예시 import { useEffect, useMemo, useState } from "react"; import * as XLSX from "xlsx-js-style"; import MapContainer from "../../../components/map/MapContainer"; import TileLayer from "../../../components/map/TileLayer"; import CustomMarker from "../../../components/map/CustomMarker"; import useGeoLocation from "../../../hooks/useGeoLocation"; import FileInput from "../../../components/spread/FileInput"; import XlsxSheet from "../../../components/spread/XlsxSheet"; import FileExport from "../../../components/spread/FileExport"; const MapWithCSV = () => { const { loading, position } = useGeoLocation(); const [data, setData] = useState([[]]); const [markers, setMarkers] = useState([[]]); useEffect(() => { if (loading || !position) return; //더미 데이터를 다음과 같이 만들어준다. const initData = Array.from({ length: 10 }, (_, idx) => ({ id: `data-${idx}`, latitude: parseFloat(Math.random() / 20) + position[0], longitude: parseFloat(Math.random() / 20) + position[1], })); setMarkers(initData); }, [loading, position]); //더미 데이터의 형태를 바꾸어 엑셀 함수가 잘 읽을 수 있게 한다. const refinedData = useMemo(() => { const titleKeys = Object.keys(markers[0]); const rows = markers.map((item) => Object.values(item)); return [titleKeys, ...rows]; }, [markers]); useEffect(() => { if (!markers.length) return; const csvContent = refinedData.map((row) => row.join(",")).join("\n"); const workbook = XLSX.read(csvContent, { type: "string" }); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; setData(sheet); }, [markers]); return <main> <div style={{ width: `100%`, height: `50vh` }}> {!loading && ( <MapContainer id="map_04" center={position}> <TileLayer id="map_1" url={"https://tile.openstreetmap.org/{z}/{x}/{y}.png"} attribution={"04"} /> {markers.length > 0 && markers.map((d) => ( <CustomMarker key={d.id + d} id={d.id} latlng={[d.latitude, d.longitude]} ></CustomMarker> ))} </MapContainer> )} </div> <section> <XlsxSheet data={data}></XlsxSheet> <FileInput setData={setData}></FileInput> <FileExport data={markers}></FileExport> </section> </main> }; export default MapWithCSV;
6.6 지도 스타일링 및 커스터마이징
지도의 스타일링을 위해 다음 사이트에서 원하는 타일맵을 선택하는 것으로 시작하자.
6.6.1 좌표계 설정하기
대한민국 지도 API를 사용하는 등 url의 형태가 달라진다면, 어떤 좌표계를 사용하는 지에 따라 좌표계를 정의하는 등 사전에 추가해야 하는 설정이 있다. 그러나 좌표계에 대한 정확한 지식이 없다면 바꾸지 않는 것을 추천한다. 이는 공식문서에서 찾아볼 수 있는 내용이다.
Documentation - Leaflet - a JavaScript library for interactive maps
This reference reflects Leaflet . Check this list if you are using a different version of Leaflet.
Option | Type | Default | Description |
crs | L.CRS.EPSG3857 | The Coordinate Reference System to use.
Don't change this if you're not sure what it means. |
참고로 좌표계를 별도로 정의할 경우 다음과 같다.
// 좌표계 정의 const EPSG5181 = new L.Proj.CRS( 'EPSG:5181', '+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=500000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', { resolutions: [2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.25], origin: [-30000, -60000], bounds: L.bounds([-30000-Math.pow(2,19)*4, -60000], [-30000+Math.pow(2,19)*5, -60000+Math.pow(2,19)*5]) } ); map.options.crs = EPSG5181;
6.6.2 유용한 메서드 소개
이어서, 앞서 설명했던 MapContainer, Tilelayer, Marker, Popup, Tooltip 과 관련하여 유용한 메서드들을 소개하고자 한다.
- setView: 해당 좌표를 지도의 중심으로 만든다.
- fitBounds: 여러 개의 좌표 데이터를 토대로 화면에 모든 좌표가 보일 수 있게 zoom, center 등을 수정한다.
- flyTo: 현재 지도의 center에서 해당 좌표까지 이동한다. zoom 애니메이션이 기본적으로 동작한다.
- getCenter: 지도의 center를 얻거나, vector 등 여러 좌표의 가운데 지점을 계산하여 반환한다.
- getLatLng: 마커 등 지도에 bind된 좌표를 얻는다.
- openPopup/closePopup: 레이어에 bind된 팝업을 열고 닫는다.
공식문서에 더 많은 메서드들을 제공하고 있으나, 예제에서 사용한 메서드들을 기반으로 정리한다.
6.7 왜 D3로 하지 않고 Leaflet을 사용하는가?
6.7.1 D3.js와 Leaflet의 차이점
D3.js와 Leaflet은 웹에서 데이터 시각화와 지도 생성을 위한 두 가지 주요 JavaScript 라이브러리이다. 기능 면에서 유사한 점이 있지만, 몇 가지 주요 차이점이 존재한다.
1. 데이터 시각화 vs 지도 생성
D3.js는 데이터 시각화에 중점을 두고 있어 대화형 차트, 그래프, 데이터 시각적 표현을 만드는 도구를 제공한다. 반면, Leaflet은 대화형 지도 및 지리공간 시각화를 만들기 위해 설계된 라이브러리이다.
2. 일반성 vs 특수성
D3.js는 매우 유연하고 강력한 라이브러리로, 사용자가 SVG, HTML, CSS로 데이터를 조작하고 사용자 맞춤 시각화를 만들 수 있게 해준다. 하위 수준 프로그래밍 인터페이스를 제공하여 시각화의 세부적인 제어가 가능하다. 반면, Leaflet은 지도 생성에 특화된 간소화된 기능 세트를 제공하여, 지도와 관련된 작업을 더 쉽게 할 수 있다.
3. 코드 복잡성
D3.js는 일반 목적을 위한 라이브러리로, 학습 곡선이 가파르고, Leaflet보다 복잡하다. JavaScript, SVG, 데이터 조작 개념에 대한 깊은 이해가 필요하다. 반면, Leaflet은 간단한 API를 제공하며 웹 매핑을 처음 접하는 사람에게도 쉽게 이해된다.
4. 커뮤니티와 문서
D3.js는 크고 활발한 커뮤니티와 방대한 튜토리얼, 예제를 제공하는 반면, Leaflet의 커뮤니티는 상대적으로 작지만 지리 정보에 특화된 내용을 제공한다.
5. 상호작용과 애니메이션
D3.js는 상호작용과 애니메이션을 구현하는 다양한 도구를 제공하며, 전환 효과나 이벤트 처리가 가능하다. Leaflet은 지도 관련 상호작용에 집중하여 확대, 축소, 패닝, 마커 팝업과 같은 기능을 제공한다.
6. 지리공간 기능
D3.js도 일부 지리공간 기능을 제공하지만, Leaflet만큼 포괄적이지 않다. Leaflet은 지도 타일, 오버레이, 지리공간 데이터를 쉽게 처리할 수 있어, 지리적 정보가 중요한 프로젝트에 적합하다.
다시 정리하자면, D3.js는 데이터 시각화를 위한 강력하고 유연한 라이브러리로 광범위한 사용자 정의 기능을 제공한다. 반면 Leaflet은 간단한 API와 지리 데이터를 시각화하는 데 특화된 라이브러리이다.
D3.js를 사용해 지리 데이터를 시각화할 수 있지만, 지도와의 상호작용이 필요한 경우에는 Leaflet을 사용하는 것이 더 적합하다. 특히, geoJSON, CSV 등 다양한 형식의 지리 데이터를 처리할 수 있다는 점에서 Leaflet은 지리 데이터 활용에 뛰어난 라이브러리이다. D3.js와 Leaflet을 함께 사용해, 지도 위에 D3.js를 이용한 시각화를 추가하는 방법도 자주 사용된다.