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

Peter·2023년 11월 9일
2

추상화 범위

그렇다면 어디 범위까지 추상화할 것인지 결정하는 문제가 남았습니다.
앞서 언급했던 것처럼 openlayers가 제공하는 모든 기능을 담은 라이브러리를 만드는 것이 목적이 아닌 다소 깊은 내용을 포기하더라도 사용성을 쉽게 해 빠른 개발이 가능하도록 하는 것이 목적이기 때문에 필수 기능부터 불필요한 기능까지 나누는 작업이 진행됐습니다.

가장 중요한 Map을 다루는 컴포넌트를 예시로 들자면

  • 줌이 가능한지
  • 회전이 가능한지
  • 센터 좌표
  • 줌 레벨
  • 화면에 표시할 bounds (왼쪽위 좌표, 오른쪽 아래 좌표)
  • 최대 줌, 최소 줌
  • 맵 컨테이너의 높이와 너비
  • openstreet 타일 사용 여부

위 기능이 필수적이 기능으로 분류되었습니다.

interface MapProps {
  
  /**
   * @default true
   */
  scrollWheelZoom?: boolean;

  /**
   * @default 24
   */
  maxZoom?: number;

  /**
   * @default 3
   */
  minZoom?: number;
  
  /**
   * @default true
   */
  fullscreenControl?: boolean;

  /**
   * @default true.
   */
  isZoomAbled?: boolean;

  /**
   * @default true.
   */
  isRotateAbled?: boolean;

  /**
   * @default [127.9745613, 37.3236563]
   */
  center?: Location;

  /**
   * @default 15
   */
  zoomLevel?: number;

  /**
   * @default null
   * @description [[minX, minY], [maxX, maxY]]
   */
  bounds?: [Location, Location];

  /**
   * @default "1000px"
   */
  height?: string;

  /**
   * @default "1000px"
   */
  width?: string;

  /**
   * @default true
   */
  isShownOsm?: boolean;

  /**
   * @default false
   * @description If you set this property to 'true', you can see selection of annotations.
   */
  isAbledSelection?: boolean;

  children?: ReactNode;
}

인터페이스를 선언적으로 만들어주고 구현을 시작했습니다.

하지만...

완성하고 마이그레이션에 들어갔더니 '작은 예외'들이 발생하기 시작했습니다.
인터페이스에 넣자니 사소하고, 기능을 빼자니 그럴 수 없는 문제들이 발생했습니다.

결국 사용자가 Map을 커스터마이징 해야 하는 상황을 인정할 수 밖에 없었습니다.

다시 고민을 시작합니다.
모든 기능을 제공 vs 필요한 기능만 추상화

두가지 고민을 하다가 결국 지도는 Map 객체로부터 시작해서 모든 기능들이 Map 에 담긴다는 것에 힌트를 얻었고 forwardRefcontextAPI 를 통해 화면에 그려진 Map을 담아 부모에서도 사용할 수 있고 자식에서도 사용할 수 있도록 해줍니다.

 const Map = forwardRef<Map, MapProps>(
    (
      {
		...
      },
      ref
    ) => {
export function FeatureStore({
  isAbledSelection = false,
  children,
}: FeatureStoreProps) {
  const map = useMap();
  ...구현...
}

context api로 내려준 map 객체는 Map 인터페이스가 담지 못한 기능들을, 별도의 책임을 가진 컴포넌트로 구현할때 사용할 수 있습니다.

Map을 기본 틀이라고 생각했더니 Map 컴포넌트의 책임을 분리시키면서 확장성을 챙겨올 수 있었습니다.

Dom에 그리기

openlayers는 한개의 div를 사용해 canvas를 자식으로 만들고 그 위에 여러가지 레이어들을 쌓아 화면에 그려주는 프로세스를 가지고 있습니다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Quick Start</title>
  </head>
  <body>
    <div id="map"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>

오픈레이어 공식문서 에 표기되어 있는 예시 html 형태입니다.
id가 map인 div 위에 지도를 그려주는 형태입니다.

const Map = () => {
  	const map = new Map()
    
    map.setTarget('map')
  	
	return <div id="map"></div>
}

위 코드처럼 Map 컴포넌트 구현이 시작됐습니다.
하지만 곧 문제를 만나게 되는데 '같은 화면 여러곳에서 동시에 지도를 사용할 수 있다는 가능성'이었습니다.
id를 map으로 사용하는 div 를 사용하고 있기 때문에 여러곳에서 Map 컴포넌트를 사용하게 되면 id가 map 인 div가 여러개 생성된다는 것을 의미합니다.

저는 React 가 제공하는 useId 훅을 사용해보기로 합니다

useId란 컴포넌트에서 고유값을 사용해야 할때 화면안에 겹치는 값이 없도록 고유의 값을 뱉어주는 훅입니다.

const Map = () => {
  	const map = new Map();
    const id = useId();
    const mapId = `react-openlayers-map-${id}`;
    
    map.setTarget(mapId)
  	
	return <div id={mapId}></div>
}

위 코드처럼 이제 각 맵이 고유의 값을 가지게 되었고 같은 화면에서 여러개의 맵을 사용할 수 있도록 구현됩니다.

맵이 쌓이는 문제

const Map = () => {
  	const map = new Map();
    const id = useId();
    const mapId = `react-openlayers-map-${id}`;
    
    map.setTarget(mapId)
  	
	return <div id={mapId}></div>
}

이 컴포넌트의 문제점은 Map 컴포넌트가 렌더링될때마다 Map 이 계속해서 인스턴스화 되어 메모리에 누적된다는 것입니다.

const Map = () => {
  	const map = useRef<Map>(new Map())
    const id = useId();
    const mapId = `react-openlayers-map-${id}`;
    
  	useEffect(() => {
    	map.current.setTarget(mapId)
      
      	return () => {
        	map.current.setTarget(undefined)
          	map.current = null
        }
    }, []) 
    
  	
	return <div id={mapId}></div>
}

Map 인스턴스가 컴포넌트 렌더링에 영향을 받지 않도록 구조를 변경하고
컴포넌트가 unmount 될때 인스턴스를 망각하도록 수정해줬습니다.

결론

결국 Map 인스턴스를 다루고 도움을 주는 여러 객체들을 만들어 조합을 해서 사용하는 라이브러리기 때문에 나눠진 객체의 책임과, 그 객체를 담고 있는 컴포넌트의 책임이 일치하는 것이 얼마나 중요한지를 알게되는 작업이었습니다.

추상화 작업에선 인터페이스가 인스턴스의 역할을 제한하고
컴포넌트의 라이프 사이클에 인스턴스들을 생명주기를 동기화 시켜주는 것이 중요했습니다.

profile
컴퓨터가 좋아

0개의 댓글