현재 진행하고 있는 프로젝트는 지도 기능이 매우 중요한 프로젝트입니다. 지도위에 마커를 그리고, 선분을 그려 각 끝에 해당하는 좌표가 필요하고, 좌표에 해당하는 사진을 띄우기도 해야합니다. 초기에 작성된 요구사항 명세서대로 기능들을 완성했지만 시간이 지남에 따라 지도위에 여러가지 복잡한 기능들의 추가가 요구됐습니다.
프로젝트의 프로토타이핑 모델부터 지금까지 Leaflet
이라는 지도 라이브러리를 사용하고 있었습니다. 하지만 라이브러리가 가벼운 만큼 지도위에서 할 수 있는 일들이 한정적이었고, 이미 한계점이 들어났지만 이미 많은 작업을 Leaflet
으로 해왔기 때문에, 사실상 구현이 불가한 요구사항들을 억지로 돌려 구현하며 떼우는 식으로 구현해왔습니다. 그러다 어플리케이션이 커짐에 따라서 '떼우는식'으로 지속하기 어려운 지경에 이르렀고 지도 라이브러리의 전면적인 교체가 요구되는 상황이 오고야 말았습니다.
시장에서 사용되는 지도 라이브러리는 상당히 많았습니다. 국내로는 카카오맵, 네이버지도가 서비스중이었고 해외로 눈을 돌리면 google
맵, openlayers
, leaflet
, mapbox
등 몇가지가 주력으로 사용되고 있습니다.
서비스의 특성상 해외 지도가 필요했고 국내지도는 대한민국만을 커버하므로 선택지에서 제외됐습니다.
google
맵은 추상화가 잘 되어 있는 맵 서비스였습니다. 다만 편의성을 위한 추상화가 목적이다보니 지도를 이렇게 저렇게 커스터마이징하고 기능을 추가할 수 있는 확장성이 떨어졌습니다.
mapbox
같은 경우 길찾기, 최단거리 탐색 등 신기하고 좋은 기능을 제공하고 있었습니다. 다만 기능 자체가 유료서비스인데다가 진행중인 프로젝트의 방향성과는 조금 다른 기능을 많이 담고 있었습니다. J커브를 기대하며(?) 트래픽이 발생할때마다 비용을 지불해야 하는 서비스를 선택할 수 없었습니다.
최종 후보로 선택된 라이브러리는 openlayers7
였습니다.상당히 많은 기능을 담고 있고 사용을 위해서 설정해야 하는 것들이 아주 많지만 설정할 수 있는 부분이 많다는건 자유도가 높다는 의미이자 여러가지 원하는 기능을 구현하기 좋다는 것을 의미했습니다. 그러나 현재 진행중인 프로젝트에 '적합해 보인다'는 것이지 구현하고자 하는 것을 완벽하게 커버할 수 있다는 결론은 내릴 수 없었습니다. 따라서 현재 사용중인 Leaflet
과 최종 후보 Openlayers7
을 비교해봅니다.
Leaflet
은 html dom을 적극적으로 사용하는 지도 라이브러리 입니다. 타일 이미지를 배치할때도 여러개의 사진 element
들을 정해진 dom 안에 적절히 배치해 사용할 수 있도록 합니다.
3.74MB
로 상대적으로 가벼운 편입니다. Multi Polygon
, Multi Point
, Compass
같은 것들입니다. Compass
는 npm에서 따로 제공하는 라이브러리가 있지만 공식적으로 leaflet이 제공하고 있지 않아 사용이 조금 꺼려지는것이 사실입니다.Dom
을 사용하고 있기 때문에 렌더링 성능이 떨어집니다. polygon
, circle
등 어노테이션을 svg element
로 제공해 Dom에 올리는 방식입니다.img tag
를 사용해 배치하는 형태이기 때문에 dom 렌더링에 상당한 시간이 걸립니다.leaflet
이 제공하고 있는 기능에 대한 문서화가 다소 늦습니다. types을 제공하고 있어 공식 문서보단 기능 인터페이스를 빠르게 알 수 있지만. [key:string]: string
와 같은 타입들이 등장하곤 합니다. Openlayers
는 canvas 엘리먼트안에 설정된 요소들을 벡터기반으로 표현해 지도를 렌더링하는 라이브러리입니다.
Canvas
안에 벡터들을 그려내기 때문에 Leaflet
보다 빠른 렌더링 속도를 가집니다.9.78MB
의 큰 무게를 가집니다.STROKE
, FILL
같이 선과 면을 설정하는데도 Openlayers
가 정해준 객체를 사용해야 하므로 제공하지 않는 옵션은 사용할 수 없습니다만... 엄청나게 많은 옵션을 제공하고 있습니다. 진행하고 있는 프로젝트가 leaflet
지도 라이브러리와 함께하며 직면한 문제는, 기능적인 제한사항이므로 openlayers
로 선정하는데 이견이 없었습니다.
다만 이미 덩치가 커져버린 프로젝트 속 leaflet
기능들을 어떻게 덜어내고 openlayers
를 어떻게 심어야 하는지의 논의가 필요했습니다.
interface
들을 모으는 것이 첫번째였습니다. 여기서 interface
란 typescript
에서 사용하는 interface
를 포함한 보다 넓은 범위의 구현을 뜻합니다.interface
를 모아 구현들을 파악했다면 확장성을 생각해야 했습니다. 기능 구현의 한계로 인해 라이브러리 교체 사태가 발생했기 때문에 앞으로의 구현과 더 나아간 미래에 대한 구현을 염두했어야 했습니다.leaflet
, openlayers
는 각각 지도 라이브러리라는 공통점을 가지면서도 상당히 다른 방식의 사용을 제공하므로 leaflet
을 초점으로 맞춰진 추상화 컴포넌트를 손보면서도, 최대한 적은 작업시간을 가져가기 위해 현재에서 크게 벗어나지 않는 인터페이스로의 openlayers
이식 작업이 요구됐습니다.서버 <-> 클라이언트
환경에서 잘 사용될 수 있는 openlayers7
레이어가 필요하다 판단했고 React Component
형태를 가진 라이브러리를 만들어보자는 결론에 도달합니다. openlayers
의 모든 클래스들을 컴포넌트로 구현한 라이브러리입니다.
예를 들면 openlayers
에서 마커를 구현하기 위해 아래에 기술된 객체들이 필요합니다.
Point
Feature
VectorSource
VectorLayer
Map
각각의 객체는 마커를 지도위에 표현되기 위해 스스로의 역할을 하고 있습니다(구체적인 롤 설명은 생략하겠습니다)
바닐라 자바스크립트를 통해 구현하려면
const point = new Point([0, 0])
const feature = new Feature(point)
const vectorSource = new VectorSource({
features: [feature]
})
const vectorLayer = new VectorLayer({
source: vectorSource
})
map.addLayer(vectorLayer)
위와 같은 단계를 거쳐 지도위에 표현됩니다.
rlayers
라이브러리는 openlayers
의 추상화 단계를 그대로 옮기는 컨셉으로 만들어졌습니다.
<RMap className='example-map' initial={{center: fromLonLat([2.364, 48.82]), zoom: 11}}>
<ROSM />
<RLayerVector zIndex={10}>
<RStyle.RStyle>
<RStyle.RIcon src={locationIcon} anchor={[0.5, 0.8]} />
</RStyle.RStyle>
<RFeature
geometry={new Point(fromLonLat([2.295, 48.8737]))}
onClick={(e) => e.map.getView().fit(e.target.getGeometry().getExtent(), {
duration: 250,
maxZoom: 15
})
}
>
<ROverlay className='example-overlay'>
Arc de Triomphe
<br />
<em>⬉ click to zoom</em>
</ROverlay>
</RFeature>
</RLayerVector>
</RMap>
위 코드는 rlayers
공식 문서에 기술되어 있는 기본 사용법으로 코드를 살펴보자면 feautre
를 담고 Vector
에서 스타일과 오버레이를 커버하고 있으며 앞서 언급했듯이 openlayers
가 가지고 있는 객체들을 거의 그대로 옮겨 컴포넌트로 구현했습니다.
이런 부분들은 openlayers
의 모든 기능을 사용할 수 있다는 장점을 가집니다. 실제로 거의 대부분의 옵션들을 컴포넌트 props
로 받기 때문에 Openlayers
의 모든 기능을 react component
로 구현이 가능합니다.
하지만 진행하고 있는 프로젝트와 Openlayers
의 그 중간을 연결시켜줄 레이어가 필요했습니다. leaflet
으로 작업된 프로젝트를 openlayers
로 옮기려면 완전히 leaflet
과 닮은 정도는 아니지만 openlayers
와의 중간 그 어딘가에 위치한 레이어링이 필요했습니다.
진행중인 프로젝트에서 지도는 아주 중요한 역할을 하고 있지만 핵심 도메인요소는 아니었기 때문에 openlayers
사용법을 팀에 있는 모든 프론트엔드 개발자가 알아야할 필요성도, 시간도 없었기 때문입니다.
React-Leaflet
은 아주 쉽고 단순하게 추상화된 React 컴포넌트 라이브러리
입니다.
const position = [51.505, -0.09]
render(
<MapContainer center={position} zoom={13} scrollWheelZoom={false}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={position}>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
</MapContainer>
)
실제로는 위 코드에서 파악할 수 있는 구조에 더 복잡한 구조를 가지고 있음에도, 지도에서 보이는 요소 이름만을 사용해 완성된 지도를 구현할 수 있도록 하고 있습니다.
leaflet
자체가 가진 기술적 한계로 표현하고자 하는 바가 제한되지만, 매우 좋은 러닝커브를 가지고 있다는 점에서 참고할만하다고 판단했습니다.
Openlayers
를 사용하고 있지만 필요 이상으로 과도하게 닮아있는 rlayers
와 직관적이고 쉽게 추상화되어 있지만 기능적 한계가 있는 React-Leaflet
의 그 중간 어딘가에 추상화를 해보기로 합니다.
기존 프로젝트가 사용하고 있는 인터페이스의 변화를 최소화 하면서 기능적 확장성을 늘리는 방향으로 설정했습니다.(구현은... 내부가 알아서 해주면 되ㄴ...)
react component
를 만들기 전, 어떤 컴포넌트를 만들고 그 컴포넌트에 어디부터 어디까지의 책임을 맡길것이며 어떤 인터페이스를 가지게 될지 설계하는 작업이 필요했습니다.
특히 한개의 canvas
안에 이런 저런것들이 렌더링 되는 openlayers
를 어떻게 react component
로 만들것인지에 대해서도 고민이 필요했습니다.
Openlayers
각 기능의 라이프 사이클을, 어떻게하면 React 라이프 사이클에 녹일 수 있을지 고민이 필요했습니다.
openlayers
에 대해 학습하며 openlayers
가 react
를 상당히 닮았다는 생각이 들었습니다. 바로 리액트의 렌더링과 openlayers
의 드로잉이 방식이 그랬습니다.
<Parent>
<Child></Child>
</Parent>
리액트는 App
컴포넌트 하위로 이런저런 컴포넌트들이 달려있는 SPA
로 구성됩니다.
컴포넌트의 children property
를 통해 하위 트리가 구성됩니다. 트리에서 컴포넌트가 제외되거나 추가되면, 그 컴포넌트의 상위 컴포넌트가 렌더링됩니다.
const vectorLayer = new VectorLayer()
map.addLayer(vectorLayer)
Openlayers
도 하나의 canvas
아래, 정확히 말하자면 map
객체 아래에 여러가지 레이어, 소스들, 피처 등 다른 요소들이 얽혀있는 형태로 표현됩니다.
리액트의 componentWillMount
처럼 map.removeLayer(vectorLayer)
로 제거될 수 있습니다.
ex)
feature.setStyle(new Style({
text: ...,
image: ...
}))
리액트 컴포넌트들이 자신만의 properties
를 가지고 그 값이 업데이트됨에 따라 벌어질 작업들을 설정할 수 있듯이, openlayers
도 VectorLayer
, VectorSource
, Feature
등 각각의 단계에서 설정된 값을 변경하도록 하는 메소드들을 가지고 있습니다.
결론적으로 리액트 라이프사이클에 openlayers
의 기능 사이클을 충족시켜 구현하는 방향으로 컨셉을 잡게 됐습니다.