ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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 components for Mapbox GL JS

    react-map-gl is a suite of React components for Mapbox GL JS

    uber.github.io

     

     

    그리고 아래 유튜브 동영상은 처음 맵을 띄울 때 

    매우 도움이 많이 된 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 메소드를 사용하였다.

     

     

     

     

    끝!

     

     

    반응형

    COMMENT