react-naver-maps-kit 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2,7 +2,6 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  let react = require("react");
3
3
  let react_jsx_runtime = require("react/jsx-runtime");
4
4
  let react_dom = require("react-dom");
5
- let react_dom_client = require("react-dom/client");
6
5
 
7
6
  //#region src/core/loader/loadNaverMapsScript.ts
8
7
  const NAVER_MAPS_SCRIPT_BASE_URL = "https://oapi.map.naver.com/openapi/v3/maps.js";
@@ -876,15 +875,23 @@ function useNaverMapInstance(options = {}) {
876
875
  //#endregion
877
876
  //#region src/overlays/marker-clusterer/ClustererContext.ts
878
877
  /**
879
- * `<MarkerClusterer>`가 제공하는 React Context.
878
+ * `<MarkerClusterer>`가 제공하는 registry Context.
879
+ * register/unregister/enabled만 포함하며, enabled가 바뀔 때만 갱신됩니다.
880
880
  *
881
- * `<Marker>`는 이 context를 통해 클러스터러에 자신을 등록합니다.
882
- * `null`이면 클러스터러 밖에 있는 것이므로 일반 마커로 동작합니다.
883
- *
884
- * @internal 라이브러리 내부 전용. 직접 사용하지 마세요.
881
+ * @internal
885
882
  */
886
883
  const ClustererContext = (0, react.createContext)(null);
887
884
  ClustererContext.displayName = "ClustererContext";
885
+ /**
886
+ * `<MarkerClusterer>`가 제공하는 visibility Context.
887
+ * 재계산 시마다 갱신되며, 클러스터링 안 된 마커 ID 집합을 담습니다.
888
+ *
889
+ * `<Marker>`는 이 context를 읽어 자신이 보여야 할지 판단합니다.
890
+ *
891
+ * @internal
892
+ */
893
+ const ClustererVisibilityContext = (0, react.createContext)(/* @__PURE__ */ new Set());
894
+ ClustererVisibilityContext.displayName = "ClustererVisibilityContext";
888
895
 
889
896
  //#endregion
890
897
  //#region src/overlays/marker/Marker.tsx
@@ -1036,11 +1043,15 @@ function toLatLngLiteral(position) {
1036
1043
  const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1037
1044
  const { map: contextMap, sdkStatus } = useNaverMap();
1038
1045
  const clustererRegistry = (0, react.useContext)(ClustererContext);
1046
+ const visibleIds = (0, react.useContext)(ClustererVisibilityContext);
1039
1047
  const isInsideClusterer = clustererRegistry !== null && clustererRegistry.enabled;
1048
+ const autoId = (0, react.useId)();
1049
+ const effectiveId = props.clustererItemId ?? autoId;
1050
+ const isHiddenByClusterer = isInsideClusterer && !visibleIds.has(effectiveId);
1040
1051
  const markerRef = (0, react.useRef)(null);
1041
1052
  const markerEventListenersRef = (0, react.useRef)([]);
1042
1053
  const onMarkerDestroyRef = (0, react.useRef)(props.onMarkerDestroy);
1043
- const [markerDiv, setMarkerDiv] = (0, react.useState)(null);
1054
+ const [markerDiv] = (0, react.useState)(() => document.createElement("div"));
1044
1055
  const [portalReady, setPortalReady] = (0, react.useState)(false);
1045
1056
  const hasChildren = props.children !== void 0 && props.children !== null;
1046
1057
  const targetMap = props.map ?? contextMap;
@@ -1050,8 +1061,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1050
1061
  });
1051
1062
  (0, react.useEffect)(() => {
1052
1063
  if (!clustererRegistry) return;
1053
- const id = props.clustererItemId;
1054
- if (id === void 0 || id === null) return;
1064
+ const id = props.clustererItemId ?? autoId;
1055
1065
  const latLng = toLatLngLiteral(props.position);
1056
1066
  if (!latLng) return;
1057
1067
  clustererRegistry.register({
@@ -1064,6 +1074,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1064
1074
  clustererRegistry.unregister(id);
1065
1075
  };
1066
1076
  }, [
1077
+ autoId,
1067
1078
  clustererRegistry,
1068
1079
  props.clustererItemId,
1069
1080
  props.position,
@@ -1071,17 +1082,10 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1071
1082
  props.icon
1072
1083
  ]);
1073
1084
  (0, react.useEffect)(() => {
1074
- if (isInsideClusterer) return;
1075
1085
  onMarkerDestroyRef.current = props.onMarkerDestroy;
1076
- }, [isInsideClusterer, props.onMarkerDestroy]);
1077
- (0, react.useEffect)(() => {
1078
- if (isInsideClusterer) return;
1079
- if (typeof document === "undefined") return;
1080
- setMarkerDiv(document.createElement("div"));
1081
- }, [isInsideClusterer]);
1086
+ }, [props.onMarkerDestroy]);
1082
1087
  (0, react.useEffect)(() => {
1083
- if (isInsideClusterer) return;
1084
- if (!hasChildren || !markerDiv) {
1088
+ if (!hasChildren) {
1085
1089
  setPortalReady(false);
1086
1090
  return;
1087
1091
  }
@@ -1097,11 +1101,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1097
1101
  return () => {
1098
1102
  observer.disconnect();
1099
1103
  };
1100
- }, [
1101
- isInsideClusterer,
1102
- hasChildren,
1103
- markerDiv
1104
- ]);
1104
+ }, [hasChildren, markerDiv]);
1105
1105
  const invokeMarkerMethod = (0, react.useCallback)((methodName, ...args) => {
1106
1106
  const marker = markerRef.current;
1107
1107
  if (!marker) return;
@@ -1157,7 +1157,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1157
1157
  setZIndex: (...args) => invokeMarkerMethod("setZIndex", ...args)
1158
1158
  }), [invokeMarkerMethod]);
1159
1159
  (0, react.useEffect)(() => {
1160
- if (isInsideClusterer) return;
1160
+ if (isHiddenByClusterer) return;
1161
1161
  if (sdkStatus !== "ready" || !targetMap || markerRef.current) return;
1162
1162
  if (hasChildren && !portalReady) return;
1163
1163
  try {
@@ -1173,7 +1173,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1173
1173
  propsRef.current.onMarkerError?.(normalizedError);
1174
1174
  }
1175
1175
  }, [
1176
- isInsideClusterer,
1176
+ isHiddenByClusterer,
1177
1177
  hasChildren,
1178
1178
  markerDiv,
1179
1179
  portalReady,
@@ -1181,7 +1181,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1181
1181
  targetMap
1182
1182
  ]);
1183
1183
  (0, react.useLayoutEffect)(() => {
1184
- if (isInsideClusterer) return;
1184
+ if (isHiddenByClusterer) return;
1185
1185
  const marker = markerRef.current;
1186
1186
  if (!marker || !targetMap) return;
1187
1187
  const rafId = requestAnimationFrame(() => {
@@ -1195,7 +1195,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1195
1195
  cancelAnimationFrame(rafId);
1196
1196
  };
1197
1197
  }, [
1198
- isInsideClusterer,
1198
+ isHiddenByClusterer,
1199
1199
  hasChildren,
1200
1200
  markerDiv,
1201
1201
  portalReady,
@@ -1214,7 +1214,7 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1214
1214
  props.collisionBoxSize
1215
1215
  ]);
1216
1216
  (0, react.useEffect)(() => {
1217
- if (isInsideClusterer) return;
1217
+ if (isHiddenByClusterer) return;
1218
1218
  const marker = markerRef.current;
1219
1219
  if (!marker) return;
1220
1220
  bindMarkerEventListeners(marker, markerEventListenersRef, buildMarkerEventBindings(propsRef));
@@ -1224,14 +1224,14 @@ const Marker = (0, react.forwardRef)(function MarkerInner(props, ref) {
1224
1224
  markerEventListenersRef.current = [];
1225
1225
  }
1226
1226
  };
1227
- }, [isInsideClusterer]);
1227
+ }, [isHiddenByClusterer]);
1228
1228
  (0, react.useEffect)(() => {
1229
- if (isInsideClusterer) return;
1229
+ if (isHiddenByClusterer) return;
1230
1230
  return () => {
1231
1231
  teardownMarker();
1232
1232
  };
1233
- }, [isInsideClusterer, teardownMarker]);
1234
- if (isInsideClusterer) return null;
1233
+ }, [isHiddenByClusterer, teardownMarker]);
1234
+ if (isHiddenByClusterer) return null;
1235
1235
  if (!hasChildren || !markerDiv) return null;
1236
1236
  return (0, react_dom.createPortal)(props.children, markerDiv);
1237
1237
  });
@@ -2225,7 +2225,8 @@ function padBounds(bounds, padding) {
2225
2225
  * 내부 registry에 위치·데이터를 등록합니다.
2226
2226
  * 2. `MarkerClusterer`는 지도 이벤트(`idle`, `move`, `zoom`) 발생 시
2227
2227
  * 현재 줌·뷰포트 기준으로 클러스터를 재계산합니다.
2228
- * 3. 클러스터 마커는 `createRoot`를 이용해 React 컴포넌트를 HTML 아이콘으로 렌더링합니다.
2228
+ * 3. 클러스터 마커는 `<Marker>` JSX로 렌더링되며, children으로 클러스터 아이콘이 전달됩니다.
2229
+ * 4. 클러스터에 포함되지 않은 단독 포인트는 원래 `<Marker>` children을 그대로 표시합니다.
2229
2230
  *
2230
2231
  * ## 기본 사용법
2231
2232
  *
@@ -2241,10 +2242,11 @@ function padBounds(bounds, padding) {
2241
2242
  * {points.map(p => (
2242
2243
  * <Marker
2243
2244
  * key={p.id}
2244
- * clustererItemId={p.id}
2245
2245
  * position={p.position}
2246
2246
  * item={p}
2247
- * />
2247
+ * >
2248
+ * <CustomPin />
2249
+ * </Marker>
2248
2250
  * ))}
2249
2251
  * </MarkerClusterer>
2250
2252
  * </NaverMap>
@@ -2252,7 +2254,6 @@ function padBounds(bounds, padding) {
2252
2254
  *
2253
2255
  * ## 주의 사항
2254
2256
  *
2255
- * - `<Marker>`에 반드시 `clustererItemId` prop을 지정해야 합니다.
2256
2257
  * - `<MarkerClusterer>`는 반드시 `<NaverMap>` 내부에 위치해야 합니다.
2257
2258
  * - `enabled={false}`로 설정하면 클러스터링이 해제되고 각 `<Marker>`가 개별 마커로 렌더링됩니다.
2258
2259
  *
@@ -2291,10 +2292,8 @@ function MarkerClusterer(props) {
2291
2292
  if (prev && prev !== algorithm) prev.destroy?.();
2292
2293
  };
2293
2294
  }, [algorithm]);
2294
- const pointMarkersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2295
- const clusterMarkersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2296
- const clusterIconRootsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2297
- const clusterIconContainersRef = (0, react.useRef)(/* @__PURE__ */ new Map());
2295
+ const [clusters, setClusters] = (0, react.useState)([]);
2296
+ const [visibleIds, setVisibleIds] = (0, react.useState)(/* @__PURE__ */ new Set());
2298
2297
  const helpersRef = (0, react.useRef)({
2299
2298
  zoomToCluster: () => {},
2300
2299
  fitBounds: () => {}
@@ -2327,94 +2326,22 @@ function MarkerClusterer(props) {
2327
2326
  (0, react.useEffect)(() => {
2328
2327
  onClusterClickRef.current = onClusterClick;
2329
2328
  }, [onClusterClick]);
2330
- const clusterIconRef = (0, react.useRef)(clusterIcon);
2331
- (0, react.useEffect)(() => {
2332
- clusterIconRef.current = clusterIcon;
2333
- }, [clusterIcon]);
2334
2329
  const recompute = (0, react.useCallback)(() => {
2335
2330
  if (!map || sdkStatus !== "ready" || !enabled) return;
2336
2331
  const zoom = map.getZoom();
2337
2332
  const bounds = getMapBounds(map);
2338
2333
  const items = Array.from(registryRef.current.values());
2339
- const { clusters, points } = algorithm.cluster(items, {
2334
+ const { clusters: rawClusters, points } = algorithm.cluster(items, {
2340
2335
  zoom,
2341
2336
  bounds
2342
2337
  });
2343
2338
  const maxItems = clusterData?.maxItemsInCluster;
2344
2339
  const includeItems = clusterData?.includeItems ?? true;
2345
- const processedClusters = clusters.map((c) => ({
2340
+ setClusters(rawClusters.map((c) => ({
2346
2341
  ...c,
2347
2342
  items: includeItems ? maxItems !== void 0 ? c.items?.slice(0, maxItems) : c.items : void 0
2348
- }));
2349
- const nextPointIds = new Set(points.map((p) => p.id));
2350
- const prevPointMarkers = pointMarkersRef.current;
2351
- for (const [id, marker] of prevPointMarkers) if (!nextPointIds.has(id)) {
2352
- marker.setMap(null);
2353
- prevPointMarkers.delete(id);
2354
- }
2355
- for (const point of points) {
2356
- const existing = prevPointMarkers.get(point.id);
2357
- if (existing) {
2358
- const pos = new naver.maps.LatLng(point.position.lat, point.position.lng);
2359
- existing.setPosition(pos);
2360
- if (point.markerOptions) existing.setOptions({ ...point.markerOptions });
2361
- } else {
2362
- const opts = {
2363
- position: new naver.maps.LatLng(point.position.lat, point.position.lng),
2364
- map
2365
- };
2366
- if (point.markerOptions) Object.assign(opts, point.markerOptions);
2367
- const marker = new naver.maps.Marker(opts);
2368
- prevPointMarkers.set(point.id, marker);
2369
- }
2370
- }
2371
- const nextClusterIds = new Set(processedClusters.map((c) => c.id));
2372
- const prevClusterMarkers = clusterMarkersRef.current;
2373
- const prevRoots = clusterIconRootsRef.current;
2374
- const prevContainers = clusterIconContainersRef.current;
2375
- for (const [id, marker] of prevClusterMarkers) if (!nextClusterIds.has(id)) {
2376
- marker.setMap(null);
2377
- prevClusterMarkers.delete(id);
2378
- const root = prevRoots.get(id);
2379
- if (root) {
2380
- root.unmount();
2381
- prevRoots.delete(id);
2382
- }
2383
- prevContainers.delete(id);
2384
- }
2385
- for (const cluster of processedClusters) {
2386
- const existingMarker = prevClusterMarkers.get(cluster.id);
2387
- const renderer = clusterIconRef.current;
2388
- const iconNode = renderer ? renderer({
2389
- cluster,
2390
- count: cluster.count
2391
- }) : DefaultClusterIcon({ count: cluster.count });
2392
- if (existingMarker) {
2393
- existingMarker.setPosition(new naver.maps.LatLng(cluster.position.lat, cluster.position.lng));
2394
- const root = prevRoots.get(cluster.id);
2395
- if (root) root.render(iconNode);
2396
- } else {
2397
- const container = document.createElement("div");
2398
- container.style.cursor = "pointer";
2399
- const root = (0, react_dom_client.createRoot)(container);
2400
- root.render(iconNode);
2401
- prevContainers.set(cluster.id, container);
2402
- prevRoots.set(cluster.id, root);
2403
- const marker = new naver.maps.Marker({
2404
- position: new naver.maps.LatLng(cluster.position.lat, cluster.position.lng),
2405
- map,
2406
- icon: { content: container },
2407
- clickable: true
2408
- });
2409
- naver.maps.Event.addListener(marker, "click", () => {
2410
- onClusterClickRef.current?.({
2411
- cluster,
2412
- helpers: helpersRef.current
2413
- });
2414
- });
2415
- prevClusterMarkers.set(cluster.id, marker);
2416
- }
2417
- }
2343
+ })));
2344
+ setVisibleIds(new Set(points.map((p) => p.id)));
2418
2345
  }, [
2419
2346
  map,
2420
2347
  sdkStatus,
@@ -2455,31 +2382,31 @@ function MarkerClusterer(props) {
2455
2382
  behavior?.debounceMs,
2456
2383
  recompute
2457
2384
  ]);
2458
- (0, react.useEffect)(() => {
2459
- return () => {
2460
- for (const marker of pointMarkersRef.current.values()) marker.setMap(null);
2461
- pointMarkersRef.current.clear();
2462
- for (const marker of clusterMarkersRef.current.values()) marker.setMap(null);
2463
- clusterMarkersRef.current.clear();
2464
- for (const root of clusterIconRootsRef.current.values()) root.unmount();
2465
- clusterIconRootsRef.current.clear();
2466
- clusterIconContainersRef.current.clear();
2467
- };
2468
- }, []);
2469
2385
  (0, react.useEffect)(() => {
2470
2386
  if (enabled) return;
2471
- for (const marker of pointMarkersRef.current.values()) marker.setMap(null);
2472
- pointMarkersRef.current.clear();
2473
- for (const marker of clusterMarkersRef.current.values()) marker.setMap(null);
2474
- clusterMarkersRef.current.clear();
2475
- for (const root of clusterIconRootsRef.current.values()) root.unmount();
2476
- clusterIconRootsRef.current.clear();
2477
- clusterIconContainersRef.current.clear();
2387
+ setClusters([]);
2388
+ setVisibleIds(/* @__PURE__ */ new Set());
2478
2389
  }, [enabled]);
2479
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClustererContext.Provider, {
2390
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClustererContext.Provider, {
2480
2391
  value: registry,
2481
- children
2482
- });
2392
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClustererVisibilityContext.Provider, {
2393
+ value: visibleIds,
2394
+ children
2395
+ })
2396
+ }), enabled && clusters.map((cluster) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Marker, {
2397
+ position: cluster.position,
2398
+ clickable: true,
2399
+ onClick: () => {
2400
+ onClusterClickRef.current?.({
2401
+ cluster,
2402
+ helpers: helpersRef.current
2403
+ });
2404
+ },
2405
+ children: clusterIcon ? clusterIcon({
2406
+ cluster,
2407
+ count: cluster.count
2408
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultClusterIcon, { count: cluster.count })
2409
+ }, cluster.id))] });
2483
2410
  }
2484
2411
  MarkerClusterer.displayName = "MarkerClusterer";
2485
2412