문제 해결

React Kakao Map에서 수천 개의 마커를 렌더링할 때 성능 최적화

돌핀코딩 2025. 5. 9. 14:59

프론트엔드 프로젝트에서 차량 위치를 표시하는 기능을 구현하던 중, 사업장마다 수천 대의 차량 정보를 Kakao Map에 마커로 표시해야 했다. 초반에는 react-kakao-maps-sdk의 <MapMarker>를 그대로 사용했으나, 마커가 많아질수록 다음과 같은 문제가 발생했다.

  • 2천 개 이상의 마커를 렌더링할 경우 페이지 전체가 멈추거나 느려지는 현상
  • 지도 줌 조작 후 사업장 변경 시 무한 로딩 현상 또는 이미지 요청 지연
  • 다른 페이지로 이동 시에도 대기 중이던 마커 이미지 요청이 남아있어 성능 저하

원인 분석

기존 구현은 다음과 같았다.

<MarkerClusterer>
  {carLocationList.map((item) => (
    <MapMarker position={item.position} />
  ))}
</MarkerClusterer>

 

즉, 마커 하나당 <MapMarker> 컴포넌트가 생성되며 React가 이를 모두 렌더링하게 했다. 이는 렌더링 성능과 가비지 컬렉션 처리에 심각한 병목을 일으켰다.


개선 목표

  1. 수천 개의 마커를 효율적으로 렌더링할 것
  2. React 상태 관리 최소화 (마커 직접 렌더링 제거)
  3. 기존처럼 마커 클릭 시 상세 팝업 기능 유지

해결 과정

1. <MapMarker> 제거 → Kakao API 직접 사용

React가 마커를 렌더링하지 않도록 하고, useEffect에서 직접 kakao.maps.Marker 객체를 만들어 MarkerClusterer에 추가하도록 변경했다.

const newMarkers = carLocationList.map(item => {
  const marker = new kakao.maps.Marker({
    position: new kakao.maps.LatLng(item.location.la, item.location.lo),
    image: markerImage,
  });
  marker.myData = item; // 팝업 데이터 저장

  kakao.maps.event.addListener(marker, 'click', () => {
    setOverlayPosition({ lat: item.location.la, lng: item.location.lo });
    setOverlayItem(item);
    setIsOverlayVisible(true);
  });

  return marker;
});

cluster.addMarkers(newMarkers);
setMarkerInstances(newMarkers);

 

2. 마커 이미지 useMemo로 캐싱

const markerImage = useMemo(() => {
  return new kakao.maps.MarkerImage(RedDotIcon, new kakao.maps.Size(16, 16), {
    offset: new kakao.maps.Point(8, 16),
  });
}, []);

 

이렇게 처리해 마커를 여러 번 렌더링하더라도 동일한 이미지 인스턴스를 재사용하여 이미지 중복 요청을 방지했다.

 

3. 팝업(CustomOverlayMap) 기능 유지

기존 <MapMarker> 내부의 children으로 처리하던 커스텀 팝업도 CustomOverlayMap을 활용해 다음과 같이 표시했다:

{isOverlayVisible && overlayPosition && (
  <CustomOverlayMap position={overlayPosition} yAnchor={1}>
    <div className="custom-marker">
      <div className="mb-2">
        <img
          src={CloseIcon}
          alt="닫기"
          className="close cursor-pointer"
          onClick={() => {
            setIsOverlayVisible(false);
            setOverlayItem(null);
          }}
        />
      </div>
      <div className="marker-data">
        {overlayItem.vehicleNumber} | {overlayItem.vehicleType}<br />
        {overlayItem.vehicleModel}
      </div>
      <img className="polygon" src={PolygonIcon} alt="마커-polygon" />
    </div>
  </CustomOverlayMap>
)}

 

마커를 클릭하면 팝업이 해당 위치에 나타나도록 했고, 클러스터를 클릭했을 때는 줌 확대만 되도록 유지했다.

 

4. 정리 및 메모리 누수 방지

페이지 이동 시 마커와 클러스터 모두 정리해 메모리 누수를 방지했다:

useEffect(() => {
  return () => {
    markerInstances.forEach(marker => marker.setMap(null));
    marketClusterRef.current?.clear();
  };
}, []);

결과

  • 수천 개의 마커를 표시해도 부드럽게 작동했다.
  • 사업장 변경 시 이미지 로딩 문제를 해소했다.
  • 클러스터 클릭 시 기본 줌 동작은 유지하고, 실제 마커를 클릭했을 때만 팝업이 노출되도록 처리했다.

마무리

이번 작업은 퍼포먼스가 민감한 맵 UI에서 "React와 DOM 조작의 적절한 분리"가 얼마나 중요한지를 보여준 예시였다.

특히 지도와 같이 외부 SDK에서 상태를 관리하는 컴포넌트에서는 React의 선언적 방식보다 Imperative 방식이 유리한 경우가 많다.


참고자료