이미지 lazy loading 적용

This commit is contained in:
2026-05-14 11:56:31 +09:00
parent 190e9aa151
commit 2879e1ee0a
3 changed files with 49 additions and 28 deletions
+11 -2
View File
@@ -29,11 +29,20 @@ function _onEnter() {
clearTimeout(_timer); clearTimeout(_timer);
_showLoader(card); _showLoader(card);
_timer = setTimeout(() => { _timer = setTimeout(async () => {
_removeLoader(); _removeLoader();
const thumb = card.querySelector('img.card-thumb'); const thumb = card.querySelector('img.card-thumb');
if (!thumb) return; if (!thumb) return;
_imgEl.src = thumb.src;
// 아직 lazy-load가 안 된 경우 직접 로딩
let src = thumb.src;
if (!src) {
src = await window.api.readImage(card.dataset.filepath) || '';
if (src) thumb.src = src; // 카드에도 캐싱
}
if (!src) return;
_imgEl.src = src;
_visible = true; _visible = true;
_show(card); _show(card);
}, DELAY); }, DELAY);
+28 -26
View File
@@ -4,6 +4,8 @@ import * as selection from './handlers/selection.js';
import * as dragDrop from './handlers/drag-drop.js'; import * as dragDrop from './handlers/drag-drop.js';
import * as hoverPreview from './handlers/hover-preview.js'; import * as hoverPreview from './handlers/hover-preview.js';
let _cardObserver = null;
// ── DOM ────────────────────────────────────────────── // ── DOM ──────────────────────────────────────────────
const grid = document.getElementById('grid'); const grid = document.getElementById('grid');
const emptyState = document.getElementById('empty-state'); const emptyState = document.getElementById('empty-state');
@@ -70,40 +72,47 @@ async function openFolder() {
state.files.push(...list); state.files.push(...list);
state.selectedIdxs.clear(); state.selectedIdxs.clear();
setStatus(`${state.files.length}개 파일 로드됨`, true); setStatus(`${state.files.length}개 파일 로드됨`, true);
await renderGrid(); renderGrid();
updatePreview(); updatePreview();
btnRename.disabled = false; btnRename.disabled = false;
btnReveal.disabled = false; btnReveal.disabled = false;
} }
// ── 그리드 렌더링 ──────────────────────────────── // ── 그리드 렌더링 ────────────────────────────────
async function renderGrid() { function renderGrid() {
if (_cardObserver) { _cardObserver.disconnect(); _cardObserver = null; }
grid.innerHTML = ''; grid.innerHTML = '';
emptyState.style.display = 'none'; emptyState.style.display = 'none';
grid.style.display = 'grid'; grid.style.display = 'grid';
fileCount.textContent = `${state.files.length}개 파일`; fileCount.textContent = `${state.files.length}개 파일`;
updateSelCount(); updateSelCount();
showLoading(`썸네일 로딩 중... (0/${state.files.length})`); // 카드 껍데기만 즉시 생성 (이미지 없음)
state.files.forEach((file, idx) => grid.appendChild(createCard(file, idx)));
const BATCH = 10; // 뷰포트 ±400px 범위에 들어온 카드만 이미지 로딩
for (let i = 0; i < state.files.length; i += BATCH) { const observer = new IntersectionObserver((entries) => {
const batch = state.files.slice(i, i + BATCH); entries.forEach(async (entry) => {
const results = await Promise.all(batch.map(f => window.api.readImage(f.path))); if (!entry.isIntersecting) return;
results.forEach((dataUrl, bi) => { observer.unobserve(entry.target);
grid.appendChild(createCard(state.files[i + bi], i + bi, dataUrl)); const img = entry.target.querySelector('img.card-thumb');
if (!img || img.src) return;
const dataUrl = await window.api.readImage(entry.target.dataset.filepath);
if (dataUrl) img.src = dataUrl;
}); });
loadingText.textContent = `썸네일 로딩 중... (${Math.min(i + BATCH, state.files.length)}/${state.files.length})`; }, { root: gridContainer, rootMargin: '400px 0px', threshold: 0 });
}
Array.from(grid.children).forEach(card => observer.observe(card));
_cardObserver = observer;
hideLoading();
refreshCardSelection(); refreshCardSelection();
updateNumberBadges(); updateNumberBadges();
updateNewNameLabels(); updateNewNameLabels();
} }
// ── 카드 생성 ──────────────────────────────────── // ── 카드 생성 ────────────────────────────────────
function createCard(file, idx, dataUrl) { function createCard(file, idx) {
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card'; card.className = 'card';
card.dataset.idx = idx; card.dataset.idx = idx;
@@ -118,18 +127,11 @@ function createCard(file, idx, dataUrl) {
checkBadge.className = 'card-check'; checkBadge.className = 'card-check';
checkBadge.textContent = '✓'; checkBadge.textContent = '✓';
let thumb; // src 없이 생성 — IntersectionObserver가 뷰포트 진입 시 로딩
if (dataUrl) { const thumb = document.createElement('img');
thumb = document.createElement('img'); thumb.className = 'card-thumb';
thumb.className = 'card-thumb'; thumb.alt = file.name;
thumb.src = dataUrl; thumb.draggable = false;
thumb.alt = file.name;
thumb.draggable = false;
} else {
thumb = document.createElement('div');
thumb.className = 'card-thumb-placeholder';
thumb.textContent = '🖼️';
}
const body = document.createElement('div'); const body = document.createElement('div');
body.className = 'card-body'; body.className = 'card-body';
@@ -208,7 +210,7 @@ async function doRename() {
state.files.length = 0; state.files.length = 0;
state.files.push(...newList); state.files.push(...newList);
state.selectedIdxs.clear(); state.selectedIdxs.clear();
await renderGrid(); renderGrid();
} catch (e) { } catch (e) {
hideLoading(); hideLoading();
alert('오류 발생: ' + e.message); alert('오류 발생: ' + e.message);
+10
View File
@@ -165,6 +165,16 @@ body {
background: #08080f; background: #08080f;
display: block; display: block;
} }
/* 이미지 로딩 전 shimmer */
.card-thumb:not([src]) {
background: linear-gradient(90deg, #08080f 25%, #14141f 50%, #08080f 75%);
background-size: 200% 100%;
animation: thumb-shimmer 1.4s ease-in-out infinite;
}
@keyframes thumb-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.card-thumb-placeholder { .card-thumb-placeholder {
width: 100%; width: 100%;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;