본문 바로가기

 

📢 들어가며

이번 포스팅은 지난 포스팅에서 이어진다.

 

지난 포스팅에선 글꼴, 아이콘 등 구체적인 UI를 구현했었다.
이번 포스팅에선 본격적으로 기능을 구현해볼 것이다.
지도를 클릭했을 때 주소를 입력 받을 수 있게하거나
주소를 입력하면 지도에 위치가 표시되도록 하는 기능을 구현할 것이다.

 

모든 소스코드는 깃헙에서 확인할 수 있다.

🍜 지도 클릭 이벤트

Openlayers는 지도 클릭 이벤트를 지원한다.
지난 포스팅에서 만들었던 olMap에 클릭 이벤트 기능을 추가해주는 방식으로 구현 가능하다.

 

일단 본격적인 기능 구현에 앞서, 지도에서 클릭한 부분의 위도, 경도를 받아와지는지 확인하자.

olMap 을 생성해준 mounted 부분에, 아래 코드를 추가해보자.

// ...
import {fromLonLat, toLonLat} from 'ol/proj.js'
// ...

mounted() {
    this.olMap = new OlMap({
        // ...
    })

    this.olMap.on('click', (e) => {
      console.log(toLonLat(e.coordinate));
    })
  }

MainMap.vue

 

Openlayers 의 클릭 이벤트는 coordinate 라는 것을 제공한다.
이는 직역하면 좌표계 라는 뜻이다.
ol/proj.js엔 이 제공받은 좌표를 위도 경도로 변환시키는 메소드가 존재한다. (toLonLat)
지난 시간에 사용했던 fromLonLat 과 같은 방법으로 impoort 해서 사용하자.

💡 참고
fromLonLat : 위도, 경도 => 좌표계
toLonLat : 좌표계 => 위도, 경도

 

이제 지도를 클릭하고 콘솔을 확인해보면 위도, 경도 값을 담은 배열이 콘솔에 찍히는 것을 확인할 수 있다.

지도 클릭 후 콘솔 결과

🍜 위도, 경도 정보로 주소 가져오기

이제 이 위도, 경도를 주소로 변환시킬 것이다.
주소 변환 API는 네이버, 카카오, 구글, 행정안전부 등 여러 곳에서 제공하지만
KEY를 받아 사용해야한다는 귀찮음이 있어서 나는 Nominatim이라는 오픈소스를 활용하기로 했다.

Nominatim은 OpenStreeMap 데이터를 사용하여 주소를 가져온다.

💡 참고
지오코딩 (geocoding) : 주소 -> 위도, 경도
리버스 지오코딩 (reverse geocoding) : 위도, 경도 -> 주소

 

Nominatim이 제대로 작동하는지 확인해보기 위해 나는 Postman을 사용해봤다.

API 사용 결과

http://nominatim.openstreetmap.org/reverse 온라인 GET API에 format과 아까 콘솔로 본 위도 경도를 lon, lat params 로 담아 보내보았다.
format의 값으로 json을 입력했더니, 결과가 json으로 온 것을 확인할 수 있었다.
대충 아무 곳이나 찍었었는데 치킨 집이 나왔다...

아무튼 무사히 리버스 지오코딩이 된 것을 확인할 수 있었다! 🎉

 

이제 본격적으로 화면에 주소를 출력시켜보자.

API통신을 위해서 Axios 를 사용해 줄 것이다.
Axios는 브라우저와 node.js에서 사용할 수 있는 Promise 기반 HTTP 클라이언트 라이브러리이다.

나는 npm으로 설치해줬다.

npm install axios

설치 후, package.json에 axios가 생긴 걸 확인할 수 있었다.

"dependencies": {
    // ...
    "axios": "^0.26.1"
    // ...
}

 

package.json

 

methods 내에 getAddress()라는 메소드를 파고 아래와 같이 입력해준다.

import axios from 'axios'

// ...
methods: {
    getAddress (lon, lat) {
      return axios.get(
          'https://nominatim.openstreetmap.org/reverse',
          {
            params: {
              format: 'json',
              lon: lon,
              lat: lat
            }
          })
    }
  }

MainMap.vue

 

axiosget 메소드에 api url과 파라미터(format, lon, lat)를 담아 전송한다.
결과 값은 Postman에서 확인한 값과 같을 것이다.

🍜 받아온 주소 UI 출력

이제 getAddress를 통해 받아온 주소를 사이드바에 뿌려주는 일을 할 것이다.

 

주소가 출력된 부분

사이드바에서 주소가 표시되었으면 하는 부분은 바로 이 부분이다.


SideBar.vuedata()address를 추가하고 출력하고자하는 Inputv-model을 추가해줬다.
adress에 값이 들어가면 Input에 출력될 것이다.

// ....

<div class="location-info-area">
    <FontAwesomeIcon icon="location-dot" />
      <BInput
          placeholder="위치 정보 직접 입력하기"
          v-model="address"
    />
</div>

// ...
data() {
    return {
      // ...
      address: undefined,
    }
  },

SideBar.vue

 

본격적으로 UI 에 표현하기에 앞서, 짚고 넘어가야할 포인트가 있다.
바로, 주소 값을 가지고 있는 지도 컴포넌트와 사이드바 컴포넌트가 서로 형제 컴포넌트라는 점이다.

<div id="app">
    <MainMap/>
    <SideBar class="side-bar"/>
  </div>

App.vue

 

Vue 컴포넌트 간 데이터 공유 방법

부모 자식간의 컴포넌트는 $emit, $refs 등으로 데이터를 주고 받을 수 있지만,
형제 컴포넌트 들은 바로 전달하지 못하고 eventBus$root 등을 사용해야한다.

때문에 나는 $root.$refs를 사용했다.

 

먼저, 데이터를 보내고 싶은 곳(목적지) 컴포넌트를 $root.$refs에 등록한다.
쉽게 설명하면, $root 라는 최상위 컴포넌트에 $refs 로 목적지 컴포넌트를 등록하여
어떤 컴포넌트든 해당 $refs 컴포넌트 값이나 메소드에 접근할 수 있도록 하는 것이다.

 

나의 목적지 컴포넌트는 SideBar.vue 이기 때문에 created 부분에서 아래와 같이 등록해줬다.

created() {
    this.$root.$refs.sideBar = this;
  },

SideBar.vue

 

이제 sideBar라는 이름의 $refs 컴포넌트에 $root를 통해 누구나 접근할 수 있게 되었다.
바로 아래처럼 말이다.

this.olMap.on('click', async (e) => {
      const lonLatArr = toLonLat(e.coordinate)
      const lon = lonLatArr[0]
      const lat = lonLatArr[1]
      const addressInfo = await that.getAddress(lon, lat)
      this.$root.$refs.sideBar.address = addressInfo.data.display_name.split(", ").reverse().join(" ");
    })

MainMap.vue

 

getAddress로 받아온 결과값의 data.display_name은 콤마로 구분된 문자열이다.
그래서 콤마로 split을 해줬고, 작은 범위부터 시작되던 결과 값을 한국 주소에 맞춰 reverse 시켜줬다.
그리고 공백으로 join!

 

이제 지도를 클릭하면 해당 주소가 사이드바에 표시된다!

 

🍜 주소로 위도, 경도 정보 가져오기

이젠 반대로 주소로 위도, 경도 정보를 가져와 UI에 표시해볼 것이다.

 

찾아보니 행안부에서 제공하는 도로명 주소 API가 있었지만, 이 역시 KEY를 발급받아야하기 때문에 귀찮아서 다른 방법을 찾아보았다.
Openlyaers의 확장 라이브러리인 ol-geocoder를 쓰기로 했다.
이 라이브러리를 추가하면 Openlyaers에 검색 input 창이 만들어지게 되고, 이것으로 주소를 검색하고 그에 대한 정보를 얻을 수 있다.

사용법은 README 에 아주 잘 나와 있었다.

 

먼저 npm으로 install 한다.

npm install ol-geocoder

그리고 바로 다른 ol 라이브러리 처럼 사용 가능하다.
나는 olMap 을 선언했을 때와 같이 mounted 부분에 아래 코드를 입력해줬다. (+ css)

// ...
import Geocoder from 'ol-geocoder'
// ...

mounted() {
// ...
  const geocoder = new Geocoder('nominatim', {
      provider: 'osm',
      lang: 'kr',
      placeholder: '주소 검색',
      limit: 5, // 자동 완성 결과 최대 개수
      autoComplete: true,
      keepOpen: true
    });
    this.olMap.addControl(geocoder);

    geocoder.on('addresschosen', (evt) => {
      that.setUiAddress(evt.address.details.name);
    });
// ...

methods: {
// ... 
   setUiAddress(str) {
      this.$root.$refs.sideBar.address = str.split(', ').reverse().join(' ');
   },
// ...
}

// ...
<style lang="scss" scoped>
.main-map {
// ...
  ::v-deep.ol-geocoder {
    position: absolute;
    right: 0;
    padding: 10px;

    button {
      display: none;
    }

    input::placeholder {
      color: white;
      opacity: 0.7;
    }

    input, ul {
      border-style: none;
      width: 200px;
      background-color: rgba(0, 0, 0, 0.5);
      border-radius: 5px;
      border-color: unset;
      padding: 0 5px;
      color: white;
    }

    ul {
      margin-top: 5px;
      padding: 0;
      list-style: none;

      li:hover {
        background-color: rgba(0, 0, 0, 0.3);
      }

      li {
        padding: 5px 10px;
        font-size: 13px;

        a {
          text-decoration: none;

          .gcd-road {
            color: white;
          }
        }
      }
    }
  }
// ...
}
</style>

MainMap.vue

 

new Geocoderinstall 해온 ol-geocoder를 생성한다.

여기서 nominatim은 라틴어로 '이름'이라는 뜻이고,
osm 자료의 이름과 주소를 검색하고, 위경도 좌표로 부터 주소를 생성하는 도구를 말한다.


그 뒤로, 옵션을 설정해줄 수 있다. 더 많은 옵션은 여기서 확인 가능하다.

 

생성해준 geocoder 를 지난 포스팅에서 만들어 준 this.olMapaddControl 해준다.
addControl은 Openlayers 에서 제공하는 메소드로, 커스텀 컨트롤(이벤트)를 주입할 수 있게 한다.

 

다음으로 주소가 선택되었을 때 발생하는 이벤트를 설정해준다. (이는 geocoder에서 제공하는 기능이다.)
나는 사이드바의 주소 입력란에 해당 주소가 포맷되어 보여지도록 설정해줬다.
앞서 구현했던 코드와 같은 내용이어서, 메소드화 해줬다. (setUiAddress)

검색 결과가 사이드바 주소 입력란에 표시되는 것을 볼 수 있다.

성공적으로 사이드바에 주소가 뜨는 것을 확인할 수 있었다!🎉

 

다만, 이 라이브러리의 단점은...

지도에 표시되는 주소만 검색이 가능하다는 것이다. 한 건물 내에 많은 음식점이 있지만,

지도는 간추려서 대표되는 몇군데의 지도만 표시하고 있다.

우편번호 등 상세 주소로 검색 자체는 가능하겠지만, 가게 명으로 검색을 하는 데엔 약간의 한계가 있었다.

이럴 땐 어쩔 수 없이 근처에서 내가 위치를 클릭해서 입력하는 수 밖에 없겠다 😂

🍜 지도 클릭 위치 표시

주소가 출력되는 건 좋지만, 뭔가 지도 클릭한 곳도 UI 표시되는 게 좋을 것 같아서
클릭한 곳에 아이콘을 추가해주기로 했다.

 

아이콘을 추가하기 위해선 OpenLayers의 Feature, Source, Layer, Projection 에 대해서 알아야한다.

이전 포스팅에서 Layer는 화면에 종이 한장을 얹는 느낌이라고 말했었다.
그리고 그 종이엔 Tile 형태의 OSM 지도가 그려져 있다.

 

우린 아이콘을 위한 Layer를 한 장 더 얹을 것이다.
일러스트를 그려본 사람들은 알 것이다. 레이어를 나누지 않고 한 레이어에 그림을 냅다 그리면 대참사가 난다는 것을...
아무튼 이건 단순 비유였고 ㅋㅋ

 

Source에 대해서도 알아보자.

SourceLayer의 알맹이라고 생각하면 되겠다.
LayerSource로 구성되어 있다.

Map을 선언해준 부분을 보면 layersource로 이루어진 것을 확인할 수 있다.

 this.olMap = new OlMap({
      // ...
      layers: [
          new OlLayerTile({
            source: new OSM()
          })
      ],
      // ...
    })

MainMap.vue

 

그리고 이 SourceFeature로 구성되어 있다.

Feature는 쉽게 말하면 지도위에 그려지는 요소들이다.
동그라미가 될수도, 선이 될 수도, 아이콘이 될 수도 있다.

 

범위를 정리하자면 이런식으로 되겠다.

Feature < Source < Layer

 

이제 마지막으로 Projection에 대해 알아보자.
Prjoection은 해석하면 지도 투영법 이라는 뜻이다.


즉, 위선과 경선으로 이우러진 지구상의 가상적 좌표를 평면상에 옯기는 방법을 가리킨다.

지구는 구체이기 때문에, 아무리 작은 공간의 지도를 작성한다 할 지라도 그 왜곡을 피할수 없다.
따라서 투영법은 이 왜곡을 처리하는 방법이라고 정의할 수 있다.

 

Projection 은 좌표(coordinate)를 어떤 식으로 표현하느냐에 따라 종류를 나눌 수 있다.
대표적으로 EPSG:4326, EPSG:3857, EPSG:5179 등이 있다.

 

이 전 포스팅에서 지도의 첫 화면을 경기도 성남시로 맞춰뒀었는데,
그때 사용한 좌표가 [127.1388684, 37.4449168] 이었다.
이는 EPSG:4326 표현 방식이다.

 

그런데 OSM 지도는 EPSG:3857로 표현된다.
따라서 EPSG:4326 좌표계를 EPSG:3857 방식으로 수정해야한다.


이를 OpenLayers api transform, toLonLat, fromLonLat로 제공하고 있다.
toLonLat, fromLonLat 은 default 반환값이 EPSG:3857 방식이기 때문에 우린 이를 사용해줄 것이다.

 

자, 이제 본격적으로 지도를 클릭했을 때 아이콘을 그려보자.
아이콘을 그려줄 layer를 추가해 줄 것이다.

olMap 선언 부분을 아래와 같이 수정해주자.

import OlVectorSource from 'ol/source/Vector.js'
import OlVectorLayer from 'ol/layer/Vector.js'

const EPSG_3857 = 'EPSG:3857';

//...

    mounted() {
        const vectorSource = new OlVectorSource(EPSG_3857);
        const vectorLayer = new OlVectorLayer({
            source: vectorSource
        })
        this.olMap = new OlMap({
            target: this.$refs.map,
            controls: defaults({
                attribution: false,
                zoom: false,
                rotate: false,
            }),
            layers: [
                new OlLayerTile({
                    source: new OSM()
                }),
                vectorLayer
            ],
            view: new OlView({
                center: fromLonLat([127.1388684, 37.4449168]), // 경기도 성남
                zoom: 10,
                projection: EPSG_3857 // 생략 가능
            })
        })
    }

MainMap.vue

 

layersvectorLayer를 추가해주었다.
vectorLayervectorSource로 이루어진 OlVectorLayer이다.
그리고 이 vectorSource는 'EPSG:3857' 방식으로 이루어져있다.

 

요약하면, 'EPSG:3857' 방식의 알맹이(source)로 이루어진 layer 를 추가해준 것이다.
여기서 Vector라는 말이 뭔가 싶을 수도 있는데, 어렵게 생각할 필요 없이 작은 점이 모여 그려진 화면(또는 화면을 그리는 방식)이라고 생각하면 된다.

 

이제 추가한 layer에 아이콘을 그려볼 것이다.
지도 클릭 이벤트를 아래와 같이 수정해주자.

    // ...
    mounted() {
            this.olMap.on('click', async (e) => {
            await addUiAddress();
            drawMapIcon();

            async function addUiAddress() {
                const lonLatArr = toLonLat(e.coordinate)
                const lon = lonLatArr[0]
                const lat = lonLatArr[1]
                const addressInfo = await that.getAddress(lon, lat)
                that.setUiAddress(addressInfo.data.display_name);
            }

            function drawMapIcon() {
                vectorSource.clear();
                geocoder.getSource().clear();
                const feature = new OlFeature({
                    geometry: new OlPoint(e.coordinate)
                })
                feature.setStyle(new OlStyle({
                    image: new OlIcon({
                        scale: 0.7,
                        src: '//cdn.rawgit.com/jonataswalker/map-utils/master/images/marker.png'
                    })
                }))
                vectorSource.addFeature(feature);    
            }
        })
    }

addUiAddress() 부분은 위에서 구현 했던 내용으고,
drawMapIcon() 부분을 추가해 줬다.
(기능별로 function으로 묶어 코드를 정리해줬다.)

 

drawMapIcon() 을 살펴보자.

vectorSource.clear();
geocoder.getSource().clear();

클릭했을 떄 기존에 그려진 아이콘을 제거하는 로직이다.
vectorSource에 그려진 아이콘을 clear하고,
주소 검색 라이브러리의 source (geocoder.getSource()) 에 그려진 아이콘을 clear 해준다.

import OlFeature from 'ol/Feature.js';
import OlPoint from 'ol/geom/Point';
import OlStyle from 'ol/style/Style.js'
import OlIcon from 'ol/style/Icon.js'

// ...

const feature = new OlFeature({
    geometry: new OlPoint(e.coordinate)
})
feature.setStyle(new OlStyle({
        image: new OlIcon({
        scale: 0.7,
        src: '//cdn.rawgit.com/jonataswalker/map-utils/master/images/marker.png'
    })
}))
vectorSource.addFeature(feature);

Feature를 선언 부분이다.
geometry는 직역하면 '상대적 위치'라는 뜻인데, Feature가 그려질 위치를 말한다.
이는 클릭이벤트에서 받아온 좌표계(e.coordinate)로 넣어주면 된다.

 

다음으로 Feature의 스타일을 입혀준다.
나는 이미지로 아이콘을 그려줬다.
이미지는 Geocoder 라이브러리에서 사용된 아이콘을 그대로 가져왔다.

 

마지막으로, sourceaddFeature를 해 주면
VectorLayerFeature가 그려지게 된다

 

 

성공적으로 아이콘이 그려지는 걸 확인할 수 있었다!🎉


이번 포스팅에선 클릭이벤트 또는 검색을 통해 주소를 입력받는 기능을 구현해보았다.

다음 포스팅에선 사진을 업로드하고 저장하는 기능을 구현해보겠다.

(드디어 백엔드!)

 

댓글, 하트, 피드백은 언제나 환영입니다!😇

Seize the day!

Spring MVC | Spring Boot | Spring Security | Mysql | Oracle | PostgreSQL | Vue.js | Nuxt.js | React.js | TypeScript | JSP | Frontend | Backend | Full Stack | 자기계발 | 미라클 모닝 | 일상