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