From b797535b178c2c0c2c464e0ba064eafc9c9ddcbf Mon Sep 17 00:00:00 2001 From: Mina Choi Date: Mon, 7 Sep 2026 13:33:34 +0900 Subject: [PATCH] =?UTF-8?q?Revert=20"[fix]=20solution/site:=20=EC=98=9B=20?= =?UTF-8?q?=ED=95=B4=EC=8B=9C=20=EC=9E=90=EC=82=B0=EC=9D=84=2030=EC=9D=BC?= =?UTF-8?q?=20=EB=82=A8=EA=B8=B4=EB=8B=A4=20=E2=80=94=20=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=EC=99=80=20=EC=A0=84=EC=B2=B4=20=EC=9E=AC=EA=B5=BD=EA=B8=B0?= =?UTF-8?q?=EB=A5=BC=20=EB=97=80=EB=8B=A4"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8f6ea16f6595107636a5c4366e569cbcee9ce227. --- AGENTS.md | 9 +-- docs/DEPLOY.md | 20 ++---- docs/DEVLOG.md | 35 ---------- solution/site/scripts/prerender.ts | 103 +---------------------------- 4 files changed, 11 insertions(+), 156 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 784c678..5652113 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,12 +28,9 @@ - **번들 파일명은 콘텐츠 해시다.** HTML 은 `/assets/index-DvNTmLhy.css` 를 **루트 절대경로**로 가리킨다. 경로는 프리렌더가 `dist/client/.vite/manifest.json` 에서 읽어 박는다 (`prerender.ts:160`). 렌더러 CSS 를 고치면 이름이 바뀐다. -- **옛 해시 자산은 30일 남는다** (`prerender.ts` `ASSET_RETENTION_DAYS`). 예전에는 빌드마다 - `out/assets` 를 통째로 갈아서, 새 번들로 일부만 구우면 나머지 사이트가 CSS 404 였다. - 지금은 남긴다 — 구글은 HTML 을 가져간 뒤 렌더를 **나중에** 돌리므로, 그 사이 자산이 사라지면 - 스타일 없는 페이지를 렌더한 것으로 기록된다. 보관 근거는 `out/assets/.builds.json` 대장이고 - 파일 mtime 이 아니다. - → 재굽기는 여전히 필요하지만 **급하지 않다**. 디자인이 반영 안 될 뿐, 깨지지는 않는다. +- **로컬 `out/assets` 는 빌드마다 통째로 갈린다** (`prerender.ts:573` `rmSync`). 옛 해시 파일이 + 사라지므로 새 번들로 일부 사이트만 구우면 나머지는 CSS 가 404 다. + → 프리렌더 기동 시 전체 재굽기가 이 구멍을 메운다. - **★ 프론트(`solution/site`)를 배포하면 반드시 전체 재굽기 + 전체 재업로드.** `azure_static.publish(slug)` 는 공용 자산 + `s/` 만 올린다 — **렌더러를 고쳐도 다른 사이트에는 반영되지 않는다.** diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 9a27c1c..2490f3b 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -66,23 +66,13 @@ site-out/ ``` 프론트 수정 → 새 번들(새 해시) - 로컬 out/assets : 새 해시 추가, 옛 해시 30일 보관 ← 안 깨진다. 옛 디자인으로 뜰 뿐 - Azure : 새 해시 추가, 옛 해시 유지 ← 같다 + 로컬 out/assets : 통째로 교체 (옛 해시 삭제) ← 재굽기 안 한 사이트는 CSS 404 + Azure : 새 해시 추가, 옛 해시 유지 ← 안 깨지지만 옛 디자인 그대로 박제 ``` -**2026-09-07 이전에는 로컬 `out/assets` 를 통째로 갈았다.** 그래서 재굽기 전까지 나머지 사이트가 -CSS 404 였다 — 하필 크롤러가 그 순간 렌더하면 스타일 없는 페이지를 본 것으로 기록된다. -지금은 `ASSET_RETENTION_DAYS`(30일) 동안 옛 해시를 남긴다. 보관 근거는 `out/assets/.builds.json` -대장이다(파일 mtime 이 아니다 — 복사·동기화가 시각을 갈아 버린다). - -**그래서 재굽기는 여전히 필요하지만 급하지는 않다.** 안 하면 그 사이트만 옛 디자인으로 뜬다. -프리렌더 컨테이너는 **기동할 때 payload 전체를 다시 굽는다.** 하지만 Azure 는 발행 잡이 도는 -사이트 하나씩만 올린다 — 그 짝을 맞추는 게 `solution/backend/scripts/republish_all.py` 다. - -⚠️ 남은 것: `azure_static._upload_shared` 는 매 발행마다 `assets/` **전체**를 다시 올린다. -옛 해시를 남기기 시작했으므로 보관 기간만큼 업로드량이 는다. Azure 를 켤 때는 이미 있는 -블롭(해시 파일이라 이름이 같으면 내용도 같다)을 건너뛰도록 먼저 고친다. - +프리렌더 컨테이너는 **기동할 때 payload 전체를 다시 굽는다.** 그래서 로컬 out/ 은 재시작만 +하면 정합이 맞는다. 하지만 Azure 는 발행 잡이 도는 사이트 하나씩만 올린다 — 그 짝을 맞추는 게 +`solution/backend/scripts/republish_all.py` 다. **규칙: `solution/site` 를 배포하면 반드시 전체 재굽기 + 전체 재업로드.** diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 0d33e6c..f56c53f 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -5,41 +5,6 @@ --- -## 2026-09-07 — 옛 해시 자산을 30일 남긴다 — 배포와 재굽기를 뗀다 - -**왜** -`writeSharedAssets` 가 빌드마다 `out/assets` 를 통째로 지우고 다시 깔았다. HTML 은 자산 경로를 -파일명 해시까지 박아 굽기 때문에, 렌더러를 배포하는 순간 **아직 다시 굽지 않은 사이트는 전부 -CSS·JS 404** 였다. 구멍을 "기동 시 전체 재굽기" 와 "배포하면 반드시 전체 재업로드" 라는 **규칙** -으로 막고 있었다 — 규칙으로 막는다는 건 구조가 못 막는다는 뜻이다. - -진짜 위험은 방문자가 아니라 크롤러다. 구글은 HTML 을 가져간 뒤 렌더를 **나중에** 돌린다. -그 사이에 자산이 사라지면 스타일도 스크립트도 없는 페이지를 렌더한 것으로 기록한다 — -하필 지금이 신규 도메인이 평가받는 시기다. 유예 창이 필요하다는 건 업계 통념이고 -(Vercel 은 검색봇에 한해 스큐 보호 창을 60일로 늘린다), 우리 창은 0초였다. - -**바꾼 것** (`scripts/prerender.ts`) -- `assets/` 를 통째로 지우지 않는다. 권한 때문에 지웠던 것인데 `copyDirectoryFiles` 가 - **파일마다** 먼저 `rmSync` 하므로 그 문제는 그대로 해결된다 -- `ASSET_RETENTION_DAYS`(30일) · `ASSET_MIN_BUILDS`(2) — 기간이 지나도 직전 빌드는 남는다 -- `out/assets/.builds.json` 대장: 어떤 빌드가 어떤 파일을 깔았는지. **mtime 으로 나이를 재지 - 않는다** — 복사·동기화가 시각을 갈아 버리면 옛 파일이 영원히 젊어지거나 산 파일이 지워진다. - 발행마다 이 함수가 도므로, 번들이 그대로면 줄을 늘리지 않고 맨 앞 줄의 시각만 갱신한다 -- 점(.)으로 시작해 `azure_static` 의 dotfile 필터에 걸러진다 — 대장은 업로드되지 않는다 - -**얻은 것** — 프론트 배포와 전체 재굽기가 **분리된다.** 재굽기를 안 하면 그 사이트만 옛 -디자인으로 뜬다(예전엔 깨졌다). AGENTS.md 의 ★규칙은 남지만 이유가 "안 하면 죽는다" 에서 -"안 하면 반영이 안 된다" 로 내려온다. - -**남은 것** — `azure_static._upload_shared` 가 매 발행마다 `assets/` 전체를 올린다. 보관 기간만큼 -업로드량이 는다. Azure 는 지금 꺼져 있으므로(DEPLOY.md) 켤 때 이미 있는 블롭을 건너뛰도록 고친다. - -**검증** — 실제로 세 번 구워 확인: 번들 해시가 바뀌어도 옛 파일 3개가 그대로 남고, 같은 번들로 -다시 구우면 대장이 늘지 않으며(2줄 유지), 대장의 마지막 줄을 60일 전으로 돌리자 그 빌드의 -파일 3개만 정리됐다. `tsc·eslint` 통과, `vitest` 22 passed. - ---- - ## 2026-09-07 — 사이트맵 lastmod 를 파일 mtime 에서 뗐다 **왜** diff --git a/solution/site/scripts/prerender.ts b/solution/site/scripts/prerender.ts index 22941c6..b6395e9 100644 --- a/solution/site/scripts/prerender.ts +++ b/solution/site/scripts/prerender.ts @@ -479,115 +479,18 @@ function writeReport(payloadFile: string, report: RenderReport) { renameSync(tmp, target); } -/** - * 옛 자산 보관 기간. - * - * ★ 왜 지우지 않고 남기나 — HTML 은 자산 경로를 **파일명 해시까지 박아** 굽는다 - * (`/assets/index-DvNTmLhy.css`). 렌더러를 배포하면 이름이 바뀌는데, 그 순간 옛 파일을 - * 지우면 아직 다시 굽지 않은 사이트는 CSS·JS 가 **404** 다. 예전 구현이 그랬다. - * - * ★ 더 나쁜 건 크롤러다. 구글은 HTML 을 가져간 뒤 렌더를 **나중에** 돌린다. 그 사이에 - * 자산이 사라지면 스타일도 스크립트도 없는 페이지를 렌더한 것으로 기록한다 — 하필 - * 신규 도메인이 평가받는 시기에 그렇게 된다. 유예 창이 필요하다는 게 업계 통념이고 - * (Vercel 은 검색봇에 한해 스큐 보호 창을 60일로 늘린다), 우리 창은 0초였다. - * - * 한 벌이 400KB 안팎이라 한 달치를 남겨도 10MB 남짓이다 — 싸게 사는 안전이다. - */ -const ASSET_RETENTION_DAYS = 30; -/** 하루에 여러 번 배포해도 직전 빌드는 반드시 남는다(보관 기간과 무관). */ -const ASSET_MIN_BUILDS = 2; -/** - * 어떤 빌드가 어떤 파일을 깔았는지. **보관 기간의 근거는 이 파일이다.** - * - * ★ 파일 mtime 으로 나이를 재지 않는다 — 복사·동기화가 시각을 갈아 버리면 옛 파일이 - * 영원히 젊어지거나 산 파일이 지워진다. 점(.)으로 시작해 업로드에서 빠진다 - * (azure_static 이 dotfile 을 거른다). - */ -const ASSET_LEDGER = '.builds.json'; - -/** 자산 대장 한 줄 — 한 번의 번들 빌드가 깐 파일 목록. */ -interface AssetBuild { - /** 이 번들을 마지막으로 깐 시각(ISO). 같은 번들로 다시 구우면 갱신된다. */ - at: string; - /** assets/ 기준 상대 경로. */ - files: string[]; -} - -/** 디렉토리 안의 파일을 상대 경로로 편다(하위 디렉토리 포함). */ -function listRelativeFiles(dir: string, prefix = ''): string[] { - const found: string[] = []; - for (const entry of readdirSync(dir, {withFileTypes: true})) { - const rel = prefix ? `${prefix}/${entry.name}` : entry.name; - if (entry.isDirectory()) found.push(...listRelativeFiles(join(dir, entry.name), rel)); - else if (entry.isFile()) found.push(rel); - } - return found; -} - -function readAssetLedger(assetsDir: string): AssetBuild[] { - try { - const parsed = JSON.parse(readFileSync(join(assetsDir, ASSET_LEDGER), 'utf-8')) as { - builds?: AssetBuild[]; - }; - return Array.isArray(parsed.builds) ? parsed.builds : []; - } catch { - // 없거나 깨졌으면 이번 빌드부터 다시 센다. 옛 파일은 그대로 남는다 — - // 대장을 못 읽었다고 지우는 쪽으로 실패하면 사이트가 깨진다. - return []; - } -} - -/** - * 보관 기간이 지난 옛 해시 파일만 지운다. - * - * ★ 발행할 때마다 이 함수가 돈다(사이트 하나만 구울 때도). 번들이 그대로면 대장에 줄이 - * 늘지 않고 맨 앞 줄의 시각만 갱신된다 — 안 그러면 발행 횟수만큼 대장이 자란다. - */ -function pruneAssets(assetsDir: string, current: string[]) { - const signature = (files: string[]) => [...files].sort().join('\n'); - const previous = readAssetLedger(assetsDir); - const head: AssetBuild = {at: new Date().toISOString(), files: current}; - const builds = - previous[0] && signature(previous[0].files) === signature(current) - ? [head, ...previous.slice(1)] - : [head, ...previous]; - - const cutoff = Date.now() - ASSET_RETENTION_DAYS * 24 * 60 * 60 * 1000; - const kept = builds.filter( - (build, index) => index < ASSET_MIN_BUILDS || Date.parse(build.at) >= cutoff, - ); - const alive = new Set(kept.flatMap((build) => build.files)); - - let removed = 0; - for (const file of listRelativeFiles(assetsDir)) { - if (file === ASSET_LEDGER || alive.has(file)) continue; - rmSync(join(assetsDir, file), {force: true}); - removed += 1; - } - - writeFileSync( - join(assetsDir, ASSET_LEDGER), - JSON.stringify({schemaVersion: 1, builds: kept}, null, 2), - 'utf-8', - ); - if (removed > 0) { - console.log(` ✓ 보관 기간(${ASSET_RETENTION_DAYS}일)이 지난 자산 ${removed}개 정리`); - } -} - /** * 클라이언트 번들과 public/ 을 대상 디렉토리에 깐다. * - * ★ assets/ 를 통째로 지우고 다시 깔지 않는다(예전 구현). 옛 해시 파일은 보관 기간까지 - * 남겨야 한다 — 근거는 ASSET_RETENTION_DAYS 주석. 권한 때문에 통째로 지웠던 것인데, - * copyDirectoryFiles 가 **파일마다** 먼저 rmSync 하므로 그 문제는 그대로 해결된다. + * Docker bind mount 에서 이전 빌드가 남긴 파일 권한이 호스트와 어긋날 수 있어 + * assets/ 는 통째로 교체한다(낡은 해시 파일과 권한을 함께 제거). */ function writeSharedAssets(destRoot: string) { const assetsSrc = join(CLIENT_DIR, 'assets'); if (existsSync(assetsSrc)) { const assetsDest = join(destRoot, 'assets'); + rmSync(assetsDest, {recursive: true, force: true}); copyDirectoryFiles(assetsSrc, assetsDest); - pruneAssets(assetsDest, listRelativeFiles(assetsSrc)); } const publicDir = join(SITE_ROOT, 'public'); if (existsSync(publicDir)) {