그리드 레이아웃 리사이즈/줌 미반영 버그 수정 + 스크롤 성능 개선

증상: 창을 최대화하거나 썸네일 줌을 바꿔도 그리드가 옛 폭에 갇혀
오른쪽이 비고 느리게 느껴짐.

근본 원인: <For>가 인덱스로 셀 DOM을 재사용하는데 셀의 x/y/w/h를
item 함수 본문에서 1회만 계산해, layout 변경(리사이즈/줌) 시 재사용된
셀 위치가 갱신되지 않았음(뷰 변경=인덱스 전면 교체 때만 반영됐음).

수정:
- 셀 위치/크기를 반응형(box())으로 읽어 layout 변경 시 재배치
- 레이아웃 계산을 Web Worker→메인 스레드 동기 계산으로 전환
  (프로덕션 Tauri 빌드에서 module worker가 첫 메시지 후 무응답)
- 리사이즈/줌을 폴링(clientWidth+gridRowHeight 직접 읽기)+Ctrl휠 직접
  recompute로 구동, 스냅샷 변경은 effect로. 이벤트 누락에 견고.
- 가상 그리드 썸네일 lazy→eager 로딩으로 스크롤 중 즉시 표시

결과: 최대화 시 폭 99% 채움, 줌 재배치 정상, 스크롤 ~34→~57fps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
강 한
2026-07-17 08:30:19 +09:00
co-authored by Claude Opus 4.8
parent dd9d6bfa0c
commit 6bff43c4e9
2 changed files with 61 additions and 58 deletions
+78
View File
@@ -0,0 +1,78 @@
// Justified 레이아웃 — 그리디 행 채우기. 메인 스레드 동기 계산.
// (프로덕션 Tauri 빌드에서 module worker가 첫 메시지 후 응답하지 않는 이슈를 피하기 위해
// 워커 대신 동기 계산을 쓴다. 재계산은 리사이즈/줌/데이터 변경 시에만 발생하며
// 5만 항목도 수십 ms 내에 끝나 스크롤 성능에 영향 없다.)
export interface LayoutResult {
/** n*4: [x, y, w, h] */
boxes: Float32Array;
/** 행별 상단 y (마지막 요소 = 전체 높이) */
rowTops: Float32Array;
/** 행별 첫 항목 인덱스 */
rowFirst: Uint32Array;
totalHeight: number;
}
export function computeLayout(
ratios: Float32Array,
containerWidth: number,
targetRowHeight: number,
gap: number,
): LayoutResult {
const n = ratios.length;
const boxes = new Float32Array(n * 4);
const rowTopsArr: number[] = [];
const rowFirstArr: number[] = [];
const clamp = (r: number) => Math.min(4, Math.max(0.25, r || 1));
let y = 0;
let rowStart = 0;
let ratioSum = 0;
const flushRow = (end: number, justify: boolean) => {
const count = end - rowStart;
if (count <= 0) return;
const gaps = gap * (count - 1);
let h = targetRowHeight;
if (justify) {
h = (containerWidth - gaps) / ratioSum;
} else {
const natural = ratioSum * targetRowHeight + gaps;
if (natural > containerWidth) h = (containerWidth - gaps) / ratioSum;
}
let x = 0;
for (let i = rowStart; i < end; i++) {
const w = clamp(ratios[i]) * h;
boxes[i * 4] = x;
boxes[i * 4 + 1] = y;
boxes[i * 4 + 2] = w;
boxes[i * 4 + 3] = h;
x += w + gap;
}
rowTopsArr.push(y);
rowFirstArr.push(rowStart);
y += h + gap;
rowStart = end;
ratioSum = 0;
};
for (let i = 0; i < n; i++) {
ratioSum += clamp(ratios[i]);
const width = ratioSum * targetRowHeight + gap * (i - rowStart);
if (width >= containerWidth) {
flushRow(i + 1, true);
}
}
flushRow(n, false);
const totalHeight = y > 0 ? y - gap : 0;
rowTopsArr.push(totalHeight); // 마지막 = 전체 높이 (이진탐색 경계)
return {
boxes,
rowTops: new Float32Array(rowTopsArr),
rowFirst: new Uint32Array(rowFirstArr),
totalHeight,
};
}