-
[React] React-Map-Gl 라이브러리를 사용하여 지도 띄우고 마커 표시하기Frontend 2019. 8. 30. 21:09
바닐라코딩 과제로 Rest API를 이용한 과제가 주어졌었는데,
여러 가지 지도 오픈소스 중에 하나를 활용해야만 했다.
구글 맵, 카카오 맵 등등 많은 지도 오픈소스가 존재하지만
나는 그 중에서 맵박스(Mapbox)를 이용해서 만들었다.
그 이유는 다음과 같다.
1. 무료라서
2. 디자인이 예뻐서 + 기본 디자인 외에 다른 디자인으로 커스텀 가능
3. 한국에만 국한된 지도가 아니어서
근데 맵박스 자료를 찾아보니 Mapbox-gl이 있고
Uber(우버)에서 만든 React-map-gl이 있고
뭔가 엄청 복잡했다..ㄷㄷ..
나는 그 중에서 우버에서 만든 React-map-gl을 사용하기로 했다.
왜냐하면 문서가 깔끔하고 보기 쉽게 잘 만들어져있기 때문이다.
https://uber.github.io/react-map-gl/#/Documentation/getting-started/state-management
그리고 아래 유튜브 동영상은 처음 맵을 띄울 때
매우 도움이 많이 된 React-map-gl 튜토리얼이다.
https://www.youtube.com/watch?v=JJatzkPcmoI&feature=youtu.be
npm install --save react-map-gl
일단 react-map-gl을 install해준다.
import React, { useState, useEffect } from 'react'; import ReactMapGL from 'react-map-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; import './App.css';
리액트 훅을 이용해서 구현할 생각이므로 위와 같이 import한다.
const Mapbox = () => { const MAP_TOKEN = 'pk... your mapbox token'; const [ viewport, setViewport ] = useState({ latitude: 37.532600, longitude: 127.024612, width: '100vw', height: '100vh', zoom: 12 }); return ( <div className="Mapbox"> <ReactMapGL {...viewport} mapboxApiAccessToken={MAP_TOKEN} > </ReactMapGL> </div> ); }; export default Mapbox;
Mapbox 사이트에 가입한 후 만들어진 token을 사용한다.
viewport 객체에 초기값들은 자유롭게 지정하면 된다.
나는 지도를 브라우저 전체에 꽉 채울 생각이기 때문에
width와 height는 각각 100vw, 100vh로 지정하였다.
latitude와 longitude에는 처음 초기 장소의 위도, 경도를 입력한다.
zoom은 화면에 보여질 때 확대 정도를 설정하는 것으로
minZoom이나 maxZoom도 설정할 수 있다.
위 코드를 똑같이 따라 하면 이렇게 회색빛의 기본 지도가 화면에 띄워진다.
그러나 아직은 어떤 interaction도 없는 기본 상태이다.
return ( <div className="Mapbox"> <ReactMapGL {...viewport} mapboxApiAccessToken={MAP_TOKEN} mapStyle="mapbox://styles/mapbox/streets-v9" onViewportChange={(viewport) => { setViewport(viewport); }} > </ReactMapGL> </div> );
이제 지도를 움직일 수 있도록 만들어주자.
mapStyle에 인터넷에서 찾은 마음에 드는 지도 모양의 style명을 입력해주었다.
그리고 onViewportChange이벤트로 viewport 정보를 바꿔준다.
색깔도 예쁘게 변했고, 이제 마우스로 드래그하면 지도가 자유롭게 움직인다.
import ReactMapGL, { NavigationControl, FlyToInterpolator } from 'react-map-gl';
지도의 움직임에 부드러운 효과를 주기 위해 FlyToInterpolator를 import했다.
지도 오른쪽에 확대 축소 버튼을 달고 싶어 NavigationControl도 import했다.
return ( <div className="Mapbox"> <ReactMapGL {...viewport} transitionDuration={800} transitionInterpolator={new FlyToInterpolator()} mapboxApiAccessToken={MAP_TOKEN} mapStyle="mapbox://styles/mapbox/streets-v9" onViewportChange={(viewport) => { setViewport(viewport); }} > <div className="navi-control"> <NavigationControl /> </div> </ReactMapGL> </div> );
transition속성들을 추가하였고 NavigationControl도 <ReactMapGl /> 컴포넌트 안에 넣었다.
.navi-control { position: absolute; right: 0; }
NavigationControl은 css로 position: absolute 속성을 주고 위치를 원하는 곳으로 바꾼다.
이렇게 네비게이션 바가 추가되었고,
지도를 움직일 때 부드럽게 축소 -> 확대되는 효과도 생겼다.
지도의 움직임 효과도 추가 설정으로 커스텀할 수 있으니
변경하고 싶은 사람은 문서를 잘 살펴보자.
const storeList = [ { name: 'CU', location: [37.565964, 126.986574] }, { name: '할리스', location: [37.564431, 126.986591] }, { name: '세븐일레븐', location: [37.565188, 126.983238] }, { name: '파리바게트', location: [37.564869, 126.984450] }, { name: '스타벅스', location: [37.562003, 126.985829] } ];
API로 받아 온 데이터를 이용하여 원하는 지점에 Marker 표시를 할 수 있다.
지금은 간단한 예시이므로 위와 같은 배열을 임의로 만들어보았다.
위의 배열이 API로 받아 온 가게 위치 데이터라고 가정해보자.
import ReactMapGL, { Marker, Popup, NavigationControl, FlyToInterpolator } from 'react-map-gl';
Marker와 Popup을 추가로 import한다.
return ( <div className="Mapbox"> <ReactMapGL {...viewport} transitionDuration={800} transitionInterpolator={new FlyToInterpolator()} mapboxApiAccessToken={MAP_TOKEN} mapStyle="mapbox://styles/mapbox/streets-v9" onViewportChange={(viewport) => { setViewport(viewport); }} > <div className="navi-control"> <NavigationControl /> </div> { storeList.map((store, i) => ( <Marker key={i} latitude={store.location[0]} longitude={store.location[1]} > <button className="btn-marker" /> </Marker> )) } </ReactMapGL> </div> );
가게 리스트 배열을 iterate하면서 <Marker /> 컴포넌트를 만들어준다.
.btn-marker { background: url('./marker_red.png') no-repeat center / contain; width: 22px; height: 35px; border: none; } .btn-marker:focus { outline: transparent; }
마커 컴포넌트 안에 button 태그를 추가하였고,
css로 버튼을 핀 모양으로 바꾸었다.
이렇게 원하는 위치에 마커 버튼이 잘 나타난다.
const [ selectedStore, setSelectedStore ] = useState(null); return ( <div className="Mapbox"> <ReactMapGL {...viewport} transitionDuration={800} transitionInterpolator={new FlyToInterpolator()} mapboxApiAccessToken={MAP_TOKEN} mapStyle="mapbox://styles/mapbox/streets-v9" onViewportChange={(viewport) => { setViewport(viewport); }} > <div className="navi-control"> <NavigationControl /> </div> { storeList.map((store, i) => ( <Marker key={i} latitude={store.location[0]} longitude={store.location[1]} > <button className="btn-marker" onClick={() => setSelectedStore(store)} /> </Marker> )) } { selectedStore && ( <Popup offsetLeft={10} latitude={selectedStore.location[0]} longitude={selectedStore.location[1]} onClose={() => setSelectedStore(null)} > <div>{selectedStore.name}</div> </Popup> ) } </ReactMapGL> </div> );
이제 버튼을 클릭할 때마다 팝업창이 뜨도록 만들 것이다.
selectedStore라는 state를 추가하였다.
기본 값은 null이고, Marker 버튼을 클릭할 떄마다 setState에
클릭한 위치에 있는 가게 정보가 selectedState에 담긴다.
selectedStore에 값이 있으면 <Popup /> 컴포넌트가 보여지게끔 설정한다.
Popup 컴포넌트에 onClose 이벤트를 달고
setSelectedStore(null)을 해주면 팝업창을 클릭할 떄마다
state가 null로 초기화되어 팝업창이 닫히게 된다.
팝업창 전체가 아니라 x 버튼을 클릭했을 때만 팝업창을 닫히게 하는 법도
문서를 잘 읽어보면 나와있다.
내가 원하던대로 팝업창이 잘 표시된다!
그런데 문제는 지도가 처음에 로드할 때는 브라우저 사이즈에 꽉 차게 뜨는데,
resize를 할 때 지도 사이즈가 안바뀐다는 것이다.
useEffect(() => { const mapResizeEvent = _.throttle(() => { setViewport(Object.assign({}, { ...viewport, width: `${window.innerWidth}px`, height: `${window.innerHeight}px` })); }, 2000); window.addEventListener('resize', mapResizeEvent); return () => { window.removeEventListener('resize', mapResizeEvent); } }, [ viewport ]);
이를 해결하기 위해 resize 이벤트를 추가하였다.
리사이즈 이벤트는 매우 호출이 많이 되기 때문에
throttle을 이용하여 호출되는 빈도수를 낮춰야한다.
나는 lodash의 throttle 메소드를 사용하였다.
끝!
반응형'Frontend' 카테고리의 다른 글
[TypeScript] 타입스크립트 - Installation & Basic Types (252) 2020.01.27 [React/Node.js] Chat App, Typing Indicator - 채팅 앱에서 대화 상대가 텍스트를 입력 중일 경우 표시해주는 기능 구현하기 (842) 2019.10.06 [React] React.js와 firebase를 사용하여 간단한 메모 앱 만들기[2] - 메모 저장 및 삭제하기 (609) 2019.08.30 [React] React.js와 firebase를 사용하여 간단한 메모 앱 만들기[1] - 리스트 출력하기 (967) 2019.08.29 [React] 리덕스 (Redux) 이해하기 (609) 2019.08.24 COMMENT