[npm] React-Openlayers7 구현기(1)

Peter·2023년 6월 21일
5

React-Openlayers7

발단

현재 진행하고 있는 프로젝트는 지도 기능이 매우 중요한 프로젝트입니다. 지도위에 마커를 그리고, 선분을 그려 각 끝에 해당하는 좌표가 필요하고, 좌표에 해당하는 사진을 띄우기도 해야합니다. 초기에 작성된 요구사항 명세서대로 기능들을 완성했지만 시간이 지남에 따라 지도위에 여러가지 복잡한 기능들의 추가가 요구됐습니다.

프로젝트의 프로토타이핑 모델부터 지금까지 Leaflet 이라는 지도 라이브러리를 사용하고 있었습니다. 하지만 라이브러리가 가벼운 만큼 지도위에서 할 수 있는 일들이 한정적이었고, 이미 한계점이 들어났지만 이미 많은 작업을 Leaflet으로 해왔기 때문에, 사실상 구현이 불가한 요구사항들을 억지로 돌려 구현하며 떼우는 식으로 구현해왔습니다. 그러다 어플리케이션이 커짐에 따라서 '떼우는식'으로 지속하기 어려운 지경에 이르렀고 지도 라이브러리의 전면적인 교체가 요구되는 상황이 오고야 말았습니다.

Map Libray Leaflet vs Openlayers

시장에서 사용되는 지도 라이브러리는 상당히 많았습니다. 국내로는 카카오맵, 네이버지도가 서비스중이었고 해외로 눈을 돌리면 google맵, openlayers, leaflet, mapbox 등 몇가지가 주력으로 사용되고 있습니다.

서비스의 특성상 해외 지도가 필요했고 국내지도는 대한민국만을 커버하므로 선택지에서 제외됐습니다.

google맵은 추상화가 잘 되어 있는 맵 서비스였습니다. 다만 편의성을 위한 추상화가 목적이다보니 지도를 이렇게 저렇게 커스터마이징하고 기능을 추가할 수 있는 확장성이 떨어졌습니다.

mapbox 같은 경우 길찾기, 최단거리 탐색 등 신기하고 좋은 기능을 제공하고 있었습니다. 다만 기능 자체가 유료서비스인데다가 진행중인 프로젝트의 방향성과는 조금 다른 기능을 많이 담고 있었습니다. J커브를 기대하며(?) 트래픽이 발생할때마다 비용을 지불해야 하는 서비스를 선택할 수 없었습니다.

최종 후보로 선택된 라이브러리는 openlayers7 였습니다.상당히 많은 기능을 담고 있고 사용을 위해서 설정해야 하는 것들이 아주 많지만 설정할 수 있는 부분이 많다는건 자유도가 높다는 의미이자 여러가지 원하는 기능을 구현하기 좋다는 것을 의미했습니다. 그러나 현재 진행중인 프로젝트에 '적합해 보인다'는 것이지 구현하고자 하는 것을 완벽하게 커버할 수 있다는 결론은 내릴 수 없었습니다. 따라서 현재 사용중인 Leaflet과 최종 후보 Openlayers7 을 비교해봅니다.

Leaflet

Leaflet은 html dom을 적극적으로 사용하는 지도 라이브러리 입니다. 타일 이미지를 배치할때도 여러개의 사진 element들을 정해진 dom 안에 적절히 배치해 사용할 수 있도록 합니다.

장점

  • 라이브러리 무게가 3.74MB 로 상대적으로 가벼운 편입니다.
  • 가볍지만 어노테이션, 이미지, 비디오 오버레이, 커스텀 타일, geoJson 등 기본적은 모든 기능은 다 갖추고 있습니다.
  • 이런 저런 방법으로 구현을 하면 기본으로 제공하는 기능 이상으로 활용이 가능합니다.

단점

  • 고급 GIS 라고 할 수 있는 기능이 제한적입니다. 예를들면 Multi Polygon, Multi Point, Compass 같은 것들입니다. Compass 는 npm에서 따로 제공하는 라이브러리가 있지만 공식적으로 leaflet이 제공하고 있지 않아 사용이 조금 꺼려지는것이 사실입니다.

  • 여러개의 Dom을 사용하고 있기 때문에 렌더링 성능이 떨어집니다. polygon, circle 등 어노테이션을 svg element로 제공해 Dom에 올리는 방식입니다.
  • 위 사진이 보여주듯이 각각의 타일 이미지들을 불러와 img tag를 사용해 배치하는 형태이기 때문에 dom 렌더링에 상당한 시간이 걸립니다.
  • leaflet이 제공하고 있는 기능에 대한 문서화가 다소 늦습니다. types을 제공하고 있어 공식 문서보단 기능 인터페이스를 빠르게 알 수 있지만. [key:string]: string 와 같은 타입들이 등장하곤 합니다.

Openlayers

Openlayers는 canvas 엘리먼트안에 설정된 요소들을 벡터기반으로 표현해 지도를 렌더링하는 라이브러리입니다.

장점

  • Canvas안에 벡터들을 그려내기 때문에 Leaflet보다 빠른 렌더링 속도를 가집니다.
  • 방대한 양의 기능을 제공하고 있습니다. 세세한 기능들을 잘 활용한다면 안되는 것 빼곤 다 되는 정도의 양입니다. '이것도 되나?' 라는 생각이 들면 된다고 보시면 됩니다.

단점

  • 방대한 양의 기능들을 담고 있기 때문에 9.78MB 의 큰 무게를 가집니다.
  • 기능이 방대하다는 것은 좋지 않은 러닝커브를 의미합니다. 공부하기 상당히 어렵습니다. 특히 좌표계 맞추는 작업이 힘들었습니다.
    • 스타일링이 제한적입니다. STROKE, FILL 같이 선과 면을 설정하는데도 Openlayers가 정해준 객체를 사용해야 하므로 제공하지 않는 옵션은 사용할 수 없습니다만... 엄청나게 많은 옵션을 제공하고 있습니다.

Openlayers 선정

진행하고 있는 프로젝트가 leaflet 지도 라이브러리와 함께하며 직면한 문제는, 기능적인 제한사항이므로 openlayers로 선정하는데 이견이 없었습니다.

다만 이미 덩치가 커져버린 프로젝트 속 leaflet 기능들을 어떻게 덜어내고 openlayers를 어떻게 심어야 하는지의 논의가 필요했습니다.

기능 추상화

  1. 지도 관련 기능들이 사용하고 있는 interface들을 모으는 것이 첫번째였습니다. 여기서 interfacetypescript에서 사용하는 interface를 포함한 보다 넓은 범위의 구현을 뜻합니다.
  2. interface를 모아 구현들을 파악했다면 확장성을 생각해야 했습니다. 기능 구현의 한계로 인해 라이브러리 교체 사태가 발생했기 때문에 앞으로의 구현과 더 나아간 미래에 대한 구현을 염두했어야 했습니다.
  3. leaflet, openlayers는 각각 지도 라이브러리라는 공통점을 가지면서도 상당히 다른 방식의 사용을 제공하므로 leaflet을 초점으로 맞춰진 추상화 컴포넌트를 손보면서도, 최대한 적은 작업시간을 가져가기 위해 현재에서 크게 벗어나지 않는 인터페이스로의 openlayers 이식 작업이 요구됐습니다.
  4. 결론적으로 서버 <-> 클라이언트 환경에서 잘 사용될 수 있는 openlayers7 레이어가 필요하다 판단했고 React Component 형태를 가진 라이브러리를 만들어보자는 결론에 도달합니다.

React & Openlayers

벤치마킹

rlayers

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>&#11017; 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-Leaflet은 아주 쉽고 단순하게 추상화된 React 컴포넌트 라이브러리입니다.

const position = [51.505, -0.09]
        
render(
  <MapContainer center={position} zoom={13} scrollWheelZoom={false}>
    <TileLayer
      attribution='&copy; <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 와 Openlayers의 닮은꼴

라이프 사이클

react component 를 만들기 전, 어떤 컴포넌트를 만들고 그 컴포넌트에 어디부터 어디까지의 책임을 맡길것이며 어떤 인터페이스를 가지게 될지 설계하는 작업이 필요했습니다.

특히 한개의 canvas안에 이런 저런것들이 렌더링 되는 openlayers를 어떻게 react component로 만들것인지에 대해서도 고민이 필요했습니다.

Openlayers 각 기능의 라이프 사이클을, 어떻게하면 React 라이프 사이클에 녹일 수 있을지 고민이 필요했습니다.

openlayers에 대해 학습하며 openlayersreact를 상당히 닮았다는 생각이 들었습니다. 바로 리액트의 렌더링과 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를 가지고 그 값이 업데이트됨에 따라 벌어질 작업들을 설정할 수 있듯이, openlayersVectorLayer, VectorSource, Feature 등 각각의 단계에서 설정된 값을 변경하도록 하는 메소드들을 가지고 있습니다.

결론적으로 리액트 라이프사이클에 openlayers의 기능 사이클을 충족시켜 구현하는 방향으로 컨셉을 잡게 됐습니다.

profile
컴퓨터가 좋아

0개의 댓글