문제 해결
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가 이를 모두 렌더링하게 했다. 이는 렌더링 성능과 가비지 컬렉션 처리에 심각한 병목을 일으켰다.
개선 목표
- 수천 개의 마커를 효율적으로 렌더링할 것
- React 상태 관리 최소화 (마커 직접 렌더링 제거)
- 기존처럼 마커 클릭 시 상세 팝업 기능 유지
해결 과정
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 방식이 유리한 경우가 많다.