1. 시작하며: 포스트 관리의 확장성과 서버리스 호출 최적화 과제
SvelteKit Blog Engine(SKBE)은 다국어(한국어, 영어, 일본어) 지원과 다양한 미디어 형식을 아우르는 독립형 블로그 솔루션입니다. 블로그에 등록되는 글의 종류와 수가 늘어남에 따라, 어드민 포스트 관리 화면(apps/admin/src/routes/posts)에서 다음 두 가지 구현 과제가 발생했습니다:
- 포스트 분류 체계의 세분화: 작성일 순 정렬이나 단순 언어별 분기를 넘어, 특정 카테고리별 글, 카테고리가 없는 독립 정적 페이지(Static Pages), 발행 전 상태인 초안(Drafts), 그리고 카테고리가 누락된 미분류 글을 명확히 구분하여 조회할 수 있는 필터링 체계가 필요했습니다.
- Cloudflare Serverless 인프라 호출 최소화: 어드민 내부에서 대시보드, 포스트 관리, 설정, 미디어 메뉴 등을 이동할 때마다 SvelteKit의 SPA 네비게이션이 서버
load함수(__data.json)를 호출하여 Cloudflare Workers 실행 횟수와 D1 읽기 쿼리가 누적되는 문제를 개선해야 했습니다.
이번 SKBE v1.0.1.2 업데이트에서는 언어와 카테고리를 직교 결합한 2단 필터링 구조를 도입하고, 브라우저 HTTP 캐시와 로컬 스토리지를 활용한 스마트 캐시 무효화 파이프라인을 구축하여 불필요한 서버리스 호출을 최소화했습니다.
2. 2단 직교형 다중 필터링 아키텍처: 언어 1차 축 × 분류 2차 축
1) 구조적 설계: 2단 툴바 구성
기존 어드민 화면은 언어별 탭만 단일로 존재하여 특정 카테고리의 글만 모아보거나 초안 상태의 글을 찾기 위해 여러 페이지를 넘겨야 했습니다.
v1.0.1.2에서는 상단에 1단 언어 축과 2단 카테고리/특수분류 축을 결합한 2차원 필터링 툴바를 구성했습니다:
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ [1단: 언어 탭] │
│ [전체 (35)] [KR 한국어 (20)] [EN English (10)] [JA 日本語 (5)] │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ [2단: 카테고리 & 특수 분류 탭] │
│ [전체 (35)] │ [📁 개발 (12)] [📁 일상 (6)] │ [📄 정적 페이지 (4)] [📝 초안 (3)] [📂 미분류 (8)]│
└────────────────────────────────────────────────────────────────────────────────────────┘

2) 특수 분류(Special Classifications) 분리 관리
단순 카테고리 슬러그 매칭 외에 운영상 구분이 필요한 3가지 특수 상태를 독립 필터 탭으로 분리했습니다:
📄 정적 페이지(type === 'page'): 소개, 이용약관, 개인정보처리방침 등 일반 포스트 목록 흐름과 분리된 독립 페이지 필터링.📝 초안/임시저장(status === 'draft'): 아직 발행되지 않은 미완성 글만 모아서 검수 및 수정 지원.📂 미분류(type !== 'page' && !category_slug): 카테고리가 지정되지 않은 일반 글을 추출하여 분류 정리 지원.
3) Svelte 5 Runes 기반 실시간 반응형 동기화
1단에서 언어를 변경하면(예: KR ➔ EN), 영문 포스트들이 실제로 사용 중인 카테고리 목록과 각 카테고리의 글 개수, 그리고 영문 정적페이지/초안 개수가 $derived.by를 통해 메모리 상에서 실시간으로 재계산됩니다:
// [apps/admin/src/routes/posts/+page.svelte]
// 현재 언어 기준 특수 분류(초안, 정적페이지, 미분류) 실시간 집계
let specialCounts = $derived.by(() => {
let draft = 0, page = 0, uncategorized = 0;
postsByLang.forEach((p: any) => {
if (p.status === 'draft') draft++;
if (p.type === 'page') page++;
if (p.type !== 'page' && (!p.category_slug || p.category_slug.trim() === '')) {
uncategorized++;
}
});
return { draft, page, uncategorized };
});
4) 사이드 이펙트 방지: 고유 번호 보존 & 페이지네이션 안전망
- 포스트 고유 번호(
#1-kr) 보존: 글 고유 번호 매핑 로직(postNumberMap)은 필터링된 배열이 아닌 **전체 원본 데이터(cachedPosts)**를 기준으로 계산되도록 유지하여, 필터 적용 시 글 고유 번호가 재할당되는 번호 불일치 문제를 방지했습니다. - 페이지네이션 Out of Bounds 방어: 글이 1개뿐인 카테고리를 선택했을 때 이전 페이지 번호가 남아 빈 화면이 노출되지 않도록, 필터 전환 시 즉시
currentPage = 1로 초기화하고$effect로 총 페이지 범위를 자동 보정하도록 처리했습니다.
3. Cloudflare Workers 호출을 최소화하는 브라우저 캐싱 레이어
1) 서버리스 환경의 비용 및 지연 문제
SvelteKit의 SPA 네비게이션은 메뉴를 이동할 때마다 서버 load 함수를 재실행하기 위해 __data.json 요청을 보냅니다. 관리자가 여러 관리자 화면을 오갈 때마다 Cloudflare Workers가 실행되고 D1 쿼리가 발생하므로, 불필요한 읽기 연산이 누적되는 구조였습니다.
2) 브라우저 HTTP 캐시 (Cache-Control) 적용
이를 방지하기 위해 +page.server.ts에서 브라우저 캐시 헤더를 적용했습니다:
// [apps/admin/src/routes/posts/+page.server.ts]
export const load: PageServerLoad = async ({ locals, setHeaders, url }) => {
const isRefreshed = url.searchParams.has('refreshed');
if (!isRefreshed) {
// 일반 조회 시 브라우저 캐시 적용 -> Workers 호출 차단
setHeaders({
'cache-control': 'private, max-age=86400, stale-while-revalidate=3600'
});
} else {
// 강제 갱신 요청 시 서버에서 직접 D1 최신 데이터 조회
setHeaders({
'cache-control': 'no-store, no-cache, must-revalidate'
});
}
// ... D1 쿼리 병렬 수행
};
이 설정을 통해 관리자가 어드민 메뉴를 이동할 때 브라우저 디스크/메모리 캐시를 우선 활용하므로, Cloudflare Workers 실행 및 D1 읽기 요청이 발생하지 않습니다.
3) localStorage 영구 보존 및 스마트 캐시 무효화 (Cache Busting)
- 초기 렌더링 속도 개선: 브라우저를 닫았다가 다시 열어도
localStorage(skbe_admin_posts_cache_v1)에 저장된 메타데이터를 즉시 로드하여 화면을 렌더링합니다. - 글쓰기/수정/삭제 시 자동 무효화:
- 글 작성(
new) 및 수정([id]) 완료 시/posts?refreshed=${Date.now()}로 리다이렉트되어 브라우저 HTTP 캐시와 로컬스토리지 캐시를 동시에 파기하고 최신 D1 데이터를 다시 받아옵니다. - 글 삭제 성공 시에도 로컬 캐시를 삭제하고 타임스탬프 갱신을 수행합니다.
- 글 작성(
- 수동 강제 새로고침 (
[🔄]버튼): 외부에서 DB가 변경되었을 때 관리자가 수동으로 캐시를 비우고 데이터를 다시 가져올 수 있는 새로고침 버튼을 상단 헤더에 배치했습니다.
4. 다국어(i18n) 딕셔너리 확장 및 허브(Hub) 연동 개선
1) 신규 UI 텍스트 14종 한/영/일 딕셔너리 등록
새로 추가된 새로고침 버튼, 2단 필터 탭, 특수 분류 툴팁, 맞춤형 빈 상태 안내 메시지 14종 전체를 공용 다국어 사전(packages/shared/src/i18n/index.ts)에 정식 등록했습니다.
// [packages/shared/src/i18n/index.ts]
"admin.posts.filter_page": { "ko": "정적 페이지", "en": "Static Pages", "ja": "固定ページ" },
"admin.posts.filter_draft": { "ko": "초안", "en": "Drafts", "ja": "下書き" },
"admin.posts.filter_uncategorized": { "ko": "미분류", "en": "Uncategorized", "ja": "未分類" },
"admin.posts.btn_refresh": { "ko": "캐시 비우고 최신 데이터로 새로고침", "en": "Clear cache and refresh with latest data", "ja": "キャッシュをクリアして最新データで更新" },
관리자 언어 설정을 English나 日本語로 변경해도 기본값 폴백 없이 각 언어로 정상 출력됩니다.
2) 허브(Hub) 연동 시 다국어 사이트명(site_title) 자동 추출
포스트를 발행하거나 숨길 때 블로그 허브(Hub) 플랫폼으로 전송되는 페이로드에, 다국어 사이트 제목(site_title JSON)에서 해당 포스트의 언어(lang)에 부합하는 사이트명을 안전하게 추출(resolveSiteName)하여 동봉하도록 보강했습니다.
5. 마치며
이번 SKBE v1.0.1.2는 관리자 화면의 탐색 편의성을 높이는 다차원 필터링 구조와, 서버리스 환경의 네트워크 지연 및 비용을 줄이는 클라이언트 캐싱 파이프라인을 구현한 릴리즈입니다.
데이터 양이 늘어나더라도 UI 반응성을 유지하고, 불필요한 인프라 호출 없이 안정적으로 동작하도록 지속적으로 개선해 나가겠습니다.
댓글 0개
댓글을 작성하려면 로그인이 필요합니다.