[React] MapBox 활용

mi-fasol·2023년 5월 17일
0

React

목록 보기
3/3

시험기간이다 뭐다 해서 바빠서 블로그를 못 올린지 벌써 한 달이나 됐다.

다행히 시험 점수는 괜찮게 나온 듯 싶어서 기분 좋게 포스팅을 하려고 한다.

현재 4학년으로 재학 중이라 졸업작품을 진행하고 있는데, 이번 졸업작품에서 리액트로 웹 서비스를 해야 할 일이 생겼다.

기본적인 틀은 다 MapBox에서 기본으로 제공하는 튜토리얼 소스를 활용했고, 서울시의 지도를 보여줘야 했기 때문에 구글에서 찾은 서울시 geojson 데이터를 활용했다.

우선 맵 박스는, 카카오 맵이나 구글 맵처럼 지도를 제공하는 서비스 중 하나다.

자유롭게 디자인을 변경할 수 있어 자유도가 굉장히 높은 편이라, 디자인에 따라 수정할 게 많지만 그 대가로 확실히 예쁜 지도를 얻을 수 있다.

나는 아직 디자인 단계가 아니기 때문에 기본 스타일을 그대로 활용했다.

나는 노트북에 원래 리액트 관련 소프트웨어가 설치되어 있어서 이 포스팅에서 굳이 언급하지 않겠지만, 혹시 깔려있지 않으신 분들은 미리 설치하고 이 포스팅을 참고하시길 바란다.

맵박스 예제 코드

위의 코드를 참고하여 진행했었다.

귀찮은 분들을 위해 코드의 전문을 올려보자면

Legend.js

import React from "react";

const Legend = (props) => {
  const renderLegendKeys = (stop, i) => {
    return (
      <div key={i} className="txt-s">
        <span
          className="mr6 round-full w12 h12 inline-block align-middle"
          style={{ backgroundColor: stop[1] }}
        />
        <span>{`${stop[0].toLocaleString()}`}</span>
      </div>
    );
  };

  return (
    <>
      <div className="bg-white absolute bottom right mr12 mb24 py12 px12 shadow-darken10 round z1 wmax180">
        <div className="mb6">
          <h2 className="txt-bold txt-s block">{props.active.name}</h2>
          <p className="txt-s color-gray">{props.active.description}</p>
        </div>
        {props.stops.map(renderLegendKeys)}
      </div>
    </>
  );
};

export default Legend;

Optionfield.js

import React from "react";

const Optionsfield = (props) => {
  const renderOptions = (option, i) => {
    return (
      <label key={i} className="toggle-container">
        <input
          onChange={() => props.changeState(i)}
          checked={option.property === props.property}
          name="toggle"
          type="radio"
        />
        <div className="toggle txt-s py3 toggle--active-white">
          {option.name}
        </div>
      </label>
    );
  };
  return (
    <div className="toggle-group absolute top left ml12 mt12 border border--2 border--white bg-white shadow-darken10 z1">
      {props.options.map(renderOptions)}
    </div>
  );
};

export default Optionsfield;

이 두 코드를 src 디렉토리에 하위 디렉토리 components를 생성해 넣어줬다.

data.json 파일도 하나 생성해서 원하는 geojson 데이터를 넣어주면 된다. 이건 각자 원하는 데이터가 다를 테니까 생략하고 넘어가도록 하겠다.

App.js 파일도 변경해줘야 하는데, 정말 간단하다. Map을 리턴해주기만 하면 된다.

App.js

import Map from './Map';
import React from 'react'

function App() {
  return (
    <div>
      <Map/>
    </div>
  );
}

export default App;

그 다음으로는 index.css파일을 설정해줘야 한다.

index.css

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

css 파일을 설정해줬으니 js 파일도 설정해주자.

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

그 다음은 정말 별 거 없는 Map.css 파일이다.

Map.css

.map-container {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

이제 마지막으로 가장 중요한 코드인데, MapBox에서 기본으로 제공하는 코드인 Map.js 코드다.

Map.js

import React, { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import Legend from './components/Legend';
import Optionsfield from './components/Optionsfield';
import './Map.css';
import data from './data.json';

mapboxgl.accessToken =
  'pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4M29iazA2Z2gycXA4N2pmbDZmangifQ.-g_vE53SD2WrJ6tFX7QHmA';

const Map = () => {
  const options = [
    {
      name: 'Population',
      description: 'Estimated total population',
      property: 'pop_est',
      stops: [
        [0, '#f8d5cc'],
        [1000000, '#f4bfb6'],
        [5000000, '#f1a8a5'],
        [10000000, '#ee8f9a'],
        [50000000, '#ec739b'],
        [100000000, '#dd5ca8'],
        [250000000, '#c44cc0'],
        [500000000, '#9f43d7'],
        [1000000000, '#6e40e6']
      ]
    },
    {
      name: 'GDP',
      description: 'Estimate total GDP in millions of dollars',
      property: 'gdp_md_est',
      stops: [
        [0, '#f8d5cc'],
        [1000, '#f4bfb6'],
        [5000, '#f1a8a5'],
        [10000, '#ee8f9a'],
        [50000, '#ec739b'],
        [100000, '#dd5ca8'],
        [250000, '#c44cc0'],
        [5000000, '#9f43d7'],
        [10000000, '#6e40e6']
      ]
    }
  ];
  const mapContainerRef = useRef(null);
  const [active, setActive] = useState(options[0]);
  const [map, setMap] = useState(null);

  // Initialize map when component mounts
  useEffect(() => {
    const map = new mapboxgl.Map({
      container: mapContainerRef.current,
      style: 'mapbox://styles/mapbox/streets-v11',
      center: [5, 34],
      zoom: 1.5
    });

    map.on('load', () => {
      map.addSource('countries', {
        type: 'geojson',
        data
      });

      map.setLayoutProperty('country-label', 'text-field', [
        'format',
        ['get', 'name_en'],
        { 'font-scale': 1.2 },
        '\n',
        {},
        ['get', 'name'],
        {
          'font-scale': 0.8,
          'text-font': [
            'literal',
            ['DIN Offc Pro Italic', 'Arial Unicode MS Regular']
          ]
        }
      ]);

      map.addLayer(
        {
          id: 'countries',
          type: 'fill',
          source: 'countries'
        },
        'country-label'
      );

      map.setPaintProperty('countries', 'fill-color', {
        property: active.property,
        stops: active.stops
      });

      setMap(map);
    });

    // Clean up on unmount
    return () => map.remove();
  }, []);

  useEffect(() => {
    paint();
  }, [active]);

  const paint = () => {
    if (map) {
      map.setPaintProperty('countries', 'fill-color', {
        property: active.property,
        stops: active.stops
      });
    }
  };

  const changeState = i => {
    setActive(options[i]);
    map.setPaintProperty('countries', 'fill-color', {
      property: active.property,
      stops: active.stops
    });
  };

  return (
    <div>
      <div ref={mapContainerRef} className='map-container' />
      <Legend active={active} stops={active.stops} />
      <Optionsfield
        options={options}
        property={active.property}
        changeState={changeState}
      />
    </div>
  );
};

export default Map;

위의 코드는 각 나라별 인구분포도와 GDP에 따라 세계지도에 색을 입혀 보여주는 코드다.

  • options: 해당하는 property 값에 따라 정해진 구간 별로 색을 설정
  • useEffect의 center: 지도를 띄울 때 중앙에 보일 지역
    • 본인은 서울 지도가 필요해서 뒤에 나올 코드에 보이 듯 서울의 위경도로 설정
  • useEffect의 zoom: 지도 줌 비율
  • map.on('load' ...): 지도가 로딩 될 때 설정
    • countries라는 id를 가진 속성에 geojson 타입의 데이터를 삽입
    • data.json 파일을 따로 만들어뒀기에 data를 import 함
  • map.addLayer(...): MapBox 스타일의 레이어 추가
  • map.setPaintProperty(...): 지정된 스타일 레이어의 페인트 속성 값 변경

대충 이정도로 간단하게 설명할 수 있겠다. 실행시키면 아래의 화면이 나온다.

나는 여기서 사소하게 몇 부분만 건드렸는데, 우리 졸업작품에선 포트홀에 따라 색이 바뀌어야 했기 때문에 서울시 행정구역 geojson 파일에 pothole이라는 이름의 프로퍼티를 임의로 추가해줬고, 색의 구간도 변경해줬으며 각 구역에 클릭 이벤트를 주기 위해 코드를 추가했었다.

  const options = [
    {
      name: '도로 파손 정도',
      description: '도로 파손 개수에 따른 지표',
      // data.json에 있는 포트홀 프로퍼티로 구간 사용
      property: 'pothole',
      // 내 데이터에 맞도록 각 구간 변경
      stops: [
        [0, '#f8d5cc'],
        [3, '#f4bfb6'],
        [5, '#f1a8a5'],
        [7, '#ee8f9a'],
        [9, '#ec739b'],
        [12, '#dd5ca8'],
        [15, '#c44cc0'],
        [18, '#9f43d7'],
        [20, '#6e40e6']
      ]
    },
  ];

위의 코드가 구간을 정하는 프로퍼티 값을 포트홀로 바꾸고, 구간을 재설정한 코드다. 또, 아까 살짝 언급했지만 나는 서울 지도가 필요해서 아래와 같이 지도의 설정을 바꿔줬다.

useEffect(() => {
    const map = new mapboxgl.Map({
      container: mapContainerRef.current,
      style: 'mapbox://styles/mapbox/streets-v11',
      // 센터는 서울의 위경도
      center: [127.0016958, 37.5642135],
      // 줌도 모자라서 더 확대되도록 변경
      zoom: 10.0
    });

위처럼 구간을 나눴고, addLayer 부분을 변경했다.

map.addLayer({
  	  // countries라는 id는 맞지 않다고 판단하여 변경
      id: 'region',
      type: 'fill',
      source: 'region',
      paint: {
        'fill-color': {
          // 아까 구간 설정한 것처럼 pothole로 변경
          property: 'pothole',
          stops: active.stops
        }
      }
    });

레이어의 id 값을 region으로 변경한 후 색이 입혀질 프로퍼티를 pothole로 변경해줬다.

마지막으로 지도의 각 구역을 클릭했을 때의 클릭 이벤트 리스너를 등록하는 코드를 추가적으로 넣어줬다.

map.on('click', 'region', (e) => {
      const features = map.queryRenderedFeatures(e.point, { layers: ['region'] });

      if (features.length > 0) {
        const clickedFeature = features[0];
        // 클릭된 레이어(구역)의 포트홀 데이터를 가져옴
        const potholeValue = clickedFeature.properties.pothole;
		// 콘솔창에 로그로 출력
        console.log(`해당 구역의 포트홀 개수: ${potholeValue}`);
      }
    });

이렇게 하면 아래처럼 간단한 결과를 얻을 수 있다.

왼쪽의 웹 화면에 있는 각 구역을 클릭하면 해당하는 포트홀 값을 아래 콘솔창에서 로그 값으로 보여준다.

지금은 아직 이렇게 간단한 기능밖에 없지만, 추후에 onClick 이벤트를 다이얼로그로 바꾸고 주변 지도도 좀 더 예쁘게 꾸며볼 생각이다.

맨 처음 맵박스를 활용할 때 정말 어떻게 해야 할지 감도 안 잡혔었고, 생각보다 자료도 많이 안 나와서 막막했었는데 나와 같은 경험을 한 분들이 조금이나마 수월하게 만드시기를 바라는 마음으로 올려본다.

profile
정위블

0개의 댓글