📢 들어가며
이번 포스팅은 지난 포스팅에서 이어진다.
지난 포스팅에선 글꼴, 아이콘 등 구체적인 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을 사용해봤다.
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
axios
의 get
메소드에 api url과 파라미터(format
, lon
, lat
)를 담아 전송한다.
결과 값은 Postman에서 확인한 값과 같을 것이다.
🍜 받아온 주소 UI 출력
이제 getAddress
를 통해 받아온 주소를 사이드바에 뿌려주는 일을 할 것이다.
사이드바에서 주소가 표시되었으면 하는 부분은 바로 이 부분이다.
SideBar.vue
의 data()
에 address
를 추가하고 출력하고자하는 Input
에 v-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
부모 자식간의 컴포넌트는 $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 Geocoder
로 install
해온 ol-geocoder
를 생성한다.
여기서 nominatim
은 라틴어로 '이름'이라는 뜻이고,
osm 자료의 이름과 주소를 검색하고, 위경도 좌표로 부터 주소를 생성하는 도구를 말한다.
그 뒤로, 옵션을 설정해줄 수 있다. 더 많은 옵션은 여기서 확인 가능하다.
생성해준 geocoder
를 지난 포스팅에서 만들어 준 this.olMap
에 addControl
해준다.addControl
은 Openlayers 에서 제공하는 메소드로, 커스텀 컨트롤(이벤트)를 주입할 수 있게 한다.
다음으로 주소가 선택되었을 때 발생하는 이벤트를 설정해준다. (이는 geocoder
에서 제공하는 기능이다.)
나는 사이드바의 주소 입력란에 해당 주소가 포맷되어 보여지도록 설정해줬다.
앞서 구현했던 코드와 같은 내용이어서, 메소드화 해줬다. (setUiAddress
)
성공적으로 사이드바에 주소가 뜨는 것을 확인할 수 있었다!🎉
다만, 이 라이브러리의 단점은...
지도에 표시되는 주소만 검색이 가능하다는 것이다. 한 건물 내에 많은 음식점이 있지만,
지도는 간추려서 대표되는 몇군데의 지도만 표시하고 있다.
우편번호 등 상세 주소로 검색 자체는 가능하겠지만, 가게 명으로 검색을 하는 데엔 약간의 한계가 있었다.
이럴 땐 어쩔 수 없이 근처에서 내가 위치를 클릭해서 입력하는 수 밖에 없겠다 😂
🍜 지도 클릭 위치 표시
주소가 출력되는 건 좋지만, 뭔가 지도 클릭한 곳도 UI 표시되는 게 좋을 것 같아서
클릭한 곳에 아이콘을 추가해주기로 했다.
아이콘을 추가하기 위해선 OpenLayers의 Feature, Source, Layer, Projection 에 대해서 알아야한다.
이전 포스팅에서 Layer는 화면에 종이 한장을 얹는 느낌이라고 말했었다.
그리고 그 종이엔 Tile
형태의 OSM
지도가 그려져 있다.
우린 아이콘을 위한 Layer
를 한 장 더 얹을 것이다.
일러스트를 그려본 사람들은 알 것이다. 레이어를 나누지 않고 한 레이어에 그림을 냅다 그리면 대참사가 난다는 것을...
아무튼 이건 단순 비유였고 ㅋㅋ
Source에 대해서도 알아보자.
Source
는 Layer
의 알맹이라고 생각하면 되겠다.Layer
는 Source
로 구성되어 있다.
Map을 선언해준 부분을 보면 layer
가 source
로 이루어진 것을 확인할 수 있다.
this.olMap = new OlMap({
// ...
layers: [
new OlLayerTile({
source: new OSM()
})
],
// ...
})
MainMap.vue
그리고 이 Source
는 Feature
로 구성되어 있다.
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
layers
에 vectorLayer
를 추가해주었다.
이 vectorLayer
는 vectorSource
로 이루어진 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 라이브러리에서 사용된 아이콘을 그대로 가져왔다.
마지막으로, source
에 addFeature
를 해 주면VectorLayer
에 Feature
가 그려지게 된다
성공적으로 아이콘이 그려지는 걸 확인할 수 있었다!🎉
이번 포스팅에선 클릭이벤트 또는 검색을 통해 주소를 입력받는 기능을 구현해보았다.
다음 포스팅에선 사진을 업로드하고 저장하는 기능을 구현해보겠다.
(드디어 백엔드!)
댓글, 하트, 피드백은 언제나 환영입니다!😇
'개인 프로젝트' 카테고리의 다른 글
맛집 지도 만들기(6) - 리뷰 지도에 표시하기 및 리뷰 수정, 삭제 (2) | 2022.07.22 |
---|---|
맛집 지도 만들기(5) - CRUD API 구현하기 (feat. Axios, 함수형 컴포넌트) (1) | 2022.06.19 |
맛집 지도 만들기(3) - 사이드바 UI 구현 (Font Awesome Icon, 글꼴 적용) (3) | 2022.02.13 |
맛집 지도 만들기(2) - OpenLayers 지도 띄우기 (4) | 2021.12.05 |
맛집 지도 만들기(1) - Spring Boot + Vue.js 설치 및 연동하기 (7) | 2021.11.27 |