From af721d327d0219aa2f2a543ea63caf347f578c89 Mon Sep 17 00:00:00 2001 From: zenn Date: Fri, 3 Jul 2026 14:52:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94:=20nginx=20gzip=C2=B7=EC=9D=B4=EB=AF=B8=EC=A7=80=20la?= =?UTF-8?q?zy-load=C2=B7=EC=BA=90=EC=8B=9C=20=EC=97=B0=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nginx 텍스트 자산 gzip 압축 추가로 css/js/json/HTML 전송량 절감 - cardList 카드 이미지 lazy-load로 시리즈 진입 시 즉시 다운로드 방지 - 정적 이미지 캐시 7일→30일 연장 - 미사용 style/deck-history.css 삭제 Co-authored-by: Cursor --- VERSION | 2 +- api/package.json | 2 +- cardList.html | 2 +- docs/changelog.md | 6 + docs/history.md | 5 + docs/spec.md | 4 +- docs/todo.md | 2 + docs/update.md | 12 + nginx/default.conf | 18 +- style/deck-history.css | 561 ----------------------------------------- 10 files changed, 47 insertions(+), 567 deletions(-) delete mode 100644 style/deck-history.css diff --git a/VERSION b/VERSION index 36c263a..d9b312d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v4.0.19 +v4.0.20 diff --git a/api/package.json b/api/package.json index 03c6f31..c65c840 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "uadeck-api", - "version": "4.0.19", + "version": "4.0.20", "type": "module", "main": "src/index.js", "scripts": { diff --git a/cardList.html b/cardList.html index b713ae0..7da8281 100644 --- a/cardList.html +++ b/cardList.html @@ -427,7 +427,7 @@ cardContainer.innerHTML = `
-
diff --git a/docs/changelog.md b/docs/changelog.md index 45e065b..c97ce84 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # 변경사항 +## v4.0.20 + +### 페이지 로딩 속도 개선 + +카드 데이터와 스타일 등 텍스트 자원을 압축 전송하도록 개선해 페이지가 더 빠르게 열립니다. 카드 목록 화면은 화면에 보이는 카드부터 순차적으로 이미지를 불러오도록 바뀌어, 시리즈를 처음 열 때 대기 시간이 줄어듭니다. 자주 보는 이미지의 캐시 유지 기간도 늘려 재방문 시 더 빠릅니다. + ## v4.0.19 ### 덱 목록 무한 스크롤 diff --git a/docs/history.md b/docs/history.md index 134cf78..6a849ca 100644 --- a/docs/history.md +++ b/docs/history.md @@ -1,5 +1,10 @@ # 의사결정 이력 +## 2026-07-03 v4.0.20 — 원본 이미지 변환 전 저비용 전송 최적화부터 적용 + +💡 1.8GB PNG의 WebP 일괄 변환은 파이프라인 부담이 커, 리스크가 낮고 효과가 큰 gzip·lazy-load·캐시부터 선적용하기 위함. +🔧 nginx gzip(텍스트 한정) + `cardList` 이미지 lazy-load + 이미지 캐시 30d로 연장, 원본 WebP 변환은 후속 과제로 분리. + ## 2026-07-02 v4.0.19 — 덱 목록은 무한 스크롤 + WebP 썸네일 이원화 💡 페이지네이션 대신 무한 스크롤로 초기 DOM 부담을 줄이되, 원본 1200px PNG를 목록마다 내려받는 대역폭 낭비를 없애기 위함. diff --git a/docs/spec.md b/docs/spec.md index 23438f3..294207a 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -3,7 +3,7 @@ ## 프로젝트 개요 - 서비스명: `UNIARE - Union Arena Deck Builder` -- 현재 버전: `v4.0.19` +- 현재 버전: `v4.0.20` - 프론트: 정적 HTML, CSS, JavaScript - 백엔드: Express, SQLite(`better-sqlite3`) - 운영: Docker Compose 기반 Nginx + API @@ -77,6 +77,8 @@ - 내 덱·공개 덱 목록은 클라이언트 무한 스크롤(청크 24개)로 렌더링한다. - 덱 삭제·미참조 정리 시 해당 공유 코드의 원본 PNG와 썸네일 WebP를 함께 삭제한다. - 관리자는 DB에서 참조하지 않는 미리보기 파일을 조회하고 일괄 삭제할 수 있다. +- nginx는 텍스트 자산(css/js/json/svg 등)을 gzip으로 압축 전송하고, 정적 이미지는 30일 immutable 캐시로 제공한다. +- 카드 목록 화면의 카드 이미지는 `loading="lazy"`로 지연 로드한다. ## 브라우저 상태 diff --git a/docs/todo.md b/docs/todo.md index 8a07cd2..f3ffe44 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -18,6 +18,8 @@ ## 유지보수 +- 카드 원본 이미지(약 1.8GB PNG)를 WebP로 일괄 변환하고 참조 경로·폴백을 정비해 대역폭을 절감한다. +- 유난히 큰 개별 이미지(예: `ua44/…EVA-1-079_p2.png` 1.2MB, `ua14/UAPR_DST-1-039.png` 1.0MB, `logo_unionarena.png` 431KB)를 재압축한다. - `cardList.html`, `result.html`의 대형 인라인 스크립트를 기능 단위 모듈로 분리한다. - `datas/ua99 sample.js`, `datas/ua99 sample mini.js`의 보존 목적과 배포 제외 여부를 확정한다. diff --git a/docs/update.md b/docs/update.md index 2dec5d2..6db82b2 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,17 @@ # 업데이트 이력 +## 2026-07-03 / v4.0.20 — 전송 최적화(gzip·lazy-load·캐시) + +### 수행 작업 +- nginx에 텍스트 자산 gzip 압축 추가(css/js/json/svg 등, `gzip_min_length 1024`, `comp_level 6`) — output.css·카드 데이터·HTML 전송량 약 70~80% 절감 +- `cardList.html` 카드 그리드 이미지에 `loading="lazy" decoding="async"` 추가 — 시리즈 진입 시 전체(~90장) 즉시 다운로드 방지 +- 정적 이미지 캐시 기간 `expires 7d → 30d`로 연장(내용 고정, immutable 유지) +- 미사용 `style/deck-history.css`(10.8KB) 삭제 (현재 어떤 화면도 참조하지 않음) + +### 검증 +- nginx 설정은 문법상 표준 디렉티브만 사용(배포 시 `nginx -t` 권장) +- `deck-history.css` 참조가 HTML/CSS/Dockerfile에 없음을 확인 후 삭제 + ## 2026-07-02 / v4.0.19 — 덱 목록 무한 스크롤·WebP 썸네일 ### 수행 작업 diff --git a/nginx/default.conf b/nginx/default.conf index dcbf81a..c8768ac 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -7,6 +7,20 @@ server { client_max_body_size 25m; + # 텍스트 자산 gzip 압축 (이미지는 제외) + gzip on; + gzip_vary on; + gzip_comp_level 6; + gzip_min_length 1024; + gzip_proxied any; + gzip_types + text/plain + text/css + application/javascript + application/json + image/svg+xml + application/manifest+json; + # 공유 링크 → 덱 로드 페이지 location ~ ^/d/([A-Za-z0-9]+)$ { return 302 /cardList.html?d=$1; @@ -59,9 +73,9 @@ server { try_files $uri =404; } - # 이미지 등 + # 이미지 등 — 내용이 고정된 정적 자산이므로 장기 캐시 location ~* \.(png|jpg|jpeg|gif|webp|ico|svg|woff2?)$ { - expires 7d; + expires 30d; add_header Cache-Control "public, immutable"; try_files $uri =404; } diff --git a/style/deck-history.css b/style/deck-history.css deleted file mode 100644 index 3991488..0000000 --- a/style/deck-history.css +++ /dev/null @@ -1,561 +0,0 @@ -/** - * 내 덱 관리·공개 덱 페이지 (deckHistory.html, preset.html) - */ - -.deck-history-page { - /* 배경·폰트는 .app-page */ -} - -.deck-history-main { - width: 100%; -} - -.deck-history-header { - margin-bottom: 2rem; -} - -.deck-history-header h1 { - font-size: 1.75rem; - font-weight: 900; - color: #0f172a; - letter-spacing: -0.02em; -} - -.deck-history-header .page-title-row { - margin-bottom: 0; -} - -.deck-history-toolbar { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1.5rem; - padding: 1rem 1.25rem; - background: #fff; - border: 1px solid #e2e8f0; - border-radius: 1rem; - box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); -} - -.deck-history-toolbar.hidden { - display: none; -} - -.deck-toolbar-fav-row { - display: flex; - width: 100%; - padding-bottom: 0.75rem; - border-bottom: 1px solid #f1f5f9; -} - -.deck-toolbar-main-row { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - align-items: center; - width: 100%; -} - -.deck-toolbar-main-row .deck-search-input { - flex: 1 1 12rem; - min-width: 0; -} - -.deck-toolbar-main-row .deck-series-select { - flex: 0 1 11rem; - min-width: 9rem; -} - -.deck-toolbar-main-row .deck-count-label { - margin-left: auto; -} - -.deck-search-input { - border: 1px solid #e2e8f0; - border-radius: 0.75rem; - padding: 0.625rem 1rem; - font-size: 0.875rem; - color: #0f172a; - background: #f8fafc; - outline: none; - transition: border-color 0.2s, box-shadow 0.2s; -} - -.deck-search-input:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); - background: #fff; -} - -.deck-search-input::placeholder { - color: #94a3b8; -} - -.deck-series-select { - border: 1px solid #e2e8f0; - border-radius: 0.75rem; - padding: 0.625rem 2rem 0.625rem 1rem; - font-size: 0.875rem; - color: #0f172a; - background: #f8fafc url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M3 4.5L6 7.5L9 4.5'/%3E%3C/svg%3E") no-repeat right 0.75rem center; - appearance: none; - cursor: pointer; - outline: none; -} - -.deck-series-select:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); - background-color: #fff; -} - -.deck-count-label { - flex: 0 0 auto; - font-size: 0.75rem; - font-weight: 600; - color: #94a3b8; - white-space: nowrap; -} - -.deck-favorite-filter { - display: inline-flex; - align-items: center; - gap: 0.4rem; - width: 100%; - padding: 0.625rem 1rem; - border: 1px solid #fde68a; - border-radius: 0.75rem; - font-size: 0.8125rem; - font-weight: 700; - color: #92400e; - background: #fffbeb; - cursor: pointer; - transition: background 0.2s, border-color 0.2s, box-shadow 0.2s; -} - -.deck-favorite-filter:hover { - background: #fef3c7; - border-color: #fcd34d; -} - -.deck-favorite-filter[aria-pressed='true'] { - color: #78350f; - background: #fef08a; - border-color: #f59e0b; - box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.35); -} - -.deck-favorite-filter-icon { - font-size: 1.125rem; - line-height: 1; - color: #f59e0b; -} - -.deck-favorite-filter .deck-star-icon { - color: #fbbf24; -} - -.deck-favorite-filter .deck-star-icon.is-on { - color: #d97706; -} - -.deck-fav-count { - margin-left: auto; - font-size: 0.75rem; - font-weight: 800; - color: #b45309; -} - -.deck-star-icon { - display: inline-block; - font-size: 1rem; - line-height: 1; - color: #cbd5e1; -} - -.deck-star-icon::before { - content: '☆'; -} - -.deck-star-icon.is-on { - color: #f59e0b; -} - -.deck-star-icon.is-on::before { - content: '★'; -} - -.deck-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); - gap: 1.25rem; -} - -.deck-grid-empty { - grid-column: 1 / -1; - text-align: center; - padding: 3rem 1rem; - font-size: 0.875rem; - color: #94a3b8; -} - -.deck-card { - display: flex; - flex-direction: column; - background: #fff; - border: 1px solid #e2e8f0; - border-radius: 1rem; - overflow: visible; - box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); - transition: box-shadow 0.2s, transform 0.2s, border-color 0.2s; -} - -.deck-card:hover { - border-color: #cbd5e1; - box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08); - transform: translateY(-2px); -} - -.deck-card.is-favorite { - border-color: #fcd34d; - box-shadow: 0 2px 12px rgba(245, 158, 11, 0.12); -} - -.deck-card-preview { - position: relative; - aspect-ratio: 16 / 9; - background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); - border-bottom: 1px solid #e2e8f0; - overflow: hidden; - border-radius: 1rem 1rem 0 0; -} - -.deck-card-preview img { - width: 100%; - height: 100%; - object-fit: contain; - object-position: center; - background-color: #ffffff; -} - -.deck-card-preview--holdable { - cursor: zoom-in; - user-select: none; - -webkit-user-select: none; - touch-action: manipulation; -} - -.deck-card-preview--holdable:active { - opacity: 0.92; -} - -.deck-card-preview-placeholder { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - font-size: 0.75rem; - font-weight: 600; - color: #94a3b8; - letter-spacing: 0.05em; - text-transform: uppercase; - text-align: center; - line-height: 1.4; - padding: 0.5rem; -} - -.deck-card-preview-placeholder span { - display: block; - margin-top: 0.25rem; - font-size: 0.625rem; - font-weight: 500; - text-transform: none; - letter-spacing: 0; - color: #cbd5e1; -} - -.deck-card-body { - display: flex; - flex-direction: column; - flex: 1; - padding: 1rem 1.125rem 1.125rem; - gap: 0.5rem; -} - -.deck-card-title { - font-size: 0.9375rem; - font-weight: 700; - color: #0f172a; - line-height: 1.4; - margin: 0; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.deck-card-meta { - font-size: 0.75rem; - color: #94a3b8; - line-height: 1.4; -} - -.deck-card-series { - display: inline-block; - font-size: 0.6875rem; - font-weight: 700; - color: #2563eb; - background: #eff6ff; - padding: 0.2rem 0.5rem; - border-radius: 0.375rem; - margin-bottom: 0.125rem; -} - -.deck-card-actions { - display: flex; - gap: 0.5rem; - margin-top: auto; - padding-top: 0.5rem; - align-items: center; -} - -.deck-card-actions--primary .deck-card-btn-load { - flex: 1.4; -} - -.deck-card-more-wrap { - position: relative; - flex: 0 0 auto; -} - -.deck-card-more-btn { - display: inline-flex; - align-items: center; - justify-content: center; - width: 2.25rem; - height: 2.25rem; - padding: 0; - border: 1px solid #e2e8f0; - border-radius: 0.625rem; - background: #f8fafc; - color: #64748b; - cursor: pointer; - transition: background 0.2s, border-color 0.2s, color 0.2s; -} - -.deck-card-more-btn:hover, -.deck-card-more-btn[aria-expanded='true'] { - background: #fff; - border-color: #cbd5e1; - color: #334155; -} - -.deck-card-more-menu { - position: absolute; - right: 0; - top: calc(100% + 0.35rem); - z-index: 20; - min-width: 9.5rem; - padding: 0.35rem; - background: #fff; - border: 1px solid #e2e8f0; - border-radius: 0.75rem; - box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12); -} - -.deck-card-more-menu.hidden { - display: none; -} - -.deck-card-more-item { - display: block; - width: 100%; - padding: 0.55rem 0.75rem; - border: none; - border-radius: 0.5rem; - background: transparent; - color: #334155; - font-size: 0.75rem; - font-weight: 600; - text-align: left; - cursor: pointer; - white-space: nowrap; - transition: background 0.15s; -} - -.deck-card-more-item:hover { - background: #f1f5f9; -} - -.deck-card-more-item--danger { - color: #dc2626; -} - -.deck-card-more-item--danger:hover { - background: #fef2f2; -} - -.deck-card-btn { - flex: 1; - padding: 0.5rem 0.375rem; - border: none; - border-radius: 0.625rem; - font-size: 0.6875rem; - font-weight: 700; - cursor: pointer; - transition: background 0.2s, transform 0.1s; - white-space: nowrap; -} - -.deck-card-btn:active { - transform: scale(0.97); -} - -.deck-card-btn-load { - background: #2563eb; - color: #fff; -} - -.deck-card-btn-load:hover { - background: #1d4ed8; -} - -.deck-card-btn-favorite { - background: #fffbeb; - color: #92400e; - border: 1px solid #fde68a; -} - -.deck-card-btn-favorite:hover { - background: #fef3c7; - border-color: #fcd34d; -} - -.deck-card-btn-favorite.is-on { - background: #fef08a; - color: #78350f; - border-color: #f59e0b; -} - -.deck-card-btn-favorite.is-on:hover { - background: #fde047; -} - -.deck-card-btn-favorite .deck-star-icon { - margin-right: 0.15rem; -} - -.deck-card-btn-copy { - background: #f1f5f9; - color: #475569; -} - -.deck-card-btn-copy:hover { - background: #e2e8f0; -} - -.deck-card-btn-delete { - flex: 0 0 auto; - padding-left: 0.625rem; - padding-right: 0.625rem; - background: #fef2f2; - color: #dc2626; -} - -.deck-card-btn-delete:hover { - background: #fee2e2; -} - -.deck-card-visibility { - display: flex; - align-items: center; - gap: 0.375rem; - margin-bottom: 0.125rem; - flex-wrap: wrap; -} - -.deck-card-badge-public { - font-size: 0.625rem; - font-weight: 700; - color: #059669; - background: #ecfdf5; - padding: 0.15rem 0.45rem; - border-radius: 0.375rem; -} - -.deck-card-badge-private { - font-size: 0.625rem; - font-weight: 700; - color: #64748b; - background: #f1f5f9; - padding: 0.15rem 0.45rem; - border-radius: 0.375rem; -} - -.deck-card-btn-toggle { - flex: 1; - background: #f0fdf4; - color: #047857; -} - -.deck-card-btn-toggle.is-public { - background: #f8fafc; - color: #64748b; -} - -.deck-card-btn-toggle:hover { - background: #dcfce7; -} - -.deck-card-btn-toggle.is-public:hover { - background: #e2e8f0; -} - -.deck-status-message { - text-align: center; - padding: 3rem 1rem; - font-size: 0.875rem; - color: #94a3b8; -} - -.deck-status-message.error { - color: #ef4444; -} - -.deck-preview-hold-overlay { - position: fixed; - inset: 0; - z-index: 200; - display: flex; - align-items: center; - justify-content: center; - padding: 1.5rem; - background: rgba(15, 23, 42, 0.72); - backdrop-filter: blur(4px); - cursor: zoom-out; -} - -.deck-preview-hold-overlay.hidden { - display: none; -} - -.deck-preview-hold-box { - max-width: min(92vw, 56rem); - max-height: min(88vh, 40rem); - background: #fff; - border-radius: 1rem; - padding: 0.75rem; - box-shadow: 0 24px 48px rgba(0, 0, 0, 0.25); -} - -.deck-preview-hold-box img { - display: block; - max-width: 100%; - max-height: min(84vh, 38rem); - width: auto; - height: auto; - margin: 0 auto; - object-fit: contain; -}