perf: Map/Set mutable update + 버전 카운터 패턴 적용
mergeFeatures, updateCountsThrottled, deleteFeatureById, deleteFeaturesByIds, clearDarkSignals에서 new Map()/new Set() 전체 복사를 제거하고 기존 인스턴스를 직접 mutate. Zustand 변경 감지는 featuresVersion/darkSignalVersion 카운터로 트리거. 5000척 기준 배치당 O(5000) Map 복사 → O(batch) 변경으로 개선. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
부모
f4f0cb274f
커밋
ce54d9d0db
@ -224,12 +224,14 @@ export default function useShipLayer(map) {
|
|||||||
}, [map, initDeck, render, syncViewState, handleBatchRender]);
|
}, [map, initDeck, render, syncViewState, handleBatchRender]);
|
||||||
|
|
||||||
// 선박 데이터 변경 시 레이어 업데이트
|
// 선박 데이터 변경 시 레이어 업데이트
|
||||||
|
// ※ 성능 최적화: features/darkSignalIds는 mutable이므로 참조 비교 불가
|
||||||
|
// → featuresVersion/darkSignalVersion(숫자)으로 변경 감지
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 스토어 구독하여 변경 감지
|
// 스토어 구독하여 변경 감지
|
||||||
const unsubscribe = useShipStore.subscribe(
|
const unsubscribe = useShipStore.subscribe(
|
||||||
(state) => [state.features, state.kindVisibility, state.sourceVisibility, state.isShipVisible, state.detailModals, state.isIntegrate, state.nationalVisibility, state.showLabels, state.labelOptions, state.darkSignalVisible, state.darkSignalIds],
|
(state) => [state.featuresVersion, state.kindVisibility, state.sourceVisibility, state.isShipVisible, state.detailModals, state.isIntegrate, state.nationalVisibility, state.showLabels, state.labelOptions, state.darkSignalVisible, state.darkSignalVersion],
|
||||||
(current, prev) => {
|
(current, prev) => {
|
||||||
// 필터 변경 감지 (kindVisibility, sourceVisibility, isShipVisible, isIntegrate, nationalVisibility, showLabels, labelOptions, darkSignalVisible, darkSignalIds)
|
// 필터 변경 감지 (kindVisibility, sourceVisibility, isShipVisible, isIntegrate, nationalVisibility, showLabels, labelOptions, darkSignalVisible, darkSignalVersion)
|
||||||
const filterChanged =
|
const filterChanged =
|
||||||
current[1] !== prev[1] ||
|
current[1] !== prev[1] ||
|
||||||
current[2] !== prev[2] ||
|
current[2] !== prev[2] ||
|
||||||
|
|||||||
@ -187,13 +187,24 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
|
|||||||
// 상태 (State)
|
// 상태 (State)
|
||||||
// =====================
|
// =====================
|
||||||
|
|
||||||
/** 선박 데이터 맵 (featureId -> shipData), featureId = signalSourceCode + targetId */
|
/** 선박 데이터 맵 (featureId -> shipData), featureId = signalSourceCode + targetId
|
||||||
|
* ※ 성능 최적화: Map 인스턴스를 직접 mutate하고 featuresVersion으로 변경 감지
|
||||||
|
* (5000척 기준 new Map() 전체 복사 제거 → 배치당 O(batch)만 발생) */
|
||||||
features: new Map(),
|
features: new Map(),
|
||||||
|
|
||||||
|
/** features 변경 버전 카운터 (Zustand 참조 동등성 감지용)
|
||||||
|
* features Map은 동일 인스턴스를 유지하면서 내부만 변경하므로,
|
||||||
|
* 구독자가 변경을 감지할 수 있도록 버전 번호를 증가시킨다. */
|
||||||
|
featuresVersion: 0,
|
||||||
|
|
||||||
/** 다크시그널 선박 ID Set (features와 별도 관리, 메인 프로젝트 동일 구조)
|
/** 다크시그널 선박 ID Set (features와 별도 관리, 메인 프로젝트 동일 구조)
|
||||||
* 참조: mda-react-front/src/shared/model/deckStore.ts - darkSignalIds */
|
* 참조: mda-react-front/src/shared/model/deckStore.ts - darkSignalIds
|
||||||
|
* ※ 성능 최적화: Set 인스턴스를 직접 mutate하고 darkSignalVersion으로 변경 감지 */
|
||||||
darkSignalIds: new Set(),
|
darkSignalIds: new Set(),
|
||||||
|
|
||||||
|
/** darkSignalIds 변경 버전 카운터 */
|
||||||
|
darkSignalVersion: 0,
|
||||||
|
|
||||||
/** 선박 종류별 카운트 */
|
/** 선박 종류별 카운트 */
|
||||||
kindCounts: { ...initialKindCounts },
|
kindCounts: { ...initialKindCounts },
|
||||||
|
|
||||||
@ -269,53 +280,57 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
|
|||||||
* @param {Array} ships - 선박 데이터 배열
|
* @param {Array} ships - 선박 데이터 배열
|
||||||
*/
|
*/
|
||||||
mergeFeatures: (ships) => {
|
mergeFeatures: (ships) => {
|
||||||
set((state) => {
|
// ※ 성능 최적화: Map/Set을 직접 mutate하여 O(n) 전체 복사를 제거
|
||||||
const newFeatures = new Map(state.features);
|
// 기존: new Map(state.features) → O(5000) 복사 + O(batch) 변경
|
||||||
const newDarkSignalIds = new Set(state.darkSignalIds);
|
// 최적화: state.features에 직접 set/delete → O(batch)만 발생
|
||||||
const newChangedIds = new Set();
|
// Zustand 변경 감지는 featuresVersion 카운터로 트리거
|
||||||
|
const state = get();
|
||||||
|
const { features, darkSignalIds } = state;
|
||||||
|
let darkChanged = false;
|
||||||
|
|
||||||
ships.forEach((ship) => {
|
ships.forEach((ship) => {
|
||||||
const featureId = ship.featureId;
|
const featureId = ship.featureId;
|
||||||
if (!featureId) return;
|
if (!featureId) return;
|
||||||
|
|
||||||
// 좌표가 없으면 스킵
|
// 좌표가 없으면 스킵
|
||||||
if (!ship.longitude || !ship.latitude) {
|
if (!ship.longitude || !ship.latitude) {
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActive = isAnyEquipmentActive(ship);
|
||||||
|
|
||||||
|
// 규칙 1: LOST=0(영해 내) + 모든 장비 비활성 → 저장하지 않음 (완전 삭제)
|
||||||
|
if (!ship.lost && !hasActive) {
|
||||||
|
features.delete(featureId);
|
||||||
|
if (darkSignalIds.delete(featureId)) darkChanged = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다크시그널 상태 판정
|
||||||
|
if (hasActive) {
|
||||||
|
// 장비 활성 → 다크시그널 해제 (회복)
|
||||||
|
if (darkSignalIds.delete(featureId)) darkChanged = true;
|
||||||
|
} else {
|
||||||
|
// 모든 장비 비활성 (상태 전환용 신호)
|
||||||
|
// → 이미 다크시그널이면 유지, 아니면 등록
|
||||||
|
// → 상태 플래그(위치/시간 등)는 갱신하되 카운트에 반영하지 않음
|
||||||
|
if (!darkSignalIds.has(featureId)) {
|
||||||
|
darkSignalIds.add(featureId);
|
||||||
|
darkChanged = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hasActive = isAnyEquipmentActive(ship);
|
// receivedTimestamp 부여: 서버 수신시간 기반 (초기 로드 시 정확한 경과 시간 판단)
|
||||||
|
// 장비 비활성 신호도 상태 플래그(위치, 시간 등)는 항상 갱신
|
||||||
// 규칙 1: LOST=0(영해 내) + 모든 장비 비활성 → 저장하지 않음 (완전 삭제)
|
features.set(featureId, { ...ship, receivedTimestamp: parseReceivedTime(ship.receivedTime) });
|
||||||
if (!ship.lost && !hasActive) {
|
|
||||||
newFeatures.delete(featureId);
|
|
||||||
newDarkSignalIds.delete(featureId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다크시그널 상태 판정
|
|
||||||
if (hasActive) {
|
|
||||||
// 장비 활성 → 다크시그널 해제 (회복)
|
|
||||||
newDarkSignalIds.delete(featureId);
|
|
||||||
} else {
|
|
||||||
// 모든 장비 비활성 (상태 전환용 신호)
|
|
||||||
// → 이미 다크시그널이면 유지, 아니면 등록
|
|
||||||
// → 상태 플래그(위치/시간 등)는 갱신하되 카운트에 반영하지 않음
|
|
||||||
newDarkSignalIds.add(featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// receivedTimestamp 부여: 서버 수신시간 기반 (초기 로드 시 정확한 경과 시간 판단)
|
|
||||||
// 장비 비활성 신호도 상태 플래그(위치, 시간 등)는 항상 갱신
|
|
||||||
newFeatures.set(featureId, { ...ship, receivedTimestamp: parseReceivedTime(ship.receivedTime) });
|
|
||||||
newChangedIds.add(featureId);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
features: newFeatures,
|
|
||||||
darkSignalIds: newDarkSignalIds,
|
|
||||||
changedIds: newChangedIds,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 버전 카운터 증가로 구독자에게 변경 알림
|
||||||
|
set((s) => ({
|
||||||
|
featuresVersion: s.featuresVersion + 1,
|
||||||
|
...(darkChanged ? { darkSignalVersion: s.darkSignalVersion + 1 } : {}),
|
||||||
|
}));
|
||||||
|
|
||||||
// 카운트 재계산 (다크시그널 전환, 레이더 삭제도 여기서 처리)
|
// 카운트 재계산 (다크시그널 전환, 레이더 삭제도 여기서 처리)
|
||||||
// 쓰로틀 카운트 업데이트
|
// 쓰로틀 카운트 업데이트
|
||||||
get().updateCountsThrottled();
|
get().updateCountsThrottled();
|
||||||
@ -420,24 +435,21 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
|
|||||||
countCache.lastCalcTime = now;
|
countCache.lastCalcTime = now;
|
||||||
countCache.lastFilterHash = currentFilterHash;
|
countCache.lastFilterHash = currentFilterHash;
|
||||||
|
|
||||||
// features/darkSignalIds 변경이 필요한 경우
|
// features/darkSignalIds 변경이 필요한 경우 (직접 mutate + 버전 카운터)
|
||||||
if (newDarkIds.length > 0 || deleteIds.length > 0) {
|
if (newDarkIds.length > 0 || deleteIds.length > 0) {
|
||||||
const newFeatures = new Map(features);
|
newDarkIds.forEach((fid) => darkSignalIds.add(fid));
|
||||||
const newDarkSignalIds = new Set(darkSignalIds);
|
|
||||||
|
|
||||||
newDarkIds.forEach((fid) => newDarkSignalIds.add(fid));
|
|
||||||
deleteIds.forEach((fid) => {
|
deleteIds.forEach((fid) => {
|
||||||
newFeatures.delete(fid);
|
features.delete(fid);
|
||||||
newDarkSignalIds.delete(fid);
|
darkSignalIds.delete(fid);
|
||||||
});
|
});
|
||||||
|
|
||||||
set({
|
set((s) => ({
|
||||||
features: newFeatures,
|
featuresVersion: s.featuresVersion + 1,
|
||||||
darkSignalIds: newDarkSignalIds,
|
darkSignalVersion: s.darkSignalVersion + 1,
|
||||||
kindCounts: newKindCounts,
|
kindCounts: newKindCounts,
|
||||||
totalCount,
|
totalCount,
|
||||||
darkSignalCount: newDarkSignalCount,
|
darkSignalCount: newDarkSignalCount,
|
||||||
});
|
}));
|
||||||
} else {
|
} else {
|
||||||
set({
|
set({
|
||||||
kindCounts: newKindCounts,
|
kindCounts: newKindCounts,
|
||||||
@ -470,19 +482,15 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
|
|||||||
* @param {string} featureId - 삭제할 선박 ID (signalSourceCode + targetId)
|
* @param {string} featureId - 삭제할 선박 ID (signalSourceCode + targetId)
|
||||||
*/
|
*/
|
||||||
deleteFeatureById: (featureId) => {
|
deleteFeatureById: (featureId) => {
|
||||||
set((state) => {
|
const state = get();
|
||||||
const newFeatures = new Map(state.features);
|
state.features.delete(featureId);
|
||||||
newFeatures.delete(featureId);
|
const darkChanged = state.darkSignalIds.delete(featureId);
|
||||||
|
|
||||||
const newDarkSignalIds = new Set(state.darkSignalIds);
|
set((s) => ({
|
||||||
newDarkSignalIds.delete(featureId);
|
featuresVersion: s.featuresVersion + 1,
|
||||||
|
...(darkChanged ? { darkSignalVersion: s.darkSignalVersion + 1 } : {}),
|
||||||
return {
|
selectedShipId: s.selectedShipId === featureId ? null : s.selectedShipId,
|
||||||
features: newFeatures,
|
}));
|
||||||
darkSignalIds: newDarkSignalIds,
|
|
||||||
selectedShipId: state.selectedShipId === featureId ? null : state.selectedShipId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 쓰로틀 카운트 업데이트
|
// 쓰로틀 카운트 업데이트
|
||||||
get().updateCountsThrottled();
|
get().updateCountsThrottled();
|
||||||
@ -493,21 +501,19 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
|
|||||||
* @param {Array<string>} featureIds - 삭제할 선박 ID 배열 (signalSourceCode + targetId)
|
* @param {Array<string>} featureIds - 삭제할 선박 ID 배열 (signalSourceCode + targetId)
|
||||||
*/
|
*/
|
||||||
deleteFeaturesByIds: (featureIds) => {
|
deleteFeaturesByIds: (featureIds) => {
|
||||||
set((state) => {
|
const state = get();
|
||||||
const newFeatures = new Map(state.features);
|
let darkChanged = false;
|
||||||
const newDarkSignalIds = new Set(state.darkSignalIds);
|
featureIds.forEach((featureId) => {
|
||||||
featureIds.forEach((featureId) => {
|
state.features.delete(featureId);
|
||||||
newFeatures.delete(featureId);
|
if (state.darkSignalIds.delete(featureId)) darkChanged = true;
|
||||||
newDarkSignalIds.delete(featureId);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
features: newFeatures,
|
|
||||||
darkSignalIds: newDarkSignalIds,
|
|
||||||
selectedShipId: featureIds.includes(state.selectedShipId) ? null : state.selectedShipId,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
set((s) => ({
|
||||||
|
featuresVersion: s.featuresVersion + 1,
|
||||||
|
...(darkChanged ? { darkSignalVersion: s.darkSignalVersion + 1 } : {}),
|
||||||
|
selectedShipId: featureIds.includes(s.selectedShipId) ? null : s.selectedShipId,
|
||||||
|
}));
|
||||||
|
|
||||||
// 쓰로틀 카운트 업데이트
|
// 쓰로틀 카운트 업데이트
|
||||||
get().updateCountsThrottled();
|
get().updateCountsThrottled();
|
||||||
},
|
},
|
||||||
@ -574,16 +580,16 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
|
|||||||
* 다크시그널 선박 일괄 삭제
|
* 다크시그널 선박 일괄 삭제
|
||||||
*/
|
*/
|
||||||
clearDarkSignals: () => {
|
clearDarkSignals: () => {
|
||||||
set((state) => {
|
const state = get();
|
||||||
const newFeatures = new Map(state.features);
|
state.darkSignalIds.forEach((fid) => {
|
||||||
state.darkSignalIds.forEach((fid) => {
|
state.features.delete(fid);
|
||||||
newFeatures.delete(fid);
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
features: newFeatures,
|
|
||||||
darkSignalIds: new Set(),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
state.darkSignalIds.clear();
|
||||||
|
|
||||||
|
set((s) => ({
|
||||||
|
featuresVersion: s.featuresVersion + 1,
|
||||||
|
darkSignalVersion: s.darkSignalVersion + 1,
|
||||||
|
}));
|
||||||
get().recalculateCounts();
|
get().recalculateCounts();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -843,17 +849,18 @@ const useShipStore = create(subscribeWithSelector((set, get) => ({
|
|||||||
countCache.lastCalcTime = 0;
|
countCache.lastCalcTime = 0;
|
||||||
countCache.lastFilterHash = '';
|
countCache.lastFilterHash = '';
|
||||||
|
|
||||||
set({
|
set((s) => ({
|
||||||
features: new Map(),
|
features: new Map(),
|
||||||
|
featuresVersion: s.featuresVersion + 1,
|
||||||
darkSignalIds: new Set(),
|
darkSignalIds: new Set(),
|
||||||
|
darkSignalVersion: s.darkSignalVersion + 1,
|
||||||
kindCounts: { ...initialKindCounts },
|
kindCounts: { ...initialKindCounts },
|
||||||
changedIds: new Set(),
|
|
||||||
selectedShipId: null,
|
selectedShipId: null,
|
||||||
selectedShipIds: [],
|
selectedShipIds: [],
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
darkSignalCount: 0,
|
darkSignalCount: 0,
|
||||||
});
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user