네이버 마커 클러스터링을 적용해보자 - 2. Custom Marker에 마커 클러스터링 적용 (end)

sumi-0011·2023년 6월 12일
2
post-thumbnail

이전 글에서 react-naver-maps라이브러리의 마커 클러스터링 예제를 보며 마커 클러스터링 기능을 구현해 보았다.
하지만, 밑과 같은 이유로 예제를 내 프로젝트에 똑같이 적용할 수 있는 상황이 아니였다.

이유

  • icon을 이용한 custom maker를 지도에 띄워주어야 한다.
  • marker들을 서버에서 데이터를 받아와 동적으로 생성해주어야 한다.

어떻게 하면 React환경에서 동적으로 생성한 커스텀 마커들을 지도에 띄워줄 수 있을지 고민하였다.

react-naver-maps 라이브러리에서 클러스터링을 구현하는 방법

예제는 같이 new naver.maps.Marker을 이용해서 마커 객체를 생성하고,
생성한 마커 리스트를 cluster 객체에 넘겨 클러스터링을 구현한다.
즉, 생성된 마커 리스트를 cluster 객체에 넘겨주면 되는 것이다.

그렇다면 ref를 사용하면 되지 않을까?

new naver.maps.Marker을 이용해서 생성한 마커들은 dom 객체로 생성이 된다.
그래서 동적으로 생성한 마커의 dom 객체를 ref에 저장하고 ref들의 리스트를 cluster 객체를 만들 때 사용하면 되지 않을까? 라고 생각해보고 실행에 옮겼다.

ref란? : render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법

그 결과!!

ReactuseRef hook을 이용하여 마커들의 ref를 클러스터링 객체에 넘겨주는 방식으로 해결 할 수 있었다.

결론 : 생성한 마커의 ref 리스트를 이용해서 cluster 객체를 생성하자

먼저 구현되어 있던 코드들을 정리하면..

icon으로 커스텀 되어있는 마커 컴포넌트가 있고,
마커 리스트에 따라 map에 마커들을 표시해 주는 코드가 있었다.

Marker.tsx : 커스텀 마커 컴포넌트

interface Props {
  navermaps: typeof naver.maps;
  
  item: string;
  onClick: (item: string) => void;
}

function PreatMarker({ item, navermaps, onClick }: Props) {
  return (
    <NaverMarker
      position={new navermaps.LatLng(item.latitude, item.longitude)}
      icon={{
        url: iconUrl,
        scaledSize: new navermaps.Size(20, 20),
        origin: new naver.maps.Point(0, 0),
      }}
      onClick={(e) => {
        e.pointerEvent.stopPropagation();
        onClick(item);
      }}
    />
  );
}

Map.tsx

 function Map() {
  const navermaps = useNavermaps();
  const [map, setMap] = useState<naver.maps.Map | null>(null);

  const totalMakerList = ['marker1','marker2']
   
  return (
     <MapDiv css={mapCss}>
        <NaverMap defaultCenter={center} defaultZoom={16} ref={setMap}>
          <MarkerCluster markers={elRefs} markerInfo={totalMakerList} />
          {totalMakerList.map((item, idx) => {
            return (
              <PreatMarker
                key={item.id}
                navermaps={navermaps}
                item={item}
                onClick={onMarkerClick}
              />
            );
          })}
      </NaverMap>
    </MapDiv>
   )
 }

추가로 구현할 부분!

마커 컴포넌트를 이용해서 화면에 마커들을 띄워주는 부분은 구현이 되어 있었다.

추가로 구현해야 할 부분은 밑과 같았다.

  • 해당 마커들을 ref 리스트로 관리하고,
  • ref 리스트를 이용해서 cluster 객체를 만들기

ref 리스트를 생성하기

먼저 ref를 리스트로 관리하기 위해
해당 링크를 참고하여 ref리스트를 생성하였다.

const arrLength = arr.length;
const [elRefs, setElRefs] = React.useState([]);

React.useEffect(() => {
  // add or remove refs
  setElRefs((elRefs) =>
    Array(arrLength)
      .fill()
      .map((_, i) => elRefs[i] || createRef()),
  );
}, [arrLength]);

return (
  <div>
    {arr.map((el, i) => (
      <div ref={elRefs[i]} style={...}>
        ...
      </div>
    ))}
  </div>
);

2. 생성한 마커 컴포넌트를 ref에 저장하기

마커 컴포넌트의 props로 ref를 추가적으로 받도록 수정한다.
그리고 props로 받은 ref를 연결한다.

Marker.tsx : 커스텀 마커 컴포넌트

interface Props {
  navermaps: typeof naver.maps;
  ref: any; // ref 추가

  item: string;
  onClick: (item: string) => void;
}

function PreatMarker({ item, navermaps, onClick }: Props) {
  return (
    <NaverMarker
      ref={ref} // ref 추가
      position={new navermaps.LatLng(item.latitude, item.longitude)}
      icon={{
        url: iconUrl,
        scaledSize: new navermaps.Size(20, 20),
        origin: new naver.maps.Point(0, 0),
      }}
      onClick={(e) => {
        e.pointerEvent.stopPropagation();
        onClick(item);
      }}
    />
  );
}

refany타입으로 선언하였는데,
any를 쓰지 않고는 에러가 해결이 되지 않아서 어쩔 수 없는 선택이였습니다!
해결 방법을 아시는 분은 알려주세요 🙏

ref 리스트를 생성하고, 마커 컴포넌트에 연결한다

마커 컴포넌트를 사용하는 부분에서
마커의 개수 만큼 ref 리스트를 생성하고,
마커 컴포넌트의 ref props에 해당하는 index의 ref를 넘겨준다.

Map.tsx : 마커를 사용하는 부분

function Map() {
  const navermaps = useNavermaps();
  const [map, setMap] = useState<naver.maps.Map | null>(null);

  const totalMakerList = ['marker1','marker2']
   
  // *** 추가 ***
  const [elRefs, setElRefs] = useState<RefObject<naver.maps.Marker>[]>([]);

  useEffect(() => {
    setElRefs((elRefs) =>
      Array(arrLength)
        .fill('')
        .map((_, i) => elRefs[i] || createRef()),
    );
  }, [arrLength]);

  return (
     <MapDiv css={mapCss}>
        <NaverMap defaultCenter={center} defaultZoom={16} ref={setMap}>
          <MarkerCluster markers={elRefs} markerInfo={totalMakerList} />
          {totalMakerList.map((item, idx) => {
            return (
              <PreatMarker
                ref={elRefs[idx]}	// *** 추가 ***
                key={item.id}
                navermaps={navermaps}
                item={item}
                onClick={onMarkerClick}
              />
            );
          })}
      </NaverMap>
    </MapDiv>
   )
 }

마커 클러스터링 컴포넌트를 생성한다.

생성한 ref리스트를 넘겨, 클러스터링 객체를 생성한다.
클러스터링 객체 이용해 Overlay 컴포넌트에 넘겨, 클러스터를 지도에 띄운다.

function MarkerCluster({
  markers,
}: {
  markers: RefObject<naver.maps.Marker>[];
}) {

  const navermaps = useNavermaps();
  const map = useMap();

  const { htmlMarker1, htmlMarker2, htmlMarker3, htmlMarker4, htmlMarker5 } =
    useGetClusterIcon(navermaps); // 클러스트 아이콘 dom 리스트

  // https://github.com/zeakd/react-naver-maps/blob/main/website/src/samples/marker-cluster.js
  const MarkerClustering = makeMarkerClustering(window.naver);

  const getCluster = () => {
    const markerList = markers.map((_marker) => {
      return _marker.current;
    });

    const cluster = new MarkerClustering({
      minClusterSize: 2,
      maxZoom: 14, // 조절하면 클러스터링이 되는 기준이 달라짐 (map zoom level)
      map: map,
      markers: markerList.filter((marker) => marker),
      disableClickZoom: false,
      gridSize: 120,
      icons: [htmlMarker1, htmlMarker2, htmlMarker3, htmlMarker4, htmlMarker5],
      indexGenerator: [5, 10, 15, 20, 30],
      stylingFunction: function (clusterMarker: any, count: number) {
        clusterMarker.getElement().querySelector('div:first-child').innerText =
          count;
      },
    });

    return cluster;
  }

  // Customize Overlay 참고
  // https://zeakd.github.io/react-naver-maps/guides/customize-overlays/
  const [cluster, setCluster] = useState(getCluster());

  useEffect(() => {
    // 클러스트 객체 생성해서, 상태에 저장
    setCluster(getCluster());
  }, []);

  return (
    <Overlay element={{ ...cluster, setMap: () => null, getMap: () => null }} />
  );
}

여기 까지 마커 클러스터링 구현 끝~!

마커 클러스터링 조건에 해당하게 되면,
클러스터 되는 마커들은 보이지 않게 되고, 대신 클러스터 아이콘이 overlay 되어서 보이게 된다.

클러스터링 조건과, 클러스터 아이콘 등은 cluster 객체를 생성 할때 수정 할 수 있다.

클러스터링 구현 비교 사진

클러스터링 적용 전클러스터링 구현 후

참고 문헌

profile
안녕하세요 😚

1개의 댓글

comment-user-thumbnail
2023년 7월 22일

안녕하세요- Marker 의 ref 타입은 RefObject<naver.maps.Marker> 입니다!

답글 달기