이미지 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);
_showLoader(card);
_timer = setTimeout(() => {
_timer = setTimeout(async () => {
_removeLoader();
const thumb = card.querySelector('img.card-thumb');
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;
_show(card);
}, 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 hoverPreview from './handlers/hover-preview.js';
let _cardObserver = null;
// ── DOM ──────────────────────────────────────────────
const grid = document.getElementById('grid');
const emptyState = document.getElementById('empty-state');
@@ -70,40 +72,47 @@ async function openFolder() {
state.files.push(...list);
state.selectedIdxs.clear();
setStatus(`${state.files.length}개 파일 로드됨`, true);
await renderGrid();
renderGrid();
updatePreview();
btnRename.disabled = false;
btnReveal.disabled = false;
}
// ── 그리드 렌더링 ────────────────────────────────
async function renderGrid() {
function renderGrid() {
if (_cardObserver) { _cardObserver.disconnect(); _cardObserver = null; }
grid.innerHTML = '';
emptyState.style.display = 'none';
grid.style.display = 'grid';
fileCount.textContent = `${state.files.length}개 파일`;
updateSelCount();
showLoading(`썸네일 로딩 중... (0/${state.files.length})`);
// 카드 껍데기만 즉시 생성 (이미지 없음)
state.files.forEach((file, idx) => grid.appendChild(createCard(file, idx)));
const BATCH = 10;
for (let i = 0; i < state.files.length; i += BATCH) {
const batch = state.files.slice(i, i + BATCH);
const results = await Promise.all(batch.map(f => window.api.readImage(f.path)));
results.forEach((dataUrl, bi) => {
grid.appendChild(createCard(state.files[i + bi], i + bi, dataUrl));
// 뷰포트 ±400px 범위에 들어온 카드만 이미지 로딩
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (!entry.isIntersecting) return;
observer.unobserve(entry.target);
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();
updateNumberBadges();
updateNewNameLabels();
}
// ── 카드 생성 ────────────────────────────────────
function createCard(file, idx, dataUrl) {
function createCard(file, idx) {
const card = document.createElement('div');
card.className = 'card';
card.dataset.idx = idx;
@@ -118,18 +127,11 @@ function createCard(file, idx, dataUrl) {
checkBadge.className = 'card-check';
checkBadge.textContent = '✓';
let thumb;
if (dataUrl) {
thumb = document.createElement('img');
thumb.className = 'card-thumb';
thumb.src = dataUrl;
thumb.alt = file.name;
thumb.draggable = false;
} else {
thumb = document.createElement('div');
thumb.className = 'card-thumb-placeholder';
thumb.textContent = '🖼️';
}
// src 없이 생성 — IntersectionObserver가 뷰포트 진입 시 로딩
const thumb = document.createElement('img');
thumb.className = 'card-thumb';
thumb.alt = file.name;
thumb.draggable = false;
const body = document.createElement('div');
body.className = 'card-body';
@@ -208,7 +210,7 @@ async function doRename() {
state.files.length = 0;
state.files.push(...newList);
state.selectedIdxs.clear();
await renderGrid();
renderGrid();
} catch (e) {
hideLoading();
alert('오류 발생: ' + e.message);
+10
View File
@@ -165,6 +165,16 @@ body {
background: #08080f;
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 {
width: 100%;
aspect-ratio: 1 / 1;