<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Sveltekit Blog Engine</title>
        <link>https://testblog-6br.pages.dev</link>
        <description>Cloudflare D1, KV, Pages 기반의 무료 서버리스 스벨트킷 블로그 엔진 공식 블로그입니다.</description>
        <language>ko</language>
        <atom:link href="https://testblog-6br.pages.dev/rss.xml" rel="self" type="application/rss+xml"/>
        <lastBuildDate>Tue, 22 Sep 2026 16:52:07 GMT</lastBuildDate>
        <item>
            <title><![CDATA[SKBE v1.0.1.0 업데이트: 슬롯별 디자인 독립 프리셋, 반응형 레이아웃 및 다국어 최적화]]></title>
            <link>https://testblog-6br.pages.dev/devlog/skbe-v1010-design-preset-backup-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/skbe-v1010-design-preset-backup-guide</guid>
            <pubDate>Sun, 20 Sep 2026 23:18:38 GMT</pubDate>
            <description><![CDATA[SvelteKit Blog Engine(SKBE) v1.0.1.0 업데이트 내역을 정리했습니다. DB 파괴 위험 없이 특정 슬롯의 디자인만 독립적으로 백업하고 원하는 슬롯에 주입하는 중립 프리셋 시스템, 사이드바 반응형 가변 스택 레이아웃 및 일본어 줄바꿈/다국어 완벽 지원, 배포 스크립트 프로젝트명 자동 동기화 과정을 공유합니다.]]></description>
            <content:encoded><![CDATA[<h2 id="1-시작하며-v10015-이후-도출된-운영-과제들">1. 시작하며: v1.0.0.15 이후 도출된 운영 과제들</h2>
<p>SvelteKit Blog Engine(SKBE) v1.0.0.15를 릴리즈하며 D1 사이드바 스냅샷 엔진과 미디어 마이그레이션 도구를 구축한 이후, 실제 환경에서 관리자 콘솔을 운영하고 다양한 언어(한국어, 영어, 일본어) 및 다양한 화면 해상도에서 디자인을 편집해보면서 실질적인 개선 과제들이 확인되었습니다.</p>
<p>이번 v1.0.1.0 업데이트에서는 블로그 운영의 핵심인 <strong>디자인 편집 안정성</strong>, <strong>다국어 환경에서의 UI 반응형 완성도</strong>, 그리고 <strong>배포 자동화 편의성</strong>을 전면 개편했습니다:</p>
<ol>
<li><strong>디자인 백업의 파괴적 덮어쓰기 위험 제거</strong>: DB 전체 테이블을 덮어써서 다른 슬롯 디자인까지 날려버리던 기존 백업 방식을 폐지하고, 특정 슬롯만 독립된 JSON 프리셋으로 안전하게 내보내고 원하는 슬롯에 복원하는 구조로 전면 전환</li>
<li><strong>사이드바 좁은 폭에서의 UI 넘침 및 다국어 글자 잘림 해소</strong>: 좁은 사이드바에서 버튼이 우측 프레임 밖으로 삐져나가던 레이아웃 결함과, 띄어쓰기가 없는 일본어 문장에서 줄바꿈이 되지 않던 CJK 타이포그래피 결함 해결</li>
<li><strong>슬롯 기본명의 실시간 다국어 번역</strong>: DB에 한국어로 저장된 슬롯 기본 이름(&quot;디자인 슬롯 1&quot; 등)을 관리자 언어에 맞춰 실시간으로 동적 번역(<code>デザインスロット 1</code>, <code>Design Slot 1</code>)</li>
<li><strong>배포 스크립트 프로젝트명 자동 동기화</strong>: 최초 설치나 멀티 계정 전환 시 <code>package.json</code> 배포 명령어 속 프로젝트명을 수동으로 수정할 필요가 없도록 자동화 파이프라인 구축</li>
<li><strong>사이드바 스냅샷 실시간 연동 및 의존성 보안 패치</strong>: 글 작성/수정/삭제 시 D1 캐시 즉각 동기화 및 라이브러리 취약점 해결</li>
</ol>
<hr>
<h2 id="2-디자인-에디터-슬롯별-독립-프리셋-백업복원-아키텍처">2. 디자인 에디터: 슬롯별 독립 프리셋 백업/복원 아키텍처</h2>
<h3 id="1-기존-방식의-한계와-위험성">1) 기존 방식의 한계와 위험성</h3>
<p>이전의 디자인 백업/복원 기능은 <code>/api/restore</code> 엔드포인트를 통해 데이터베이스의 <code>layouts</code>, <code>widgets</code>, <code>layout_widgets</code> 테이블을 통째로 DELETE한 뒤 백업 파일의 레코드로 다시 INSERT하는 방식이었습니다.</p>
<p>이 구조는 심각한 잠재 위험을 안고 있었습니다:</p>
<ul>
<li><strong>데이터 파괴 위험</strong>: 백업 파일 하나를 복원하는 순간, 열심히 꾸며둔 1번, 2번, 3번 슬롯의 모든 레이아웃과 위젯 배치가 예고 없이 초기화되었습니다.</li>
<li><strong>슬롯 선택 불가</strong>: 특정 슬롯(예: 2번 다크 모던 슬롯)의 디자인만 파일로 보관하거나, 다른 블로그에서 만든 멋진 디자인을 현재 블로그의 3번 슬롯에만 안전하게 가져올 수 없었습니다.</li>
<li><strong>복원 전 확인 불가</strong>: 가져오기 버튼을 누르는 즉시 실제 DB가 덮어써져 잘못된 백업 파일을 올렸을 때 되돌릴 방법이 없었습니다.</li>
</ul>
<h3 id="2-개선된-아키텍처-중립-프리셋design-preset-시스템">2) 개선된 아키텍처: 중립 프리셋(<code>design_preset</code>) 시스템</h3>
<p>기존의 위험한 DB 덤프 방식을 완전히 걷어내고, 에디터 내부의 메모리 상태(<code>slots</code>)와 결합된 <strong>독립형 디자인 프리셋 규격</strong>을 새롭게 설계했습니다.</p>
<pre><code class="language-typescript">// 특정 슬롯의 순수 디자인 데이터만 중립 규격으로 추출
const presetData = {
    version: &quot;3.0&quot;,
    backupType: &quot;design_preset&quot;,
    timestamp: new Date().toISOString(),
    presetName: slotName,
    design: {
        theme: snapshot.theme,
        header: snapshot.header,
        footer: snapshot.footer,
        site_title: snapshot.site_title,
        widget_shadow_global: snapshot.widget_shadow_global,
        layout: snapshot.layout,
        widgets: snapshot.widgets
    }
};
</code></pre>
<h4 id="신규-프리셋-시스템의-핵심-특장점">신규 프리셋 시스템의 핵심 특장점</h4>
<ul>
<li><strong>슬롯 번호 종속성 배제</strong>: 프리셋 파일 내부에는 슬롯 ID(<code>slot1</code>, <code>slot2</code> 등)가 일절 기록되지 않습니다. 따라서 1번 슬롯에서 추출한 디자인이라도 원하는 어떤 슬롯(2번, 3번 등)에든 자유롭게 주입할 수 있습니다.</li>
<li><strong>미저장 최신 편집 상태 즉시 반영</strong>: 현재 편집 중인 활성 슬롯을 백업할 경우, DB에 저장되지 않은 실시간 편집 상태(<code>getSnapshotOfCurrentSlot()</code>)까지 온전히 반영되어 즉각 다운로드됩니다.</li>
<li><strong>2단계 안전 주입 (Preview-First)</strong>: 복원 파일을 업로드하고 복원 대상 슬롯을 지정하면, DB를 즉시 건드리지 않고 <strong>에디터 메모리에 먼저 주입되어 화면 프리뷰로 결과를 즉시 확인</strong>할 수 있습니다. 사용자가 화면을 확인한 후 상단의 [블로그에 적용] 또는 [현재 설정 저장] 버튼을 명시적으로 눌러야만 최종 DB에 반영됩니다.</li>
<li><strong>구버전 백업 자동 호환 (<code>convertLegacyBackupToPreset</code>)</strong>: 과거에 생성된 테이블 덤프 형태의 백업 파일도 파서가 자동으로 구조를 감지하여 슬롯 프리셋 규격으로 정규화하므로, 기존 백업 자산을 그대로 재사용할 수 있습니다.</li>
</ul>
<hr>
<h2 id="3-사이드바-반응형-가변-스택-레이아웃-및-다국어i18n-완성">3. 사이드바 반응형 가변 스택 레이아웃 및 다국어(i18n) 완성</h2>
<h3 id="1-사이드바-요소-프레임-넘침-및-버튼-짤림-해결">1) 사이드바 요소 프레임 넘침 및 버튼 짤림 해결</h3>
<p>관리자 화면의 사이드바 가용 너비는 패딩을 제외하면 약 260px~290px 내외입니다. 기존에는 드롭다운(<code>select</code>)과 [다운로드] 버튼, 그리고 [복원 대상 슬롯] 라벨과 드롭다운, [복원 적용] 버튼이 각각 가로 1열(<code>flex-row</code>)에 나란히 배치되어 있었습니다.</p>
<p>이로 인해 드롭다운이 너비를 차지하면서 우측 버튼들이 컨테이너 경계 밖으로 밀려나 다운로드 버튼 글자가 잘리고, 복원 적용 버튼은 화면 밖으로 완전히 사라지는 문제가 있었습니다.</p>
<p>이를 **세로 스택형 가변 레이아웃(<code>flex-col</code>, <code>w-full</code>)**으로 전면 개편했습니다:</p>
<pre><code class="language-html">&lt;!-- 백업 영역: 세로 100% 폭 분리 배치 --&gt;
&lt;div class=&quot;setting-control flex flex-col gap-2 w-full&quot;&gt;
    &lt;select class=&quot;select-field w-full&quot; bind:value={slotBackupTargetId}&gt;...&lt;/select&gt;
    &lt;button class=&quot;btn-primary w-full flex items-center justify-center gap-2&quot;&gt;
        &lt;Download size={16} /&gt;
        &lt;span&gt;슬롯 백업 다운로드&lt;/span&gt;
    &lt;/button&gt;
&lt;/div&gt;
</code></pre>
<p>드롭다운과 버튼이 각각 독립된 100% 폭을 가지므로, 사이드바 너비가 좁아지더라도 절대 프레임을 벗어나지 않고 안정적으로 정렬됩니다.</p>
<h3 id="2-일본어-환경에서의-글자-깨짐-및-18글자-가타카나-버튼-돌출-해결">2) 일본어 환경에서의 글자 깨짐 및 18글자 가타카나 버튼 돌출 해결</h3>
<p>다국어 테스트 중, 한국어와 영어에서는 발생하지 않던 심각한 레이아웃 파괴 현상이 <strong>일본어 환경에서만 유독 발생</strong>하는 현상이 확인되었습니다. 면밀한 디버깅 끝에 두 가지 물리적 원인을 밝혀내고 해결했습니다:</p>
<h4 id="원인-a-일본어-띄어쓰기-부재와-word-break-keep-all의-충돌">원인 A: 일본어 띄어쓰기 부재와 <code>word-break: keep-all</code>의 충돌</h4>
<ul>
<li><strong>현상</strong>: 안내 설명문(<code>各スロットのデザインを独立したファイルとしてエ...</code>)의 우측이 잘리고 프레임을 뚫고 나감.</li>
<li><strong>원인</strong>: 한국어는 띄어쓰기가 있어 <code>keep-all</code>이 단어 단위 줄바꿈을 지원하지만, <strong>일본어 문장에는 띄어쓰기(공백)가 전혀 없습니다</strong>. 브라우저가 문장 전체를 하나의 거대한 단어로 인식하여 줄바꿈을 포기한 것입니다.</li>
<li><strong>해결</strong>: <code>overflow-wrap: anywhere; word-break: break-word;</code>를 적용하여 띄어쓰기가 없는 일본어 문장도 컨테이너 경계선 안에서 글자 단위로 매끄럽게 자동 줄바꿈되도록 처리했습니다.</li>
</ul>
<h4 id="원인-b-레거시-css-width-auto-important와-일본어-가타카나-18글자">원인 B: 레거시 CSS <code>width: auto !important;</code>와 일본어 가타카나 18글자</h4>
<ul>
<li><strong>현상</strong>: 일본어 백업 버튼(<code>スロットバックアップをダウンロード</code>)이 흰색 카드 우측 테두리를 뚫고 50px 이상 튀어나옴.</li>
<li><strong>원인</strong>: 과거 레거시 스타일에 선언되어 있던 <code>.setting-control .btn-primary { width: auto !important; }</code>가 HTML의 <code>w-full</code> 클래스를 강제로 무효화했습니다. 한국어(<code>슬롯 백업 다운로드</code>, 9글자)는 글자가 짧아 문제가 없었으나, 일본어 가타카나는 18글자로 2배 길어지면서 버튼 고유 너비가 320px 이상으로 비정상 확장된 것입니다.</li>
<li><strong>해결</strong>: CSS 셀렉터를 <code>.setting-control .btn-primary:not(.w-full)</code>로 수정하여 <code>!important</code> 강제를 걷어내고, 버튼에 <code>style=&quot;width: 100% !important; max-width: 100%; white-space: normal; word-break: break-word;&quot;</code>를 적용하여 긴 텍스트도 카드 안에서 100% 폭에 맞추어 유연하게 줄바꿈되도록 수정했습니다.</li>
</ul>
<h3 id="3-슬롯-기본명의-실시간-다국어-동적-번역-getslotdisplayname">3) 슬롯 기본명의 실시간 다국어 동적 번역 (<code>getSlotDisplayName</code>)</h3>
<p>슬롯 1, 2, 3의 기본 이름(<code>&quot;디자인 슬롯 1&quot;</code>, <code>&quot;미니멀 1열&quot;</code>, <code>&quot;다크 모던&quot;</code>)은 DB에 한국어 문자열로 영구 저장되기 때문에, 관리자 언어를 영어/일본어로 변경해도 선택창 안에는 한국어 문자열이 그대로 노출되던 문제가 있었습니다.</p>
<p><code>getSlotDisplayName</code> 헬퍼 함수를 도입하여, 사용자가 직접 수정한 커스텀 이름은 온전히 보존하면서도 기본 슬롯명은 현재 활성 언어(<code>adminLang.value</code>)에 맞춰 실시간으로 번역되어 노출되도록 개선했습니다:</p>
<ul>
<li><strong>한국어</strong>: <code>슬롯 1 (디자인 슬롯 1)</code></li>
<li><strong>영어</strong>: <code>Slot 1 (Design Slot 1)</code></li>
<li><strong>일본어</strong>: <code>スロット 1 (デザインスロット 1)</code></li>
</ul>
<hr>
<h2 id="4-배포-및-멀티-계정-자동화-프로젝트명-동기화-파이프라인">4. 배포 및 멀티 계정 자동화: 프로젝트명 동기화 파이프라인</h2>
<p>SKBE는 복수의 Cloudflare 계정으로 블로그와 어드민을 분산 배포할 수 있는 멀티 계정 배포 시스템(<code>scripts/deploy-multi.js</code>)을 갖추고 있습니다.</p>
<p>기존에는 최초 셋업(<code>setup.js</code>) 시 프로젝트명을 직접 입력하거나 자동 난수로 생성하더라도, <code>package.json</code>의 기본 배포 명령어(<code>npm run deploy:blog</code>, <code>npm run deploy:admin</code>) 내 <code>--project-name</code> 인자는 수동으로 고쳐주어야 하는 번거로움이 있었습니다.</p>
<p><code>scripts/setup.js</code>와 <code>scripts/sync-accounts.js</code>에 <code>updatePackageJsonDeployScripts</code> 파이프라인을 추가하여:</p>
<ul>
<li>최초 자동/수동 설치 시 확정된 프로젝트명으로 <code>package.json</code>의 배포 스크립트를 즉각 자동 갱신합니다.</li>
<li><code>npm run deploy:sync</code> 실행 시에도 <code>.deploy-accounts.json</code>의 <code>main</code> 계정 프로젝트명과 <code>package.json</code>이 항상 오차 없이 100% 동기화되도록 보장합니다.</li>
</ul>
<hr>
<h2 id="5-엔진-최적화-및-의존성-보안-패치">5. 엔진 최적화 및 의존성 보안 패치</h2>
<ol>
<li><strong>사이드바 스냅샷 실시간 이벤트 동기화</strong>:<ul>
<li>v1.0.0.15에서 도입된 O(1) 사이드바 스냅샷 캐시가 글 작성(<code>new</code>), 수정(<code>[id]</code>), 삭제 및 상태 변경(<code>posts</code>) 시 즉시 실시간으로 백그라운드 재생성(<code>generateSidebarSnapshot</code>)되도록 연결했습니다.</li>
</ul>
</li>
<li><strong>미디어 마이그레이션 경로 지원 확장</strong>:<ul>
<li>본문 내 프록시 URL을 직접 서빙 주소로 치환하는 <code>/api/media/migrate-urls</code> 도구의 정규표현식과 DB 쿼리를 보강하여, <code>posts/</code> 하위 경로 및 커스텀 폴더에 저장된 이미지 URL도 누락 없이 일괄 감지 및 치환되도록 확장했습니다.</li>
</ul>
</li>
<li><strong>핵심 라이브러리 보안 패치 및 Zod v4 버전 락</strong>:<ul>
<li><code>better-auth</code> 및 <code>tiptap</code> 관련 라이브러리의 보안 패치 버전을 적용하고, pnpm <code>overrides</code>를 통해 <code>zod</code> 패키지를 v4 계열로 명시적 고정하여 빌드 안정성을 확보했습니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="6-버전-요약-비교-before-vs-after">6. 버전 요약 비교 (Before vs After)</h2>
<table>
<thead>
<tr>
<th align="left">비교 항목</th>
<th align="left">기존 (v1.0.0.15 이전)</th>
<th align="left">개선 (v1.0.1.0)</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>디자인 백업/복원</strong></td>
<td align="left">DB 테이블 전체 삭제 후 강제 덮어쓰기 (타 슬롯 파괴)</td>
<td align="left"><strong>슬롯별 독립 중립 프리셋(<code>design_preset</code>) 안전 주입</strong></td>
</tr>
<tr>
<td align="left"><strong>복원 확인 단계</strong></td>
<td align="left">복원 즉시 DB 덮어쓰기 (되돌리기 불가)</td>
<td align="left"><strong>에디터 메모리 우선 주입 ➔ 화면 프리뷰 확인 ➔ 사용자 확정 저장</strong></td>
</tr>
<tr>
<td align="left"><strong>사이드바 레이아웃</strong></td>
<td align="left">가로 1열 배치로 좁은 화면에서 버튼 짤림 및 사라짐</td>
<td align="left"><strong>세로 스택(<code>flex-col</code>, <code>w-full</code>) 가변형 100% 맞춤 정렬</strong></td>
</tr>
<tr>
<td align="left"><strong>일본어 타이포그래피</strong></td>
<td align="left"><code>keep-all</code>로 인한 줄바꿈 실패, 버튼 50px 이상 돌출</td>
<td align="left"><strong><code>overflow-wrap: anywhere</code>, 가변 버튼으로 완벽 렌더링</strong></td>
</tr>
<tr>
<td align="left"><strong>슬롯명 다국어</strong></td>
<td align="left">DB에 저장된 한국어 이름 고정 노출</td>
<td align="left"><strong>기본 슬롯명 실시간 동적 번역 (<code>getSlotDisplayName</code>)</strong></td>
</tr>
<tr>
<td align="left"><strong>배포 명령어 관리</strong></td>
<td align="left">프로젝트명 변경 시 <code>package.json</code> 수동 수정 필요</td>
<td align="left"><strong>셋업 및 동기화 시 <code>package.json</code> 배포 스크립트 자동 갱신</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="7-마치며">7. 마치며</h2>
<p>이번 v1.0.1.0은 단순한 기능 추가를 넘어, 실제 사용자가 관리자 화면에서 경험할 수 있는 **데이터 안전성 결함(디자인 덮어쓰기)**과 **다국어 환경에서의 디테일한 시각적 완성도(일본어 줄바꿈 및 버튼 정렬)**를 꼼꼼하게 다듬은 안정화 릴리즈입니다.</p>
<p>앞으로도 SKBE는 Cloudflare 엣지 환경의 고유한 장점을 극대화하면서, 어떤 언어 환경에서도 쾌적하고 안전한 블로그 운영 환경을 지속적으로 고도화해 나가겠습니다.</p>
]]></content:encoded>
            <category>개발로그</category>
        </item>
        <item>
            <title><![CDATA[SKBE v1.0.0.15 업데이트: D1 스냅샷 엔진, 스토리지 마이그레이션 및 SEO 최적화]]></title>
            <link>https://testblog-6br.pages.dev/devlog/skbe-engine-optimization-v15-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/skbe-engine-optimization-v15-guide</guid>
            <pubDate>Sun, 20 Sep 2026 06:29:56 GMT</pubDate>
            <description><![CDATA[SvelteKit Blog Engine(SKBE) v1.0.0.15 업데이트 내역을 정리했습니다. D1 복합 쿼리를 O(1)로 줄이는 사이드바 스냅샷 엔진, Cloudflare Workers 무료 한도를 지키기 위한 본문 이미지 URL 동기화 파이프라인, 그리고 재배포 없는 태그 noindex 어드민 제어와 SEO 정규화 과정을 공유합니다.]]></description>
            <content:encoded><![CDATA[<h2 id="1-시작하며-운영-중-도출된-추가-개선-과제들">1. 시작하며: 운영 중 도출된 추가 개선 과제들</h2>
<p>SvelteKit Blog Engine(SKBE) v1.0.0.11 ~ v1.0.0.14를 거치며 멀티 슬롯 디자인 시스템과 레이아웃 이동(CLS 0.000) 문제를 안정화한 이후, 실제 환경에서 글을 작성하고 운영해보면서 몇 가지 추가적인 비효율과 불편한 점들이 확인되었습니다.</p>
<p>이 프로젝트는 Cloudflare 무료 티어(Pages, D1 SQLite, KV)를 기반으로 운영 비용을 들이지 않는 구조를 지향하고 있기 때문에, 사소한 쿼리 낭비나 운영상의 번거로움도 가능한 한 줄여두는 편이 장기적으로 유리합니다.</p>
<p>이번 v1.0.0.15에서는 주로 다음과 같은 부분들을 점검하고 보완했습니다:</p>
<ol>
<li><strong>사이드바 위젯의 반복 쿼리</strong>: 방문자가 페이지를 이동할 때마다 카테고리, 최근글, 인기글을 매번 D1에 조회하던 구조 개선</li>
<li><strong>Workers 호출량 절감 및 이미지 서빙 최적화</strong>: 본문 내 프록시(<code>/images/...</code>) 주소를 R2/Supabase 직렬 엔드포인트로 일괄 전환하여 Cloudflare Workers 일일 무료 한도(100,000회) 소진 방어</li>
<li><strong>레이아웃 렌더러의 마크업 정비</strong>: 검색엔진 봇이 본문과 보조 영역을 쉽게 구분할 수 있도록 시맨틱 태그 구조화</li>
<li><strong>검색엔진 메타데이터 보완 및 태그 noindex 동적 제어</strong>: 사이트맵과 Open Graph 규격을 다듬고, 태그 페이지 색인 여부를 재배포 없이 어드민에서 켜고 끌 수 있는 설정 추가</li>
</ol>
<hr>
<h2 id="2-d1-읽기-쿼리-절감-사이드바-스냅샷-도입">2. D1 읽기 쿼리 절감: 사이드바 스냅샷 도입</h2>
<h3 id="1-문제-상황">1) 문제 상황</h3>
<p>블로그 레이아웃(<code>+layout.server.ts</code>)에서는 어느 페이지를 열람하든 사이드바에 표시할 카테고리 목록, 최근 글, 인기 글 데이터를 준비해야 합니다.</p>
<p>기존에는 페이지 요청이 들어올 때마다 D1 데이터베이스에 이 세 가지 쿼리를 매번 실행했습니다. 방문자 수가 늘어날수록 D1 읽기 쿼리가 불필요하게 소모되었고, 엣지와 DB 간의 왕복 시간도 누적되었습니다.</p>
<h3 id="2-수정-내용-sidebar-snapshot-기반-정적-서빙">2) 수정 내용: <code>sidebar_snapshot</code> 기반 정적 서빙</h3>
<p><code>packages/shared/src/utils/snapshot.ts</code>를 추가하여, 카테고리/최근글/인기글 데이터를 미리 묶어 <code>blog_settings</code> 테이블의 <code>sidebar_snapshot</code> 단일 필드에 캐싱해 두는 구조로 바꾸었습니다.</p>
<pre><code class="language-typescript">// apps/blog/src/routes/+layout.server.ts
const sidebarSnapshot = safeParse(settings?.sidebar_snapshot);
const hasSnapshot = sidebarSnapshot &amp;&amp; typeof sidebarSnapshot === &#39;object&#39; &amp;&amp; sidebarSnapshot.recentPosts;

if (hasSnapshot) {
    // 스냅샷이 있으면 DB 복합 쿼리 없이 즉시 데이터 반환
    categories = sidebarSnapshot.categories?.[currentLang] || sidebarSnapshot.categories?.[defaultLang] || [];
    recentPosts = rLimit ? rawRecent.slice(0, rLimit) : rawRecent;
    popularPosts = pLimit ? rawPopular.slice(0, pLimit) : rawPopular;
    
    // 변동이 잦은 태그/댓글 위젯만 필요 시 가볍게 조회
} else {
    // 스냅샷이 없으면 1회 조회 후 백그라운드에서 스냅샷 생성
    generateSidebarSnapshot(rawD1).catch(err =&gt; console.error(&#39;[Snapshot Background]&#39;, err));
}
</code></pre>
<ul>
<li><strong>결과</strong>: 일반 방문자가 글을 탐색할 때는 사이드바용 D1 쿼리가 발생하지 않고 바로 렌더링됩니다.</li>
<li>글 작성이나 수정 시에는 백그라운드 비동기 작업으로 스냅샷을 갱신하므로 사용자 응답 속도에 영향을 주지 않습니다.</li>
</ul>
<hr>
<h2 id="3-cloudflare-workers-호출량-절감-본문-이미지-url-동기화-도구">3. Cloudflare Workers 호출량 절감: 본문 이미지 URL 동기화 도구</h2>
<h3 id="1-배경-이미지-프록시-서빙의-한계와-스토리지-선택의-딜레마">1) 배경: 이미지 프록시 서빙의 한계와 스토리지 선택의 딜레마</h3>
<p>SKBE는 ImageKit, Cloudflare R2, Supabase Storage, KV 등 다양한 미디어 스토리지를 지원합니다.</p>
<p>기술적/구조적 관점만 본다면 같은 Cloudflare 생태계에 대역폭(Egress) 무료 혜택이 있는 <strong>Cloudflare R2가 가장 적합</strong>합니다. 하지만 R2를 활성화하려면 Cloudflare 계정에 <strong>신용카드 등록이 필수</strong>입니다. R2는 대역폭 비용이 무료이지만, 저장 용량(10GB)이나 요청 한도를 넘길 경우 등록된 카드에서 자동으로 후불 결제되는 구조입니다.</p>
<p>반면 <strong>ImageKit</strong>은 신용카드 등록 없이도 무료 플랜을 사용할 수 있습니다. **미디어 저장 공간 3GB와 매월 20GB의 무료 글로벌 CDN 대역폭(2026년 9월 작성일 기준)**을 제공하며, 한도를 소진하더라도 카드로 자동 결제되지 않고 서비스만 일시 정지되므로 과금 위험이 전혀 없습니다. <em>(※ 클라우드 및 스토리지 서비스의 무료 티어 정책은 제공사의 사정에 따라 향후 변동될 수 있습니다)</em></p>
<p>따라서 개인적으로는 <strong>우선 신용카드 등록 없이 안전한 ImageKit으로 시작하고, 이후 블로그가 성장하여 무료 한도(저장 공간 3GB 또는 월 20GB 대역폭)에 다다르게 되었을 때 Cloudflare R2로 이전하는 방식</strong>을 가장 추천합니다. 이번에 구현한 본문 이미지 URL 동기화 도구가 바로 이러한 스토리지 전환 시점에 기존 글들의 이미지 주소를 버튼 클릭 한 번으로 손쉽게 옮겨주는 역할을 합니다.</p>
<p>다만 어떤 스토리지를 사용하든, 초기 구현의 단순함을 위해 모든 이미지를 자체 엔드포인트(<code>/images/...</code>)를 거치는 프록시 방식으로 서빙했을 때 큰 문제가 발생했습니다. 방문자가 이미지가 여러 개 포함된 글 하나를 열람할 때마다 이미지 수만큼 <strong>Cloudflare Worker가 매번 실행되어 일일 무료 요청 한도(100,000회)를 빠르게 잠식</strong>하는 현상이었습니다.</p>
<p>이를 방지하기 위해 ImageKit이나 R2 직접 도메인, Supabase 직접 URL 서빙 모드를 도입하여 Worker를 거치지 않고 CDN/스토리지가 직접 트래픽을 처리하도록 개선했으나, 또 다른 현실적인 문제가 남았습니다. <strong>이전에 작성된 기존 포스트 본문(<code>&lt;p&gt;&lt;img src=&quot;/images/...&quot;&gt;&lt;/p&gt;</code>) 내의 이미지 주소는 여전히 프록시 경로로 박혀 있어</strong>, 직렬 서빙 모드로 바꾸더라도 기존 글을 읽을 때는 계속 불필요한 Worker 요청이 발생하는 것이었습니다.</p>
<p>스토리지 변경이나 백업 복원 시에도 이전 주소가 그대로 남아 이미지가 깨지거나 불필요한 프록시 호출이 이어졌습니다.</p>
<blockquote>
<p>⚠️ <strong>중요 제약사항: 기본 Cloudflare KV 사용 시 URL 수정 불가</strong><br>Cloudflare KV는 퍼블릭 직접 링크(Public Direct URL)를 지원하지 않습니다. 따라서 <strong>기본 KV 스토리지를 사용하는 상태에서는 직렬 서빙 전환이나 본문 URL 수정을 적용할 수 없으며, 무조건 도메인 프록시(<code>/images/...</code>) 방식으로만 동작</strong>합니다.<br>본문 이미지 URL 동기화 도구를 통해 Workers 호출량을 줄이려면 ImageKit(추천)이나 R2, Supabase와 같은 객체 스토리지로 전환해야 합니다.</p>
</blockquote>
<h3 id="2-수정-내용-본문-이미지-url-일괄-마이그레이션-도구">2) 수정 내용: 본문 이미지 URL 일괄 마이그레이션 도구</h3>
<p>기존 글들의 이미지 요청이 Worker를 우회하여 최적의 직렬 서빙 경로를 탈 수 있도록 URL 변환 도구를 구축했습니다:</p>
<ul>
<li><p><strong>URL 마이그레이션 엔드포인트 추가</strong> (<code>apps/admin/src/routes/api/media/migrate-urls/+server.ts</code>):<br>발행된 모든 포스트의 본문을 순회하며 프록시 경로(<code>/images/...</code>)나 이전 스토리지 패턴의 URL을 정규식으로 감지하고, 현재 활성화된 스토리지(R2 직접 도메인, Supabase 직접 서빙 등)의 엔드포인트 주소로 일괄 치환하여 저장합니다.</p>
</li>
<li><p><strong>어드민 도구 UI 배치</strong> (<code>apps/admin/src/routes/media/+page.svelte</code>):<br>스토리지 설정 탭 하단에 &quot;포스트 이미지 URL 동기화 도구&quot;를 배치하여, 저장소 서빙 모드를 전환한 후 버튼 클릭 한 번으로 모든 글의 이미지 URL을 즉시 직렬 경로로 일괄 갱신할 수 있게 했습니다.</p>
</li>
<li><p><strong>백업 복원 연동</strong>:<br>백업 파일을 복원할 때도 &quot;본문 이미지 URL 자동 마이그레이션&quot; 옵션을 제공하여, 복원과 동시에 현재 블로그 환경의 직렬 서빙 주소로 자동 재구성되도록 보완했습니다.</p>
</li>
<li><p><strong>효과</strong>: 기존 글 열람 시 발생하던 이미지 프록시용 Workers 호출이 사라지고, 방문자는 스토리지/CDN에서 이미지를 직접 내려받아 Workers 일일 무료 한도를 안전하게 방어할 수 있게 되었습니다.</p>
</li>
</ul>
<h3 id="3-안전한-마이그레이션을-위한-권장-수칙">3) ⚠️ 안전한 마이그레이션을 위한 권장 수칙</h3>
<p>본문 내 이미지 URL을 일괄 치환하는 작업은 데이터베이스의 글 내용을 직접 수정하는 과정이므로, 현실적으로 모든 환경과 예외 상황에 완벽히 대비되어 있다고 단정할 수는 없습니다. 따라서 마이그레이션을 진행할 때는 반드시 아래 수칙을 지키는 것을 권장합니다:</p>
<ol>
<li><strong>마이그레이션 전 전체 백업 필수</strong>:<br>마이그레이션을 시도하기 전에 어드민의 백업 기능을 이용하여 <strong>D1 데이터베이스와 미디어 파일 전체를 반드시 로컬에 백업</strong>해 두어야 합니다.</li>
<li><strong>원복(Rollback) 대비</strong>:<br>혹시 마이그레이션 도중 오류가 발생하거나 주소가 잘못 매핑되더라도 즉시 원상태로 복원할 수 있도록, <strong>기존 이미지 서버의 데이터를 미리 삭제하지 말고 그대로 둔 상태에서 진행</strong>해야 합니다.</li>
<li><strong>테스트용 복사 블로그 검증 (강력 추천)</strong>:<br>가장 추천하는 안전한 방법은 <strong>백업받은 데이터로 테스트용 복사 블로그를 별도로 하나 개설하여 먼저 마이그레이션을 테스트해보는 것</strong>입니다. 테스트 환경에서 본문 이미지들이 깨짐 없이 정상 출력되는 것을 직접 확인한 뒤 본 블로그에 적용하는 것을 강력히 권장합니다.</li>
</ol>
<hr>
<h2 id="4-레이아웃-렌더러-시맨틱-태그-정비">4. 레이아웃 렌더러 시맨틱 태그 정비</h2>
<p>기존 <code>LayoutRenderer.svelte</code>는 컬럼과 위젯 요소를 대부분 <code>div</code> 태그로 구성하고 있었습니다. 화면 표시는 정상이었으나, 검색엔진 크롤러 입장에서 본문 영역과 보조 영역(사이드바)을 보다 명확히 구분할 수 있도록 HTML5 시맨틱 태그를 적용했습니다:</p>
<ul>
<li>본문 위젯(<code>post_content</code>)이 포함되지 않은 컬럼은 <code>&lt;aside class=&quot;layout-column sidebar-column&quot;&gt;</code>으로 감싸 보조 콘텐츠임을 명시했습니다.</li>
<li>본문이 포함된 영역은 <code>&lt;div class=&quot;layout-column main-column&quot;&gt;</code>으로 분리했습니다.</li>
<li>개별 위젯 블록에는 <code>&lt;section class=&quot;widget-item ...&quot;&gt;</code> 태그와 적절한 <code>&lt;h3&gt;</code> 타이틀을 부여했습니다.</li>
</ul>
<hr>
<h2 id="5-검색엔진seo-메타데이터-보완">5. 검색엔진(SEO) 메타데이터 보완</h2>
<p>배포된 사이트의 실제 크롤링 상태를 점검하면서 발견된 자잘한 규격 누락 사항들을 함께 손보았습니다:</p>
<ol>
<li><strong>사이트맵 홈페이지 <code>&lt;lastmod&gt;</code> 누락 보완</strong> (<code>sitemap.xml/+server.ts</code>):<br>개별 글 URL에는 수정일이 있었으나 루트(<code>/</code>) 엔트리에는 <code>&lt;lastmod&gt;</code>가 빠져 있어 최신 포스트의 수정일을 반영하도록 추가했습니다.</li>
<li><strong>사이드바 태그 링크 인코딩</strong> (<code>TagCloudWidget.svelte</code>):<br>공백이 포함된 태그가 raw 문자열로 링크되던 부분에 <code>encodeURIComponent</code>를 적용했습니다.</li>
<li><strong>홈페이지 <code>og:image</code> 폴백</strong> (<code>[[lang=lang]]/+page.server.ts</code>):<br>어드민에서 별도의 로고를 지정하지 않았을 때 SNS 공유 시 썸네일이 비어 있던 문제를, 최신 글의 대표 이미지(<code>lcpImage</code>)로 폴백되도록 처리했습니다.</li>
<li><strong><code>og:locale</code> 다국어 표준화</strong> (<code>SeoHead.svelte</code>):<br>언어 코드가 <code>en</code>, <code>ja</code>로 출력되던 것을 표준 Open Graph 규격인 <code>en_US</code>, <code>ja_JP</code>로 변환하여 출력하도록 분기를 추가했습니다.</li>
</ol>
<hr>
<h2 id="6-태그-noindex-어드민-제어-및-다국어-딕셔너리-반영">6. 태그 noindex 어드민 제어 및 다국어 딕셔너리 반영</h2>
<h3 id="1-배경">1) 배경</h3>
<p>블로그 개설 초기에는 글 수가 적어 태그 페이지마다 비슷한 글 목록이 중복 노출되기 쉽습니다. 이로 인한 검색엔진의 중복 콘텐츠 페널티를 피하기 위해 태그 페이지는 기본적으로 <code>noindex</code> 처리를 해두는 편이 안전합니다.</p>
<p>다만 블로그가 커지고 태그별로 글이 충분히 쌓였을 때 색인을 허용하고 싶다면, 기존에는 코드를 직접 고쳐 재배포해야 했습니다.</p>
<h3 id="2-수정-내용">2) 수정 내용</h3>
<ul>
<li><strong>어드민 사이트 설정에 토글 추가</strong> (<code>apps/admin/src/routes/settings/+page.svelte</code>):<br>&quot;태그 페이지 검색엔진 색인 차단 (noindex)&quot; 항목을 추가하여, 켜면 noindex(차단), 끄면 index(허용)가 되도록 구성했습니다. 기본값은 ON입니다.</li>
<li><strong>블로그 실시간 연동</strong> (<code>apps/blog/src/routes/[[lang=lang]]/tags/[tag]/+page.server.ts</code>):<br>하드코딩되어 있던 <code>noindex: true</code>를 어드민 설정값(<code>settings?.tag_page_noindex !== &#39;false&#39;</code>)으로 교체하여, 재배포 없이 설정 저장만으로 동작이 바뀌도록 했습니다.</li>
<li><strong>i18n 사전 등록</strong>:<br>해당 항목의 제목과 설명을 <code>packages/shared/src/i18n/index.ts</code>의 공통 딕셔너리에 한국어, 영어, 일본어로 각각 등록했습니다.</li>
</ul>
<hr>
<h2 id="7-마치며">7. 마치며</h2>
<p>이번 v1.0.0.15 작업은 겉으로 드러나는 화려한 기능을 추가하기보다는, 실제로 서비스를 운영하면서 마주치는 자잘한 비효율과 관리상의 번거로움을 줄이는 데 초점을 맞추었습니다.</p>
<p>반복되는 DB 조회를 줄이고, 스토리지 변경 시의 손이 덜 가도록 정리하고, 검색엔진 규격을 조금 더 깔끔하게 다듬는 것만으로도 운영 부담이 한결 가벼워지는 것을 느낍니다. 앞으로도 실제 운영 관점에서 필요한 부분들을 차분히 다듬어 나갈 생각입니다.</p>
]]></content:encoded>
            <category>개발로그</category>
        </item>
        <item>
            <title><![CDATA[SvelteKit Blog Engine 고도화 기록: 멀티 슬롯 디자인, D1 캐시 최적화, SEO 정규화 (v1.0.0.11 ~ v1.0.0.13)]]></title>
            <link>https://testblog-6br.pages.dev/devlog/skbe-engine-optimization-v11-v13-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/skbe-engine-optimization-v11-v13-guide</guid>
            <pubDate>Tue, 08 Sep 2026 02:58:10 GMT</pubDate>
            <description><![CDATA[SvelteKit Blog Engine(SKBE) v1.0.0.11부터 v1.0.0.13까지 진행된 구조 개선 내역을 정리했습니다. 멀티 슬롯 테마 시스템, D1 읽기/쓰기 절감 캐시, 모바일 CLS 0.000 대응, SEO 캐노니컬 정규화 및 llms.txt 적용 과정을 다룹니다.]]></description>
            <content:encoded><![CDATA[<h2 id="1-시작하며-운영-환경에서-발견된-개선-과제들">1. 시작하며: 운영 환경에서 발견된 개선 과제들</h2>
<p>SvelteKit Blog Engine(SKBE) v1.0.0.10을 릴리즈한 이후, 실제 환경에서 서비스를 운영하고 테스트하면서 몇 가지 구조적인 병목과 보완점이 확인되었습니다.</p>
<p>특히 이 프로젝트는 <strong>Cloudflare Free Tier(Pages, D1 SQLite, KV) 환경에서 월 비용 0원($0)으로 운영되는 서버리스 아키텍처</strong>를 지향합니다. 따라서 트래픽이나 데이터가 늘어나더라도 무료 한도 내에서 안정적으로 서비스가 유지되도록 설계하는 것이 핵심이었습니다.</p>
<p>점검 과정에서 도출된 주요 과제는 다음과 같았습니다:</p>
<ol>
<li><strong>D1 데이터베이스 쿼리 효율화</strong>: Cloudflare D1의 일일 쓰기 한도(100,000회)와 반복적인 읽기 쿼리를 어떻게 최소화할 것인가?</li>
<li><strong>모바일 렌더링 안정성</strong>: JS 마운트 시점에 레이아웃을 분기하면서 발생하던 모바일 화면의 미세한 누적 레이아웃 이동(CLS)을 어떻게 없앨 것인가?</li>
<li><strong>검색엔진(SEO) 및 AI 크롤러 규격 준수</strong>: 다국어 환경에서 canonical URL과 hreflang 사이의 모순을 해결하고, ChatGPT나 Perplexity 같은 최신 AI 검색 봇을 위한 표준 규격을 갖추었는가?</li>
</ol>
<p>이 과제들을 해결하기 위해 v1.0.0.11부터 v1.0.0.13까지 순차적으로 진행했던 기술적 검토와 코드 수정 내역을 정리해 보았습니다.</p>
<hr>
<h2 id="2-v10011-안정성-보완-애드센스-렌더링-및-런타임-오류-대응">2. [v1.0.0.11] 안정성 보완: 애드센스 렌더링 및 런타임 오류 대응</h2>
<p>첫 번째 단계에서는 광고 노출과 관련된 레이아웃 문제 및 특정 파라미터 인입 시 발생하던 500 에러를 점검했습니다.</p>
<h3 id="애드센스-데스크톱-공백-렌더링-방어">애드센스 데스크톱 공백 렌더링 방어</h3>
<p>모바일에서는 광고가 정상 표시되었으나, 데스크톱의 특정 너비 구간에서 구글 애드센스 반응형 유닛이 높이 0px로 렌더링되며 공백으로 남는 현상이 있었습니다.</p>
<ul>
<li><strong>원인</strong>: CSS 그리드 트랙의 반응형 계산 시점과 애드센스 스크립트(<code>adsbygoogle.push</code>)가 컨테이너 폭을 측정하는 시점 사이에 타이밍 차이가 존재했습니다.</li>
<li><strong>조치</strong>: 광고 래퍼에 <code>min-height: 280px</code>와 반응형 컨테이너 스타일을 지정하여, 스크립트 실행 전에도 안정적인 렌더링 영역이 확보되도록 구조를 보완했습니다.</li>
</ul>
<h3 id="특수문자-태그-및-다국어-라우팅-안정화">특수문자 태그 및 다국어 라우팅 안정화</h3>
<p>특수문자나 공백이 포함된 태그 페이지에 접근할 때 SvelteKit 라우트 매처와 D1 쿼리 바인딩 간 인코딩 불일치로 발생하던 500 에러를 수정했습니다.</p>
<hr>
<h2 id="3-v10012-구조-개선-디자인-유연성과-d1-성능-최적화">3. [v1.0.0.12] 구조 개선: 디자인 유연성과 D1 성능 최적화</h2>
<p>v1.0.0.12에서는 디자인 관리의 유연성을 높이고, Cloudflare D1의 읽기/쓰기 부하를 대폭 줄이는 데 집중했습니다.</p>
<h3 id="1-3-슬롯-멀티-디자인-시스템-multi-slot-design-architecture">1) 3-슬롯 멀티 디자인 시스템 (Multi-Slot Design Architecture)</h3>
<p>기존의 일반적인 블로그 엔진(티스토리, 워드프레스, 고스트 등)에서 디자인을 개편하려면 꽤 큰 부담을 감수해야 했습니다. 새 테마를 적용하는 순간 기존 세팅이 덮어씌워지므로 코드를 백업해 두지 않으면 이전 디자인으로 되돌리기가 번거로웠고, 무엇보다 <strong>운영 중인 블로그에서 방문자에게 깨진 레이아웃을 노출하지 않고 실시간으로 새 디자인을 실험하기가 사실상 불가능</strong>했습니다.</p>
<p>SKBE v1.0.0.12에서는 이 문제를 구조적으로 해결하기 위해 **&#39;스냅샷 기반 3-슬롯 디자인 아키텍처&#39;**를 도입했습니다.</p>
<ul>
<li><p><strong>3개의 독립된 디자인 스냅샷 동시 유지</strong>:</p>
<ul>
<li>어드민 디자인 에디터에서 **슬롯 1(기본 메인), 슬롯 2(리디자인 실험), 슬롯 3(다크/이벤트 테마)**을 각각 독립된 완결형 스냅샷으로 구성하고 저장할 수 있습니다.</li>
<li>새 디자인을 작업할 때 라이브 서비스 중인 메인 슬롯에는 아무런 영향이 없으며, 완성된 후 마우스 클릭 한 번으로 활성 슬롯을 교체하거나 언제든 1초 만에 이전 디자인으로 롤백할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>운영 전략에 따른 3가지 노출 모드</strong>:</p>
<ul>
<li><strong>단일 슬롯 고정</strong>: 관리자가 지정한 슬롯만 라이브로 단독 서비스</li>
<li><strong>세션 랜덤 로테이션</strong>: 방문자 세션마다 슬롯을 무작위로 교체 노출하여 디자인 A/B 테스트 효과 및 신선한 사용자 경험 제공</li>
<li><strong>방문자 실시간 테마 스위처</strong>: 우측 하단 플로팅 버튼을 통해 방문자가 사이트 내에서 원하는 테마를 직접 즉시 전환</li>
</ul>
</li>
<li><p><strong>성능 낭비 없는 조건부 SSR 페이로드 설계</strong>:</p>
<ul>
<li>테마가 3개나 존재한다고 해서 사이트가 무거워지면 안 됩니다.</li>
<li>방문자 테마 선택 모드가 꺼져 있을 때는 <strong>활성 슬롯 단 1개의 CSS/설정 데이터만 HTML에 번들링</strong>하고 보조 슬롯 데이터는 전송하지 않습니다.</li>
<li>방문자 선택 기능이 활성화되었을 때만 보조 슬롯 데이터를 조건부 패키징하도록 <code>+layout.server.ts</code>를 제어하여, 멀티 테마 시스템을 유지하면서도 초기 SSR 로딩 속도와 HTML 용량은 단일 테마와 완전히 동일하게 가볍게 유지했습니다.<br><img src="https://sveltekitblog.com/images/posts/skbe-engine-optimization-v11-v13-guide/desktop/img-devlog-skbe-engine-optimization-v11-v13-guide-ko-001.webp" alt="img-devlog-skbe-engine-optimization-v11-v13-guide-ko-001"></li>
</ul>
</li>
</ul>
<h3 id="2-cloudflare-d1-캐시-레이어-및-조회수-버퍼링">2) Cloudflare D1 캐시 레이어 및 조회수 버퍼링</h3>
<p>Cloudflare D1의 무료 티어를 효율적으로 활용하기 위해 인메모리 캐싱과 배치 처리 로직을 도입했습니다.</p>
<ul>
<li><strong>인메모리 TTL 캐시 (<code>cache.ts</code>)</strong>: 블로그 기본 설정, 레이아웃, 태그 목록처럼 변경 빈도가 낮은 데이터에 60초 TTL 인메모리 캐시를 적용했습니다. 그 결과 페이지 진입 시 발생하는 D1 읽기량(<code>rows_read</code>)이 <strong>98% 이상 감소</strong>했습니다.</li>
<li><strong>조회수 배치 버퍼링 (<code>viewBuffer.ts</code>)</strong>: 게시글 조회 시마다 매번 D1에 <code>UPDATE</code> 쿼리를 보내지 않고, <strong>10건이 모이거나 30초가 경과했을 때</strong> 한 번에 묶어서 DB에 반영하는 버퍼를 구축했습니다. 사용자 화면에는 버퍼에 대기 중인 카운트를 즉시 합산하여 반환하므로 화면 표시 지연은 발생하지 않습니다.</li>
<li><strong>역정규화 카운터 컬럼 적용</strong>: 글 목록 조회 시 무거운 집계(COUNT/JOIN) 연산을 방지하기 위해 카테고리에 <code>post_count</code>, 게시글에 <code>view_count</code> 컬럼을 두고 글 작성·수정·삭제 시점에 자동으로 수치가 동기화되도록 정리했습니다.</li>
</ul>
<h3 id="3-모바일-core-web-vitals-및-렌더링-최적화">3) 모바일 Core Web Vitals 및 렌더링 최적화</h3>
<ul>
<li><strong>모바일 누적 레이아웃 이동 방어 (CLS 0.000)</strong>: 기존에 JS 마운트 후 화면 폭을 판단하여 뷰를 전환하던 방식을 순수 CSS 미디어 쿼리(<code>@media (max-width: 768px)</code>)로 전면 개편했습니다. 브라우저가 첫 번째 프레임(0.001초)부터 모바일 레이아웃을 확정하므로, <strong>CLS 수치가 1.0(불량)에서 0.000(우수)으로 정돈</strong>되었습니다.</li>
<li><strong>다국어 사전 평탄화 (HTML 70% 축소)</strong>: 블로그 프론트엔드에 불필요했던 어드민 전용 번역 키를 제외하고, 현재 접속 언어 단일 문자열로 사전을 평탄화(Flattening)했습니다. 사전 페이로드가 <strong>179 KB에서 9.19 KB로 94.87% 줄어들며</strong>, 전체 HTML 문서 크기도 70% 이상 가벼워졌습니다.</li>
<li><strong>웹폰트 비차단 Preload</strong>: <code>&lt;head&gt;</code> 내 웹폰트 호출을 <code>rel=&quot;preload&quot;</code> + <code>onload</code> 교체 방식으로 구성하여 렌더링 차단 현상을 해소하고 First Contentful Paint(FCP) 속도를 개선했습니다.</li>
</ul>
<hr>
<h2 id="4-v10013-웹-표준-완성-seo-정규화-및-llmstxt-엔드포인트">4. [v1.0.0.13] 웹 표준 완성: SEO 정규화 및 llms.txt 엔드포인트</h2>
<p>세 번째 단계에서는 검색엔진 인덱싱의 모순을 바로잡고, 최신 AI 봇이 참조할 수 있는 표준 엔드포인트를 구축했습니다.</p>
<h3 id="1-seo-캐노니컬canonical-url-정규화">1) SEO 캐노니컬(Canonical) URL 정규화</h3>
<p>다국어 라우팅 환경(<code>[[lang=lang]]</code>)에서 기본 언어로 접속했을 때 canonical 메타 태그에 기본 언어 경로(예: <code>/ko</code>)가 포함되어 <code>sitemap.xml</code> 및 <code>hreflang</code> 태그와 충돌하는 현상을 해결했습니다.</p>
<ul>
<li><strong>기본 언어 접두사 자동 제거</strong>: 시스템에 기본 언어로 설정된 언어로 접속할 경우, 홈(<code>/</code>), 카테고리(<code>/tech</code>), CMS 페이지 모두 canonical URL에서 기본 언어 접두사를 제거하여 사이트맵 표준 경로와 1:1로 일치시켰습니다.</li>
<li><strong>절대 경로 Fallback 보장</strong>: 상세 페이지와 방명록에서 <code>siteUrl</code> 미설정 시 상대 경로가 생성되던 예외를 점검하고, 항상 <code>url.origin</code>을 참조하도록 폴백을 두어 모든 환경에서 유효한 절대 경로가 출력되도록 보완했습니다.</li>
</ul>
<h3 id="2-조회수-쿠키-중복-방지-및-rfc-6265-안전-해시-키">2) 조회수 쿠키 중복 방지 및 RFC 6265 안전 해시 키</h3>
<ul>
<li><strong>10분 윈도우 중복 방지</strong>: 브라우저 <code>sessionStorage</code> 외에 서버 측 쿠키 검증 로직을 추가하여, 10분 이내 새로고침이나 탭 복제로 인한 비정상적인 조회수 증가를 방어했습니다.</li>
<li><strong>한글 슬러그 500 에러 해결</strong>: 한글/특수문자 슬러그가 쿠키 이름으로 들어갈 때 Node/SvelteKit에서 헤더 규격 위반(<code>TypeError: argument name is invalid</code>)으로 500 에러가 발생하던 문제를 해결했습니다. 슬러그를 RFC 6265 호환 영숫자 해시(<code>skbe_v_${hash}</code>)로 변환하여 안전하게 쿠키 키를 발급합니다.</li>
</ul>
<h3 id="3-다국어-지원-동적-llmstxt-엔드포인트-구축">3) 다국어 지원 동적 <code>llms.txt</code> 엔드포인트 구축</h3>
<p>ChatGPT Search, Perplexity 등 대형 언어 모델 기반 검색 봇의 크롤링을 지원하기 위해 <strong><a href="https://llmstxt.org">llmstxt.org</a> 규격의 엔드포인트</strong>를 추가했습니다.</p>
<ul>
<li><strong>다국어 경로 분기</strong>: <code>/llms.txt</code>(기본 언어), <code>/en/llms.txt</code>(영어), <code>/ja/llms.txt</code>(일본어)를 제공합니다.</li>
<li><strong>마크다운 구조화</strong>: 사이트 소개, 활성 카테고리, 최근 공개 글 30건(요약문 포함), CMS 페이지, 사이트맵/RSS 피드 링크를 표준 마크다운 형식으로 동적 생성합니다.</li>
<li><strong>Cloudflare CDN 엣지 캐싱</strong>: 10분 캐시(<code>s-maxage=600</code>)를 적용하여 Google PageSpeed Insights 모바일 검사 시 발생하던 <code>Fetch of llms.txt timed out</code> 경고를 해소했습니다.</li>
</ul>
<hr>
<h2 id="5-주요-개선-지표-요약">5. 주요 개선 지표 요약</h2>
<p>3개 버전에 걸쳐 적용된 주요 개선 결과는 다음과 같습니다:</p>
<table>
<thead>
<tr>
<th align="left">점검 항목</th>
<th align="left">개선 전</th>
<th align="left">개선 후</th>
<th align="left">비고</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>D1 읽기 쿼리</strong></td>
<td align="left">페이지 로드 시 매번 DB 조회</td>
<td align="left">60초 인메모리 TTL 캐시 적용</td>
<td align="left"><strong>읽기량 98% 이상 절감</strong></td>
</tr>
<tr>
<td align="left"><strong>D1 쓰기 빈도</strong></td>
<td align="left">조회수 발생 시 즉시 DB Write</td>
<td align="left">10건 / 30초 배치 버퍼링</td>
<td align="left"><strong>일일 무료 쓰기 한도 보호</strong></td>
</tr>
<tr>
<td align="left"><strong>모바일 CLS</strong></td>
<td align="left">1.0 (JS 마운트 후 레이아웃 분기)</td>
<td align="left"><strong>0.000 (순수 CSS 미디어쿼리)</strong></td>
<td align="left"><strong>초기 로딩 흔들림 제거</strong></td>
</tr>
<tr>
<td align="left"><strong>다국어 사전 용량</strong></td>
<td align="left">179 KB (전체 키 번들링)</td>
<td align="left"><strong>9.19 KB (단일 언어 평탄화)</strong></td>
<td align="left"><strong>94.87% 페이로드 감축</strong></td>
</tr>
<tr>
<td align="left"><strong>SSR HTML 크기</strong></td>
<td align="left">약 80~100 KB</td>
<td align="left"><strong>약 20~25 KB</strong></td>
<td align="left"><strong>70% 이상 경량화</strong></td>
</tr>
<tr>
<td align="left"><strong>SEO Canonical</strong></td>
<td align="left">기본 언어 접두사 불일치 발생</td>
<td align="left">사이트맵 및 hreflang과 1:1 일치</td>
<td align="left"><strong>중복 색인 리스크 차단</strong></td>
</tr>
<tr>
<td align="left"><strong>AI 검색 지원</strong></td>
<td align="left">미지원 (Lighthouse 경고)</td>
<td align="left"><strong>다국어 llms.txt 표준 지원</strong></td>
<td align="left"><strong>차세대 검색 봇 인용 대응</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="6-정리하며-개발-비하인드와-hub-운영-안내">6. 정리하며: 개발 비하인드와 Hub 운영 안내</h2>
<h3 id="11-12-13버전을-한-번에-묶어-전하게-된-이유">11, 12, 13버전을 한 번에 묶어 전하게 된 이유</h3>
<p>사실 v1.0.0.11은 자잘한 버그 픽스 위주라 단독 포스팅으로 다루기엔 내용이 다소 가벼워, 메이저급 변경이 포함된 v1.0.0.12와 함께 정리하려고 준비 중이었습니다.</p>
<p>그런데 12버전 작업 직후, 한글 슬러그에서의 조회수 500 에러와 다국어 canonical URL의 미세한 불일치라는 치명적인 결함(v1.0.0.13)을 추가로 발견하게 되었습니다. 실제 서비스를 이용하고 클론해 가시는 분들을 위해서는 글을 작성하는 것보다 <strong>코드의 결함을 잡고 깃허브에 최우선으로 패치 배포하는 것이 급선무</strong>라 판단했습니다.</p>
<p>긴급 수정과 릴리즈 배포를 먼저 마무리 짓고 안정성을 확보한 뒤 글을 정리하다 보니, 결과적으로 11부터 13까지 세 버전을 아우르는 긴 호흡의 개발로그로 찾아뵙게 되었습니다.</p>
<h3 id="hub-기능-일시-중단-및-재오픈-안내">Hub 기능 일시 중단 및 재오픈 안내</h3>
<p>현재 블로그의 허브(Hub) 기능은 <strong>구글 애드센스 심사 진행을 위해 잠시 닫아둔 상태</strong>입니다. 심사 과정에서 사이트 구조의 일관성을 유지하고 불필요한 색인 혼선을 방지하기 위한 조치입니다.</p>
<p>애드센스 심사가 완료되는 대로 허브 시스템 전반에 대한 추가 점검과 코드 정비를 마친 뒤 다시 정상 오픈할 예정이니, 허브 기능을 기다려주시는 분들께서는 조금만 양해를 부탁드립니다.</p>
<hr>
<p>이번 v1.0.0.11부터 v1.0.0.13까지의 여정은 새 기능을 무작정 늘리기보다, <strong>Cloudflare Free Tier라는 제약 조건 속에서 실질적인 성능과 안정성, 웹 표준 준수율을 끌어올리는 데 집중한 과정</strong>이었습니다.</p>
<p>결과적으로 3개 테마를 자유롭게 전환할 수 있는 디자인 유연성을 확보하면서도, 데이터베이스 읽기/쓰기 효율화와 모바일 CLS 0.000, 그리고 SEO 정규화까지 깔끔하게 마무리할 수 있었습니다.</p>
<p>SKBE의 전체 소스 코드는 공식 GitHub 저장소에 공개되어 있으니, SvelteKit 기반 서버리스 블로그 구축이나 성능 최적화에 관심 있으신 분들께 작은 참고가 되길 바랍니다.</p>
]]></content:encoded>
            <category>개발로그</category>
            <enclosure url="https://sveltekitblog.com/images/posts/skbe-engine-optimization-v11-v13-guide/desktop/img-devlog-skbe-engine-optimization-v11-v13-guide-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[SKBE v1.0.0.10 업데이트: 포스트 언어별 탭, 허브 연동 보완 및 에디터 개선]]></title>
            <link>https://testblog-6br.pages.dev/devlog/release-v1-0-0-10-post-management-tabs-and-hub-sync</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/release-v1-0-0-10-post-management-tabs-and-hub-sync</guid>
            <pubDate>Wed, 26 Aug 2026 06:12:14 GMT</pubDate>
            <description><![CDATA[다국어 글이 늘어남에 따라 포스트 관리에서 언어별로 모아볼 수 있는 탭 필터를 추가하고, 블로그 허브 연동 안정성 및 에디터 서식 유실 방지 처리를 보완한 v1.0.0.10 릴리즈 내용입니다.]]></description>
            <content:encoded><![CDATA[<h1 id="스벨트킷-블로그-엔진-v10010-업데이트-안내">스벨트킷 블로그 엔진 v1.0.0.10 업데이트 안내</h1>
<p>스벨트킷 블로그 엔진(SvelteKit Blog Engine) v1.0.0.10 버전 업데이트 내용입니다.</p>
<p>다국어로 글을 계속 작성하다 보니 관리자 페이지에서 특정 언어의 글만 따로 모아서 보거나, 어떤 언어의 번역이 덜 작성되었는지 확인하기 불편한 점이 있었습니다. 이번 버전에서는 이 부분을 개선하기 위해 <strong>포스트 관리 페이지의 동적 언어 탭</strong>을 추가하고, <strong>블로그 허브 연동 프로토콜 보완</strong>, <strong>에디터 서식 유실 방지</strong>, <strong>배포 명령어 간소화</strong> 작업을 진행했습니다.</p>
<hr>
<h2 id="1-포스트-관리-페이지-언어별-필터-탭-추가">1. 포스트 관리 페이지 언어별 필터 탭 추가</h2>
<p>기존에는 전체 포스트가 작성일 순으로만 나열되어 있어, 한국어, 영어, 일본어 등 여러 언어가 섞여 있을 때 관리하기가 번거로웠습니다.<br><img src="https://sveltekitblog.com/images/posts/release-v1-0-0-10-post-management-tabs-and-hub-sync/desktop/img-devlog-release-v1-0-0-10-post-management-tabs-and-hub-sync-ko-002.webp" alt="img-devlog-release-v1-0-0-10-post-management-tabs-and-hub-sync-ko-002"></p>
<h3 id="주요-변경-사항">주요 변경 사항</h3>
<ul>
<li><strong>동적 언어 감지 및 카운트 배지</strong>: 코드로 언어 목록을 고정하지 않고, 데이터베이스에 등록된 언어와 실제 작성된 포스트 데이터를 종합하여 탭을 동적으로 만듭니다. 각 탭 옆에는 작성된 글 개수가 실시간으로 표시됩니다.</li>
<li><strong>글이 없는 언어 선택 시 안내</strong>: 아직 작성된 글이 없는 언어 탭(예: <code>English (0)</code>)을 누르면 단순 빈 화면 대신 안내 문구와 함께 해당 언어로 바로 글을 작성할 수 있는 링크 버튼이 표시됩니다.</li>
<li><strong>단일 언어 환경 고려</strong>: 다국어를 쓰지 않고 단 1개의 언어로만 운영되는 블로그에서는 불필요하게 탭 바가 나오지 않도록 처리했습니다.</li>
<li><strong>목록 내 언어 뱃지 및 페이지네이션 연동</strong>: 테이블의 각 제목 옆에 언어 코드(<code>KR</code>, <code>EN</code>, <code>JA</code> 등)를 표시하여 한눈에 구분할 수 있게 했고, 탭을 바꿀 때 자동으로 1페이지로 넘어가도록 처리했습니다.</li>
</ul>
<hr>
<h2 id="2-블로그-허브hub-연동-프로토콜-보완">2. 블로그 허브(Hub) 연동 프로토콜 보완</h2>
<p>중앙 피드 플랫폼인 스벨트킷 블로그 허브(<code>hub.sveltekitblog.com</code>)로 글을 전송할 때 발생하던 자잘한 엣지 케이스들을 정리했습니다.</p>
<h3 id="주요-보완-내용">주요 보완 내용</h3>
<ul>
<li><strong>서론 1,500자 HTML 자동 추출</strong>: 마크다운이든 비주얼 HTML이든, 피드 카드에 적합한 서론 1,500자 분량의 HTML을 서버 측에서 정제하여 허브로 전달합니다.</li>
<li><strong>이미지 상대경로 자동 변환</strong>: 본문에 들어간 <code>/images/posts/...</code> 같은 내부 상대 경로 이미지를 사이트 대표 도메인이 포함된 완전한 절대 URL(<code>https://도메인/images/...</code>)로 자동 변환하여, 허브 피드에서 썸네일과 본문 이미지가 깨지지 않도록 했습니다.</li>
<li><strong>글 수정 시 최초 발행일 유지</strong>: 이미 올라간 글을 수정할 때 수정일시로 덮어써지지 않고, 최초 발행일(<code>published_at</code>)을 보존하여 허브 피드 타임라인의 시간 순서가 꼬이지 않도록 수정했습니다.</li>
<li><strong>허브 자동 제출 기본값 설정</strong>: 관리자 설정(<code>⚙️ 설정</code>)에 &#39;글 작성 시 허브 자동 제출 기본 활성화&#39; 토글을 추가하여, 새 글을 쓸 때 매번 체크박스를 따로 누르지 않아도 되도록 개선했습니다.</li>
</ul>
<hr>
<h2 id="3-포스트-에디터-서식-유실-방지-가드">3. 포스트 에디터 서식 유실 방지 가드</h2>
<p>포스트 에디터에서 모드를 전환하거나 에디터 종류를 바꿀 때 작성 중이던 내용이 실수로 날아가는 일을 방지하는 안전장치를 추가했습니다.</p>
<ul>
<li><strong>HTML ➔ Visual 모드 전환 경고</strong>: HTML 에디터에서 커스텀 테이블(표)이나 인라인 스타일을 작성한 뒤 Visual 모드로 돌아갈 경우, 티프탭(Tiptap) 파서에 의해 비표준 태그가 정제되어 지워질 수 있습니다. 이런 손실을 막기 위해 모드 전환 전 확인 알림창을 띄우도록 했습니다.</li>
<li><strong>에디터 유형(HTML ↔ Markdown) 변경 락</strong>: 이미 저장된 글의 경우 데이터 보호를 위해 에디터 종류를 잠그고, 변경을 시도할 때 경고창을 통해 데이터 손실을 사전에 인지할 수 있도록 했습니다.</li>
</ul>
<hr>
<h2 id="4-다중-계정-배포deploy-multi-cli-옵션-간소화">4. 다중 계정 배포(deploy-multi) CLI 옵션 간소화</h2>
<p>여러 계정을 배포할 때 사용하는 <code>deploy-multi</code> 스크립트의 인자 입력 방식을 직관적으로 바꿨습니다.</p>
<pre><code class="language-bash"># 이전 방식
npm run deploy:multi -- myaccount --admin-only

# 변경된 방식 (간결한 옵션 지원)
npm run deploy:multi -- myaccount admin   # 어드민만 배포
npm run deploy:multi -- myaccount blog    # 블로그만 배포
npm run deploy:multi -- myaccount         # 전체(블로그+어드민) 배포
</code></pre>
<p>기존에 사용하던 <code>--admin-only</code>, <code>--blog-only</code> 플래그도 그대로 작동합니다.</p>
<hr>
<h2 id="적용-방법">적용 방법</h2>
<p>저장소에서 최신 코드를 pull 받은 뒤 배포를 진행하시면 됩니다.</p>
<pre><code class="language-bash"># 최신 코드 받기
git pull origin main

# 배포 실행 (예시)
npm run deploy:admin
# 또는 멀티 배포 시
npm run deploy:multi -- &lt;계정명&gt; admin
</code></pre>
<p>블로그를 직접 운영하면서 필요한 기능이나 수정할 점이 보이면 계속해서 다듬어나갈 예정입니다. 감사합니다.</p>
]]></content:encoded>
            <category>개발로그</category>
            <enclosure url="https://sveltekitblog.com/images/posts/release-v1-0-0-10-post-management-tabs-and-hub-sync/desktop/img-devlog-release-v1-0-0-10-post-management-tabs-and-hub-sync-ko-002.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[스벨트킷 블로그 허브(Hub) 연동 및 자동 발행 완벽 가이드]]></title>
            <link>https://testblog-6br.pages.dev/detail-manual/blog-hub-integration-manual</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/detail-manual/blog-hub-integration-manual</guid>
            <pubDate>Tue, 25 Aug 2026 22:28:22 GMT</pubDate>
            <description><![CDATA[스벨트킷 블로그 허브(hub.sveltekitblog.com)와 내 독립 블로그를 연동하여 트래픽을 확장하고, 글 작성·수정·삭제 라이프사이클을 안전하게 자동화하는 완벽 가이드입니다.]]></description>
            <content:encoded><![CDATA[<h1 id="스벨트킷-블로그-허브hub-연동-및-자동-발행-완벽-가이드">🌐 스벨트킷 블로그 허브(Hub) 연동 및 자동 발행 완벽 가이드</h1>
<p>독립형 개인 블로그를 운영할 때 가장 큰 고민은 **&quot;어떻게 새로운 독자에게 내 글을 알릴 것인가?&quot;**입니다. 상용 플랫폼과 달리 독립 블로그는 데이터와 디자인의 완전한 자유를 누릴 수 있지만, 초기 유입과 검색 노출에는 많은 시간과 노력이 필요합니다.</p>
<p>**<a href="https://hub.sveltekitblog.com/">스벨트킷 블로그 통합 허브(hub.sveltekitblog.com)</a>**는 이러한 독립 블로그들의 발견성(Discovery) 한계를 극복하고, 블로거들이 함께 트래픽과 통계를 나누며 성장할 수 있도록 설계된 분산 연합 피드 플랫폼입니다.</p>
<p>본 가이드에서는 <strong>API Key 발급부터 블로그 관리자 연동, 글 작성·수정·삭제 라이프사이클 및 애드센스 시너지</strong>까지 허브 연동의 모든 과정을 상세히 안내합니다.</p>
<hr>
<h2 id="1-허브-연동의-핵심-특징-및-원리">1. 🎯 허브 연동의 핵심 특징 및 원리</h2>
<p><img src="https://sveltekitblog.com/images/posts/blog-hub-integration-manual/desktop/img-detail-manual-blog-hub-integration-manual-ko-001.webp" alt="img-detail-manual-blog-hub-integration-manual-ko-001"></p>
<ol>
<li><strong>데이터의 완전한 독립성 &amp; 소유권</strong>:<ul>
<li>글의 원본 본문과 모든 DB 데이터는 내 Cloudflare D1에 온전히 보관됩니다.</li>
<li>허브에는 카드 노출을 위한 **표준 HTML 서론(1,000~1,500자)**과 메타데이터(제목, 썸네일, 슬러그 등)만 안전하게 전송됩니다.</li>
</ul>
</li>
<li><strong>이미지 절대 경로 자동 변환</strong>:<ul>
<li>마크다운이나 HTML에 포함된 상대 경로 이미지(<code>/images/...</code>)는 내 블로그의 도메인을 포함한 절대 URL(<code>https://...</code>)로 자동 변환되어 허브 피드에 깨짐 없이 선명하게 표시됩니다.</li>
</ul>
</li>
<li><strong>비동기 논블로킹(Non-blocking) 장애 격리</strong>:<ul>
<li>허브 서버의 네트워크 지연이나 일시적 오류가 발생하더라도, <strong>내 블로그의 글 저장 트랜잭션은 100% 정상 완료</strong>되도록 안전하게 보호됩니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-1단계-허브-api-key-발급-및-블로그-설정">2. 🔑 1단계: 허브 API Key 발급 및 블로그 설정</h2>
<h3 id="허브-플랫폼에서-api-key-발급">① 허브 플랫폼에서 API Key 발급</h3>
<p><img src="https://sveltekitblog.com/images/posts/blog-hub-integration-manual/desktop/img-detail-manual-blog-hub-integration-manual-ko-002.webp" alt="img-detail-manual-blog-hub-integration-manual-ko-002"></p>
<ol>
<li>**<a href="https://hub.sveltekitblog.com/register-site">스벨트킷 블로그 허브의 &#39;내 블로그 연동 (API 발급)&#39;</a>**에 접속합니다.</li>
<li>내 블로그 대표 URL(예: <code>https://myblog.com</code>)을 등록하고, 고유 발급된 <strong>Site API Key</strong>(<code>sk_board_...</code> 또는 <code>sk_hub_...</code>)를 복사합니다.</li>
</ol>
<h3 id="블로그-관리자-페이지-설정-등록">② 블로그 관리자 페이지(<code>⚙️ 설정</code>) 등록</h3>
<ol>
<li>내 블로그 관리자 페이지 접속 후 <strong>[설정]</strong> 메뉴로 이동합니다.</li>
<li><strong>스벨트킷블로그 허브(Hub) 연동 설정</strong> 섹션을 찾습니다.<br><img src="https://sveltekitblog.com/images/posts/blog-hub-integration-manual/desktop/img-detail-manual-blog-hub-integration-manual-ko-003.webp" alt="img-detail-manual-blog-hub-integration-manual-ko-003"></li>
</ol>
<pre><code class="language-text">[ 스벨트킷블로그 허브(Hub) 연동 설정 ]
* 허브 플랫폼 URL : https://hub.sveltekitblog.com (고정)
* 허브 발급 API Key : sk_hub_live_... (발급받은 키 입력)
* 🌐 글 작성 시 허브 동시 발행 기본 활성화 : [ON / OFF 토글]
</code></pre>
<ol start="3">
<li><code>글 작성 시 허브 동시 발행 기본 활성화</code>를 켜두시면 새 글을 작성할 때마다 자동으로 연동 체크박스가 활성화됩니다.</li>
<li>하단의 <strong>[설정 저장하기]</strong> 버튼을 눌러 설정을 완료합니다.</li>
</ol>
<hr>
<h2 id="3-2단계-포스트-에디터에서-허브-동시-발행하기">3. ✍️ 2단계: 포스트 에디터에서 허브 동시 발행하기</h2>
<p>글 작성 및 수정 화면(<code>새 글 작성</code> 또는 <code>글 수정</code>)의 메타데이터 사이드바 하단에 <strong>허브 자동 제출 체크박스</strong>가 제공됩니다.<br><img src="https://sveltekitblog.com/images/posts/blog-hub-integration-manual/desktop/img-detail-manual-blog-hub-integration-manual-ko-004.webp" alt="img-detail-manual-blog-hub-integration-manual-ko-004"></p>
<pre><code class="language-text">[☑️] 🌐 스벨트킷블로그 허브(hub.sveltekitblog.com)에 자동 제출
     체크 시 글이 발행될 때 허브 피드로 요약 카드가 자동 전송됩니다.
     체크를 해제하면 허브 피드에서 &quot;숨김&quot; 처리되어 추천(좋아요) 통계가 안전하게 보존되며,
     허브에서 완전히 삭제하려면 블로그에서 포스트를 삭제해야 합니다.
</code></pre>
<ul>
<li><strong>공개 글 발행 (<code>Status: Published</code>)</strong>:<ul>
<li>체크박스가 켜진 상태로 글을 저장하면, D1 DB 저장과 동시에 허브로 표준 HTML 서론과 썸네일이 전송되어 허브 피드에 즉시 노출됩니다.</li>
</ul>
</li>
<li><strong>임시저장 (<code>Status: Draft</code>)</strong>:<ul>
<li>상태가 임시저장인 글은 체크박스를 켜두더라도 허브 피드에 공개되지 않습니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="4-3단계-글-수정-url-변경-비공개-삭제-라이프사이클">4. 🔄 3단계: 글 수정, URL 변경, 비공개, 삭제 라이프사이클</h2>
<p>블로그를 운영하다 보면 글의 내용을 보강하거나, 카테고리를 변경하고, 글을 비공개하거나 삭제하는 상황이 발생합니다. 블로그 엔진은 이러한 상황을 자동으로 감지하여 허브와 완벽하게 동기화합니다.</p>
<table>
<thead>
<tr>
<th align="left">상황</th>
<th align="left">블로그 동작</th>
<th align="left">허브(Hub) 동기화 동작</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>글 내용 단순 수정</strong></td>
<td align="left">본문 수정 후 저장</td>
<td align="left"><strong>최초 발행일(<code>published_at</code>)을 보존</strong>한 상태로 카드 내용 및 썸네일만 최신화 (피드 상단으로 불필요하게 튀지 않음)</td>
</tr>
<tr>
<td align="left"><strong>슬러그 / 카테고리 변경</strong></td>
<td align="left">URL 변경 후 저장</td>
<td align="left">허브에 등록되어 있던 **이전 URL 카드를 자동 삭제(<code>DELETE</code>)**하고, <strong>새 URL로 신규 등록(<code>POST</code>)</strong> (깨진 404 링크 방지)</td>
</tr>
<tr>
<td align="left"><strong>비공개 전환 / 체크 해제</strong></td>
<td align="left">Draft 전환 또는 체크 해제</td>
<td align="left">허브 피드에서 <strong>숨김(<code>hidden</code>) 처리</strong> (독자들이 남긴 <strong>추천/좋아요 통계는 영구 보존</strong>)</td>
</tr>
<tr>
<td align="left"><strong>블로그에서 포스트 삭제</strong></td>
<td align="left">관리자에서 글 완전 삭제</td>
<td align="left">허브에서도 <strong>해당 포스트 카드를 완전 삭제(<code>DELETE</code>)</strong></td>
</tr>
</tbody></table>
<hr>
<h2 id="5-구글-애드센스-승인-준비-및-시너지-효과">5. 💰 구글 애드센스 승인 준비 및 시너지 효과</h2>
<p>허브 연동을 통해 유입되는 트래픽은 구글 애드센스 승인 심사 및 수익 창출에 매우 긍정적인 영향을 줍니다.</p>
<ol>
<li><strong>초기 유효 트래픽 확보</strong>:<ul>
<li>구글 봇은 방문자가 전혀 없는 블로그보다 실제 활성 독자가 유입되는 사이트를 훨씬 빠르게 크롤링하고 높게 평가합니다.</li>
</ul>
</li>
<li><strong><code>ads.txt</code> 완벽 자동 서빙</strong>:<ul>
<li>관리자 설정의 <code>ads_txt</code> 입력창에 애드센스 승인 코드(<code>google.com, pub-..., DIRECT, f08c47fec0942fa0</code>)를 넣어두면 <code>https://내도메인.com/ads.txt</code>로 즉시 서빙됩니다.</li>
</ul>
</li>
<li><strong>콘텐츠 품질 가이드</strong>:<ul>
<li>애드센스 1차 승인을 위해 <strong>1,500자 이상의 양질의 IT/기술/정보성 글을 15~20편 이상</strong> 꾸준히 허브와 함께 발행하시는 것을 권장합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="6-마치며">6. 🏁 마치며</h2>
<p>스벨트킷 블로그 허브 연동을 통해 나만의 독립 블로그가 가진 데이터 통제권을 온전히 지키면서, 더 많은 독자들과 연결되는 강력한 시너지를 경험해 보세요!</p>
]]></content:encoded>
            <category>상세 메뉴얼</category>
            <enclosure url="https://sveltekitblog.com/images/posts/blog-hub-integration-manual/desktop/img-detail-manual-blog-hub-integration-manual-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[새 릴리즈 코드를 가장 안전하게 올리는 법: Cloudflare Pages 다계정 배포 자동화]]></title>
            <link>https://testblog-6br.pages.dev/general-guide/cloudflare-pages-multi-account-deploy-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/general-guide/cloudflare-pages-multi-account-deploy-guide</guid>
            <pubDate>Sun, 23 Aug 2026 13:12:22 GMT</pubDate>
            <description><![CDATA[새로운 릴리즈 코드가 나왔을 때 테스트 계정에서 먼저 검증하고 본 계정에 안전하게 반영하는 방법! 캐시 충돌 없는 Cloudflare Pages 원클릭 다계정 배포 파이프라인을 소개합니다.]]></description>
            <content:encoded><![CDATA[<h1 id="새-릴리즈-코드를-가장-안전하게-올리는-법-cloudflare-pages-다계정-배포-자동화">🚀 새 릴리즈 코드를 가장 안전하게 올리는 법: Cloudflare Pages 다계정 배포 자동화</h1>
<p>깃허브 저장소에 새로운 릴리즈 버전이 올라왔을 때, 최신 코드를 내려받아 내 블로그에 배포하다 보면 한 가지 조심스러운 고민이 생깁니다.</p>
<blockquote>
<p><strong>&quot;새 릴리즈의 엔진 코어와 데이터베이스 마이그레이션이 실제 운영 중인 내 본 계정 블로그에 문제없이 잘 적용될까?&quot;</strong></p>
</blockquote>
<p>스벨트킷 블로그 엔진은 레이아웃, 컬러, 폰트 같은 디자인 설정과 작성한 글이 모두 <strong>D1 데이터베이스에 안전하게 보존</strong>되므로, 코드를 업데이트하더라도 어드민에서 꾸며둔 블로그 UI가 초기화되지는 않습니다.</p>
<p>하지만 새 버전에서 추가된 코어 기능이나 D1 스키마 변경, 배포 스크립트가 내 Cloudflare 환경에 정상적으로 잘 배포되는지 사전에 확인해보고 싶을 때가 있습니다. 이때 가장 안전한 방법은 <strong>무료 Cloudflare 보조 계정(테스트 인스턴스)에 먼저 배포해 본 뒤, 정상 가동을 확인하고 본 계정에 올리는 것</strong>입니다.</p>
<p>기존에는 Cloudflare 기본 도구(Wrangler)로 여러 계정을 오가려면 매번 브라우저 로그인을 다시 하거나 D1/KV ID를 일일이 수동 교체해야 했고, 로컬 캐시가 엉켜 배포가 꼬이기도 했습니다. 이번 글에서는 이러한 번거로움 없이 **새 릴리즈 코드를 테스트 계정에서 먼저 검증하고 본 계정으로 안전하게 배포하는 &#39;다계정 원클릭 배포 파이프라인&#39;**을 정리해 보았습니다.</p>
<hr>
<h2 id="1-실전-안전-배포-워크플로우">🔄 1. 실전 안전 배포 워크플로우</h2>
<p>다계정 배포 시스템을 활용하면 다음과 같이 안전한 배포 사이클을 구축할 수 있습니다:<br><img src="https://sveltekitblog.com/images/posts/cloudflare-pages-multi-account-deploy-guide/desktop/img-general-guide-cloudflare-pages-multi-account-deploy-guide-ko-001.webp" alt="img-general-guide-cloudflare-pages-multi-account-deploy-guide-ko-001"></p>
<hr>
<h2 id="2-준비물-cloudflare-api-토큰-발급-계정당-최초-1회">🔑 2. 준비물: Cloudflare API 토큰 발급 (계정당 최초 1회)</h2>
<p>브라우저 로그인 팝업 없이 터미널에서 백그라운드로 안전하게 배포하기 위해, 각 Cloudflare 계정에서 <strong>API 토큰</strong>을 딱 한 번만 발급받아 둡니다.</p>
<h3 id="2-1-안전한-최소-권한-api-토큰-만들기">2-1. 안전한 최소 권한 API 토큰 만들기</h3>
<ol>
<li><a href="https://dash.cloudflare.com">Cloudflare 대시보드</a> 로그인 ➔ 우측 상단 <strong>[내 프로필(My Profile)]</strong> ➔ <strong>[API 토큰(API Tokens)]</strong> 메뉴로 이동합니다.</li>
<li>**[토큰 만들기(Create Token)]**를 누르고, 목록에서 <strong><code>Cloudflare Pages 편집(Edit Cloudflare Pages)</code></strong> 템플릿의 **[템플릿 사용]**을 클릭합니다.</li>
<li><strong>권한(Permissions)</strong> 섹션 하단의 **<code>+ 더 추가(+ Add more)</code>**를 눌러 다음 2가지 권한을 추가합니다:<ul>
<li><strong><code>계정(Account)</code></strong> - <strong><code>D1</code></strong> - <strong><code>편집(Edit)</code></strong> <em>(D1 데이터베이스 스키마 자동 동기화용)</em></li>
<li><strong><code>계정(Account)</code></strong> - <strong><code>Workers KV Storage</code></strong> - <strong><code>편집(Edit)</code></strong> <em>(이미지 저장소 바인딩용)</em></li>
</ul>
</li>
</ol>
<blockquote>
<p><strong>💡 보안 팁</strong>: 모든 권한을 다 주는 전역 키 대신, 블로그 빌드와 배포에 꼭 필요한 **Pages, D1, KV 3개 리소스만 권한을 주는 방식(최소 권한 원칙)**이 가장 안전합니다.</p>
</blockquote>
<ol start="4">
<li>**계정 리소스(Account Resources)**에서 해당 계정을 선택하고, **[계속해서 요약 보기] ➔ [토큰 만들기]**를 누릅니다.</li>
<li>발급된 <strong>API 토큰 문자열</strong>을 복사해 둡니다.</li>
</ol>
<h3 id="2-2-32자리-계정-id-확인">2-2. 32자리 계정 ID 확인</h3>
<p>대시보드 접속 후 브라우저 주소창(URL)에서 32자리 문자열을 복사합니다:</p>
<pre><code class="language-text">https://dash.cloudflare.com/1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d/workers-and-pages
                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                             (이 부분이 Account ID입니다)
</code></pre>
<hr>
<h2 id="3-백업-파일로-1초-만에-계정-등록하기-deploysync">📦 3. 백업 파일로 1초 만에 계정 등록하기 (<code>deploy:sync</code>)</h2>
<p>새 계정의 D1 ID, KV ID, 프로젝트명을 복잡하게 손으로 적을 필요 없이, <strong>어드민의 기본 기능인 &#39;배포 설정 백업&#39; 파일</strong>을 이용해 한 번에 등록합니다.</p>
<ol>
<li>배포 대상 블로그의 어드민 페이지 ➔ **[환경설정] ➔ [데이터 관리]**에서 **[배포 설정 다운로드]**를 클릭합니다.</li>
<li>다운로드된 파일 이름을 계정 식별자로 변경하여 프로젝트 루트 폴더에 넣습니다:<ul>
<li>예: 테스트 계정 백업 파일 ➔ <code>wrangler.backup.test.json</code></li>
<li>예: 본 계정 백업 파일 ➔ <code>wrangler.backup.main.json</code></li>
</ul>
</li>
<li>터미널에서 동기화 명령을 실행합니다:</li>
</ol>
<pre><code class="language-bash">npm run deploy:sync
</code></pre>
<pre><code class="language-text">======================================================
✅ [.deploy-accounts.json] 계정 설정 동기화 완료!
======================================================
📋 [현재 등록된 계정 목록]
  - main (Main Blog Account): 🟢 준비 완료
  - test (Test Blog Account): 🟡 token/accountId 입력 필요
</code></pre>
<ol start="4">
<li>생성된 <code>.deploy-accounts.json</code> 파일을 열고, 발급받은 <code>token</code>과 <code>accountId</code>를 입력해 줍니다:</li>
</ol>
<pre><code class="language-json">{
  &quot;test&quot;: {
    &quot;name&quot;: &quot;Test Staging Account&quot;,
    &quot;token&quot;: &quot;실제_발급받은_API_토큰&quot;,
    &quot;accountId&quot;: &quot;실제_32자리_계정_ID&quot;,
    &quot;blogProject&quot;: &quot;test-blog-web&quot;,
    &quot;adminProject&quot;: &quot;test-blog-admin&quot;,
    &quot;d1&quot;: {
      &quot;BLOG_DB&quot;: { &quot;name&quot;: &quot;test-blog-db&quot;, &quot;id&quot;: &quot;11111111-2222-3333-4444-555555555555&quot; },
      &quot;USER_DB&quot;: { &quot;name&quot;: &quot;test-user-db&quot;, &quot;id&quot;: &quot;66666666-7777-8888-9999-000000000000&quot; }
    },
    &quot;kv&quot;: {
      &quot;IMAGES_KV&quot;: &quot;aaaaaaaaaabbbbbbbbbbccccccccccdd&quot;
    }
  }
}
</code></pre>
<blockquote>
<p><strong>안전 보장</strong>: <code>deploy:sync</code>는 이미 입력해 둔 토큰과 계정 ID를 절대 지우지 않고 안전하게 보존(Merge)합니다.</p>
</blockquote>
<hr>
<h2 id="4-릴리즈-업데이트-실전-배포-시나리오">🚀 4. 릴리즈 업데이트 실전 배포 시나리오</h2>
<p>이제 새 릴리즈가 떴을 때 터미널에서 명령어 한 줄로 안전하게 배포할 수 있습니다.</p>
<h3 id="시나리오-a-릴리즈-테스트-계정에-먼저-쏴보기-권장">시나리오 A. 릴리즈 테스트 계정에 먼저 쏴보기 (권장)</h3>
<p>새 버전의 엔진 코어와 마이그레이션을 테스트 계정에 먼저 올려서 Blog(웹)와 Admin(관리자)이 정상 가동되는지 확인합니다.</p>
<pre><code class="language-bash">npm run deploy:multi test
</code></pre>
<h3 id="시나리오-b-검증-완료-후-본-계정에-안전-배포">시나리오 B. 검증 완료 후 본 계정에 안전 배포</h3>
<p>테스트 인스턴스에서 새 기능과 글 작성 등이 정상 작동하는 것을 확인했다면 본 계정에 배포합니다.</p>
<pre><code class="language-bash">npm run deploy:multi main
</code></pre>
<h3 id="시나리오-c-여러-개의-블로그를-한-번에-일괄-판올림">시나리오 C. 여러 개의 블로그를 한 번에 일괄 판올림</h3>
<p>메인 블로그 외에 서브 블로그를 함께 운영하고 있다면, 새 릴리즈 코드를 등록된 모든 계정에 한 번에 일괄 배포할 수 있습니다.</p>
<pre><code class="language-bash">npm run deploy:multi -- --all
</code></pre>
<h3 id="시나리오-d-블로그-웹-또는-관리자-앱만-선별-배포">시나리오 D. 블로그 웹 또는 관리자 앱만 선별 배포</h3>
<p>블로그 프론트엔드 코드만 수정되었거나 어드민 기능만 수정되었을 때는 필요한 앱만 골라서 배포할 수 있습니다.</p>
<pre><code class="language-bash"># test 계정의 Blog(웹)만 배포
npm run deploy:multi test blog

# test 계정의 Admin(관리자)만 배포
npm run deploy:multi test admin
</code></pre>
<hr>
<h2 id="5-계정-간-충돌을-원천-차단하는-3단계-샌드박스-설계">🛡️ 5. 계정 간 충돌을 원천 차단하는 3단계 샌드박스 설계</h2>
<p>여러 Cloudflare 계정을 오가며 배포할 때 가장 위험한 문제는 <strong>로컬 캐시 엉킴으로 인해 엉뚱한 계정에 사이트가 배포되는 사고</strong>입니다. Wrangler는 로컬 PC에 이전 계정 세션을 캐시해 두는 습성이 있기 때문입니다.</p>
<p>이를 원천 차단하기 위해 배포 러너에는 <strong>3단계 샌드박스 격리 시스템</strong>이 적용되어 있습니다:<br><img src="https://sveltekitblog.com/images/posts/cloudflare-pages-multi-account-deploy-guide/desktop/img-general-guide-cloudflare-pages-multi-account-deploy-guide-ko-002.webp" alt="img-general-guide-cloudflare-pages-multi-account-deploy-guide-ko-002"></p>
<ol>
<li><strong>임시 세션 샌드박스</strong>: 배포가 진행되는 동안 <code>APPDATA</code>를 임시 디렉토리로 격리하여, 내 컴퓨터의 기존 브라우저 로그인 세션에 영향을 주지 않습니다.</li>
<li><strong>원자적(In-Memory) 설정 복구</strong>: 배포 순간에만 해당 계정의 <code>wrangler.json</code>을 적용하고, 배포가 끝나면 <code>finally</code> 블록에서 기존 파일로 100% 원래대로 복구합니다.</li>
<li><strong>로컬 캐시 자동 청소</strong>: 배포 직후 로컬 캐시를 비워주므로, 평소에 쓰시던 단일 배포(<code>npm run deploy:blog</code>, <code>deploy:admin</code>)와 다계정 배포를 번갈아 실행해도 세션이 꼬이지 않습니다.</li>
</ol>
<hr>
<h2 id="6-주요-명령어-요약">📋 6. 주요 명령어 요약</h2>
<table>
<thead>
<tr>
<th align="left">용도</th>
<th align="left">명령어</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>계정 설정 자동 동기화</strong></td>
<td align="left"><code>npm run deploy:sync</code></td>
</tr>
<tr>
<td align="left"><strong>특정 계정 배포 (Blog + Admin)</strong></td>
<td align="left"><code>npm run deploy:multi &lt;계정명&gt;</code></td>
</tr>
<tr>
<td align="left"><strong>특정 계정 Blog만 선별 배포</strong></td>
<td align="left"><code>npm run deploy:multi &lt;계정명&gt; blog</code></td>
</tr>
<tr>
<td align="left"><strong>특정 계정 Admin만 선별 배포</strong></td>
<td align="left"><code>npm run deploy:multi &lt;계정명&gt; admin</code></td>
</tr>
<tr>
<td align="left"><strong>등록된 모든 계정 일괄 판올림</strong></td>
<td align="left"><code>npm run deploy:multi -- --all</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="7-마치며">💡 7. 마치며</h2>
<p>어드민에서 직접 꾸며둔 디자인 설정과 소중한 글 데이터는 D1 데이터베이스에 그대로 보존되면서, 새로운 엔진 릴리즈 버전이 나올 때마다 테스트 인스턴스를 통해 안심하고 업데이트할 수 있는 배포 파이프라인이 완성되었습니다.</p>
<p>테스트 계정을 활용한 안전 배포 워크플로우로 나만의 블로그를 더욱 안정적이고 편안하게 운영해 보시기 바랍니다.</p>
]]></content:encoded>
            <category>통합 가이드</category>
            <enclosure url="https://sveltekitblog.com/images/posts/cloudflare-pages-multi-account-deploy-guide/desktop/img-general-guide-cloudflare-pages-multi-account-deploy-guide-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[[개발 일지] 스벨트킷 블로그 엔진 v1.0.0.9 릴리즈 및 주요 변경 사항]]></title>
            <link>https://testblog-6br.pages.dev/devlog/devlog-release-v1009</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/devlog-release-v1009</guid>
            <pubDate>Sun, 23 Aug 2026 12:48:52 GMT</pubDate>
            <description><![CDATA[스벨트킷 블로그 엔진 v1.0.0.9 릴리즈! 다계정 원클릭 Cloudflare Pages 배포, 스벨트킷 블로그 허브 피드 연동, 구글 애드센스 승인을 위한 SEO 전면 최적화 및 i18n 사전 동기화 개선 사항을 소개합니다.]]></description>
            <content:encoded><![CDATA[<h1 id="스벨트킷-블로그-엔진-v1009-릴리즈">🚀 스벨트킷 블로그 엔진 v1.0.0.9 릴리즈</h1>
<p>v1.0.0.8에 이어 진행된 이번 v1.0.0.9 릴리즈에서는 <strong>다계정 원클릭 Cloudflare Pages 통합 배포</strong>, <strong><a href="https://hub.sveltekitblog.com/">SvelteKit Blog Hub BLOG FEED 스벨트킷 블로그 통합 피드</a> 연동</strong>, <strong>구글 애드센스 승인을 위한 SEO/사이트맵 전면 최적화 및 RSS/사이트맵 디자인 통일</strong>, 그리고 <strong>다국어(i18n) 사전 완성도 개선 및 배포 동기화 편의성 향상</strong>이 도입되었습니다.</p>
<hr>
<h3 id="1-다계정-스마트-자동-동기화-및-원클릭-cloudflare-pages-배포">1. 🌐 다계정 스마트 자동 동기화 및 원클릭 Cloudflare Pages 배포</h3>
<ul>
<li><strong><code>wrangler.backup.json</code> 기반 스마트 자동 동기화 (<code>npm run deploy:sync</code>)</strong>:<ul>
<li>관리자에서 다운로드한 백업 파일(<code>wrangler.backup*.json</code>)을 루트에 넣고 동기화 명령을 실행하면 D1 ID, KV ID, 프로젝트명을 자동 감지하여 <code>.deploy-accounts.json</code>에 완벽하게 등록합니다.</li>
<li>사용자가 입력해 둔 API 토큰과 계정 ID는 100% 안전하게 보존(Merge)됩니다.</li>
</ul>
</li>
<li><strong>원클릭 다계정 배포 러너 (<code>npm run deploy:multi</code>)</strong>:<ul>
<li><code>npm run deploy:multi -- &lt;계정명&gt;</code> 커맨드 한 번으로 해당 계정의 **Blog + Admin (2개 CFP)**를 연속 자동 배포합니다.</li>
<li><code>--blog-only</code>, <code>--admin-only</code> 선택 배포 및 등록된 모든 계정 일괄 배포(<code>--all</code>)를 지원합니다.</li>
</ul>
</li>
<li><strong>완벽한 캐시 샌드박스 격리 및 자동 청소</strong>:<ul>
<li><code>APPDATA: .wrangler-multi-temp</code> 임시 폴더를 통해 전역 Wrangler 세션을 100% 격리합니다.</li>
<li>배포 순간에만 <code>wrangler.json</code>을 인메모리 스왑하고 배포 완료 후 <code>finally</code> 블록에서 순정 파일로 100% 원복합니다.</li>
<li>배포 직후 로컬 <code>.wrangler</code> 및 <code>node_modules/.cache/wrangler</code> 캐시를 자동 삭제하여 기존 단일 배포(<code>deploy:admin</code>, <code>deploy:blog</code>)에 0% 간섭을 보장합니다.</li>
</ul>
</li>
<li><strong>D1 스키마 자동 마이그레이션 (<code>sync-secrets.js</code>)</strong>:<ul>
<li>배포 파이프라인에서 D1 DB에 <code>is_syndicated</code> 컬럼을 안전하게 자동 추가합니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="2-스벨트킷-블로그-허브hub-통합-피드-및-글-연동">2. 📡 스벨트킷 블로그 허브(Hub) 통합 피드 및 글 연동</h3>
<ul>
<li><strong>실시간 허브 클라이언트 모듈 (<code>hub.ts</code>, <code>syndication.ts</code>)</strong>:<ul>
<li>글 발행 시 **<a href="https://hub.sveltekitblog.com/">SvelteKit Blog Hub BLOG FEED 스벨트킷 블로그 통합 피드</a>**로 요약 메타데이터 카드를 실시간 전송하는 연동 파이프라인을 구축했습니다.</li>
</ul>
</li>
<li><strong>관리자 에디터 허브 연동 토글 UI (<code>PostMetadataForm.svelte</code>)</strong>:<ul>
<li>글 작성/수정 시 허브 피드 전송 여부(<code>is_syndicated</code>)를 원클릭으로 켜고 끌 수 있는 토글 체크박스를 추가했습니다.</li>
</ul>
</li>
<li><strong>DB 스키마 확장 (<code>schema-blog-db.sql</code>)</strong>:<ul>
<li><code>posts</code> 테이블에 <code>is_syndicated INTEGER DEFAULT 0</code> 컬럼을 정식 추가했습니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="3-구글-애드센스-quot가치가-별로-없는-콘텐츠quot-극복-및-seo-전면-최적화">3. 🔍 구글 애드센스 &quot;가치가 별로 없는 콘텐츠&quot; 극복 및 SEO 전면 최적화</h3>
<ul>
<li><strong>순수 포스트 중심 검색 색인 전략</strong>:<ul>
<li>단순 목록성 페이지인 카테고리 모아보기(<code>/[category]</code>), 태그 모아보기(<code>/tags/[tag]</code>), 방명록(<code>/guestbook</code>)에 <code>noindex, follow</code>를 적용하여 Thin Content 감점을 원천 방어하고 크롤러가 내부 링크를 탐색하도록 유도합니다.</li>
<li>고품질 본문 포스트와 사이트 신뢰도에 필수적인 법적/소개 페이지(<code>about</code>, <code>privacy</code>, <code>contact</code> 등 CMS 정적 페이지)는 100% 정상 색인(<code>index, follow</code>)되며, 사이트맵에 0.9 우선순위로 정식 등록됩니다.</li>
</ul>
</li>
<li><strong>사이트맵 &amp; RSS 쿼리 정제 및 통일된 디자인 탑재 (<code>sitemap.xsl</code>)</strong>:<ul>
<li><code>sitemap.xml</code>과 <code>rss.xml</code>의 DB 쿼리에서 CMS 페이지와 슬러그를 배제하고 순수 발행 게시글만 엄격하게 추출합니다.</li>
<li>CMS 정적 페이지는 우선순위 0.9 및 다국어 alternate hreflang을 완벽 지원합니다.</li>
<li><strong>기존 <code>rss.xsl</code>과 디자인 톤앤매너를 100% 동일하게 맞춘 영문 <code>static/sitemap.xsl</code>을 신규 탑재</strong>하여 브라우저 접속 시 반응형 영문 테이블 뷰로 렌더링되도록 구현했습니다.</li>
<li><code>sitemap.xml</code>의 캐시 주기를 <code>public, max-age=3600</code>으로 설정하여 1시간 실시간 갱신을 보장합니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="4-다국어i18n-사전-완성도-개선-및-배포-동기화-편의성-향상">4. 🌐 다국어(i18n) 사전 완성도 개선 및 배포 동기화 편의성 향상</h3>
<ul>
<li><strong>다국어 번역 정확도 개선 및 오타 교정</strong>:<ul>
<li>UI 일관성과 번역 정확도 향상을 위해 다국어 사전을 전수 점검하고 부자연스러운 번역 및 오타를 교정했습니다.</li>
</ul>
</li>
<li><strong>관리자 배포 시 i18n DB 동기화 프롬프트 지원 (<code>deploy:admin</code>)</strong>:<ul>
<li><code>npm run deploy:admin</code> 배포 중 로컬 i18n 사전 변경사항을 D1 데이터베이스(<code>ui_dictionary</code>)에 간편하게 동기화할 수 있도록 대화형 프롬프트를 추가했습니다.</li>
</ul>
</li>
<li><strong>관리자 UI 다국어화 보완</strong>:<ul>
<li>허브 연동 관련 모달 및 설정 안내 문구를 다국어 사전과 연동하여 완전한 다국어 지원을 제공합니다.</li>
</ul>
</li>
</ul>
]]></content:encoded>
            <category>개발로그</category>
        </item>
        <item>
            <title><![CDATA[[개발 일지] 릴리즈 히스토리 요약 (v1.0.0.4 ~ v1.0.0.8)]]></title>
            <link>https://testblog-6br.pages.dev/devlog/devlog-releases-v1004-to-v1008</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/devlog-releases-v1004-to-v1008</guid>
            <pubDate>Sun, 23 Aug 2026 12:38:32 GMT</pubDate>
            <description><![CDATA[스벨트킷 블로그 엔진 릴리즈 히스토리 요약 (v1.0.0.4 ~ v1.0.0.8)]]></description>
            <content:encoded><![CDATA[<h3 id="release-v1008">🚀 Release v1.0.0.8</h3>
<ul>
<li><strong>카테고리 위젯 단일화</strong>: 혼란을 유발하던 레거시 <code>category_link</code> 설정을 제거하고 단일 <strong>&#39;카테고리 메뉴&#39; 위젯</strong>으로 일원화.</li>
<li><strong>포스트 수 표기 옵션 (<code>showPostCount</code>)</strong>: 어드민 디자인 에디터에서 카테고리명 옆에 글 개수를 표시/숨김 할 수 있는 토글 체크박스 추가 (예: <code>개발 관련 (5)</code>).</li>
<li><strong>어드민 디자인 실시간 미리보기 개선</strong>: 디자인 에디터에서 최근 글, 인기 글, 카테고리 메뉴 위젯이 실제 데이터와 유사한 플레이스홀더로 정상 프리뷰되도록 수정.</li>
<li><strong>발행 글 기준 스마트 <code>hreflang</code> 필터링 (SEO)</strong>: 실제 발행된 글이 존재하는 언어에 대해서만 <code>hreflang</code> 태그를 생성하여, 빈 다국어 페이지가 구글 애드센스 등에서 가치 없는 페이지(Thin Content)로 오판되는 현상 원천 방어.</li>
</ul>
<hr>
<h3 id="release-v1007">🚀 Release v1.0.0.7</h3>
<ul>
<li><strong>엔진 버전 시스템 정식 도입 (<code>APP_VERSION</code>)</strong>: <code>@sveltekitblog/shared</code> 패키지에 엔진 버전 상수를 추가하고, 어드민 사이드바 및 블로그 푸터에 현재 구동 중인 엔진 버전 뱃지를 표시.</li>
<li><strong>전체 라우트 다국어 메타데이터 완성</strong>: 홈, 카테고리, 태그, 검색, 방명록 등 모든 프론트엔드 라우트에 <code>hreflang</code> 및 표준 정규화(Canonical) URL 주입 완성.</li>
<li><strong>푸터 모바일 가독성 개선</strong>: 모바일 화면에서 버전 뱃지 명도 대비 및 레이아웃 여백 최적화.</li>
</ul>
<hr>
<h3 id="release-v1006">🚀 Release v1.0.0.6</h3>
<ul>
<li><strong>모바일 Core Web Vitals 및 성능 최적화</strong>: 동적 OG 이미지 생성, 구글 폰트 API 캐싱, 정적 리소스 프리로드로 모바일 로딩 성능 개선.</li>
<li><strong>사이트맵 500 에러 해결</strong>: <code>sitemap.xml</code> 생성 로직에서 존재하지 않는 레거시 태그 테이블 쿼리를 제거하여 무중단 사이트맵 발행 보장.</li>
<li><strong>동적 HTML <code>lang</code> 속성 최적화</strong>: 접속 언어 컨텍스트(<code>ko</code>, <code>en</code>, <code>ja</code>)에 따라 HTML 루트 태그의 <code>lang</code> 속성이 정확하게 자동 전환되도록 개선.</li>
<li><strong>다국어 i18n 키 구조 정비</strong>: 언어 뱃지 번역 키를 <code>common.lang.short_*</code> 그룹으로 일원화.</li>
</ul>
<hr>
<h3 id="release-v1005">🚀 Release v1.0.0.5</h3>
<ul>
<li><strong>Svelte 5 룬(Runes) 반응형 경고 해결</strong>: 어드민 컴포넌트 내 <code>$state</code> 반응형 상태 컴파일러 경고 수정.</li>
<li><strong>어드민 기본 진입 라우트 정리</strong>: 어드민 대시보드 진입점 및 피드백 흐름 개선.</li>
<li><strong>표준 URL(Canonical) 정규화</strong>: 슬래시 누락 및 다국어 URL 간의 정규화 주소 불일치 버그 수정.</li>
</ul>
<hr>
<h3 id="release-v1004">🚀 Release v1.0.0.4</h3>
<ul>
<li><strong>네이버 웹마스터 도구 H1 태그 중복 오류 해결</strong>: <code>LayoutRenderer</code>의 모바일/데스크톱 레이아웃 전환 방식을 모바일 퍼스트 SSR로 개선하여 검색엔진 봇이 H1 태그를 중복 수집하지 않도록 조치.</li>
<li><strong>푸터 레이아웃 정렬</strong>: 블로그 푸터의 최대 너비와 테두리 스타일을 헤더와 일치시키고 그림자(Shadow) 렌더링 개선.</li>
<li><strong>저장소 정리</strong>: Git 추적에 잘못 포함되어 있던 임시 <code>.bak</code> 파일 정리.</li>
</ul>
]]></content:encoded>
            <category>개발로그</category>
        </item>
        <item>
            <title><![CDATA[Cloudflare Pages로 웹사이트를 배포하는 2가지 방법과 기본 원리]]></title>
            <link>https://testblog-6br.pages.dev/general-guide/cloudflare-pages-deployment-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/general-guide/cloudflare-pages-deployment-guide</guid>
            <pubDate>Fri, 21 Aug 2026 23:51:06 GMT</pubDate>
            <description><![CDATA[Cloudflare Pages(CFP)를 이용해 웹사이트를 배포하는 2가지 인증 방식(간편 브라우저 로그인 vs API 토큰)의 차이점과 D1 데이터베이스, Workers KV 저장소의 기본 개념 및 실제 블로그 엔진 적용 구조를 알기 쉽게 정리합니다.]]></description>
            <content:encoded><![CDATA[<h1 id="cloudflare-pages로-웹사이트를-배포하는-2가지-방법과-기본-원리">🚀 Cloudflare Pages로 웹사이트를 배포하는 2가지 방법과 기본 원리</h1>
<p>웹사이트나 블로그를 직접 만들고 나면 &#39;이걸 어떻게 인터넷에 올려서 남들이 접속할 수 있게 만들까?&#39;를 고민하게 됩니다.</p>
<p>예전에는 매달 비용을 내고 리눅스 가상 서버(VPS)를 빌려 웹서버(Nginx)를 설치하고 보안 인증서(SSL)를 설정해야 했지만, 요즘은 **Cloudflare Pages(CFP)**를 이용하면 복잡한 서버 관리 없이 무료로 아주 빠르게 사이트를 배포할 수 있습니다.</p>
<p>이 글에서는 Cloudflare Pages가 무엇인지, 내 컴퓨터에서 사이트를 배포할 때 사용하는 **2가지 방법(브라우저 로그인 방식 vs API 토큰 방식)**은 어떤 차이가 있는지, 그리고 글과 사진을 저장하는 <strong>D1 데이터베이스와 KV 저장소</strong>의 기본 원리를 알기 쉽게 정리해 보았습니다.</p>
<hr>
<h2 id="1-cloudflare-pagescfp란-무엇일까요">📂 1. Cloudflare Pages(CFP)란 무엇일까요?</h2>
<p>쉽게 말해 **&quot;내가 만든 웹사이트 파일들을 전 세계 수백 개 도시에 있는 Cloudflare 컴퓨터에 올려두고, 방문자에게 가장 가까운 곳에서 초고속으로 보여주는 호스팅 서비스&quot;**입니다.</p>
<p>HTML, CSS, 이미지 같은 정적 파일은 물론이고, SvelteKit이나 Next.js처럼 화면을 서버에서 실시간으로 그려주는(SSR) 최신 웹사이트도 척척 동작하게 해줍니다.<br><img src="https://sveltekitblog.com/images/posts/cloudflare-pages-deployment-guide/desktop/img-general-guide-cloudflare-pages-deployment-guide-ko-001.webp" alt="img-general-guide-cloudflare-pages-deployment-guide-ko-001"></p>
<h3 id="왜-가상-서버vps-대신-cloudflare-pages를-쓸까요">왜 가상 서버(VPS) 대신 Cloudflare Pages를 쓸까요?</h3>
<ul>
<li><strong>서버 관리를 전혀 안 해도 됩니다</strong>: 해킹 방지용 리눅스 보안 패치를 하거나 웹서버가 죽었는지 감시할 필요가 없습니다. Cloudflare가 24시간 알아서 안전하게 관리해 줍니다.</li>
<li><strong>로딩 속도가 정말 빠릅니다</strong>: 한국 사용자는 한국에 있는 Cloudflare 센터에서, 미국 사용자는 미국에 있는 센터에서 페이지를 즉시 보내주기 때문에 사이트가 쾌속으로 열립니다.</li>
<li><strong>비용이 거의 들지 않습니다</strong>: 개인 블로그나 일반적인 포트폴리오 사이트 수준의 방문자 트래픽은 무료 요금제(Free Tier) 범위 안에서 넉넉하게 운영할 수 있습니다.</li>
</ul>
<hr>
<h2 id="2-컴퓨터에서-cloudflare로-사이트를-배포하는-2가지-방법">🔑 2. 컴퓨터에서 Cloudflare로 사이트를 배포하는 2가지 방법</h2>
<p>SvelteKit 애플리케이션을 빌드(<code>npm run build</code>)하면 <code>@sveltejs/adapter-cloudflare</code>에 의해 <strong><code>.svelte-kit/cloudflare</code></strong> 디렉토리에 정적 파일과 서버 실행용 <code>_worker.js</code>가 생성됩니다.</p>
<p>이 결과물을 Cloudflare Pages에 배포하는 실제 명령어는 <strong><code>npx wrangler pages deploy .svelte-kit/cloudflare</code></strong> 이며, 이때 계정을 인증하는 방식에 크게 2가지가 있습니다.</p>
<h4 id="방법-1-대화형-브라우저-로그인-배포-흐름-wrangler-기본-방식">[방법 1] 대화형 브라우저 로그인 배포 흐름 (Wrangler 기본 방식)</h4>
<p>프로젝트를 처음 만들고 배포할 때 가장 직관적인 방식입니다. 터미널 명령을 치면 브라우저 창이 열리며 로그인 권한을 승인받습니다.<br><img src="https://sveltekitblog.com/images/posts/cloudflare-pages-deployment-guide/desktop/img-general-guide-cloudflare-pages-deployment-guide-ko-002.webp" alt="img-general-guide-cloudflare-pages-deployment-guide-ko-002"></p>
<pre><code class="language-bash"># SvelteKit 빌드 후 Pages 배포 (대화형 브라우저 로그인)
npm run build
npx wrangler pages deploy .svelte-kit/cloudflare --project-name=my-blog-web --branch=production
</code></pre>
<ul>
<li><strong>어떻게 동작하나요?</strong>: 터미널에서 명령어를 치면 내 컴퓨터의 웹 브라우저가 자동으로 열리며 Cloudflare 로그인 화면이 뜹니다. 화면에서 [Allow(승인)] 버튼만 꾹 눌러주면 내 컴퓨터에 로그인 세션이 저장되면서 사이트 배포가 진행됩니다.</li>
<li><strong>장점</strong>: 토큰을 따로 생성할 필요 없이 마우스 클릭 몇 번이면 끝나기 때문에, <strong>계정 1개로 개인 블로그를 처음 시작할 때 가장 쉽고 직관적입니다.</strong></li>
<li><strong>아쉬운 점</strong>:<ul>
<li>Cloudflare 계정이 여러 개여서 번갈아 가며 배포해야 한다면 매번 브라우저 로그아웃과 로그인을 반복해야 합니다.</li>
<li>브라우저 창을 띄울 수 없는 원격 서버나 GitHub Actions 같은 자동화(CI/CD) 환경에서는 쓸 수 없습니다.</li>
</ul>
</li>
</ul>
<hr>
<h4 id="방법-2-api-토큰-환경변수-기반-비대화형-자동-배포-cicd-및-다계정-표준">[방법 2] API 토큰 환경변수 기반 비대화형 자동 배포 (CI/CD 및 다계정 표준)</h4>
<p>다계정 운영이나 배포를 무인 자동화할 때 사용하는 표준적인 방식입니다. 미리 발급받은 API 토큰을 환경변수로 넘겨주어 브라우저 팝업 없이 올립니다.<br><img src="https://sveltekitblog.com/images/posts/cloudflare-pages-deployment-guide/desktop/img-general-guide-cloudflare-pages-deployment-guide-ko-003.webp" alt="img-general-guide-cloudflare-pages-deployment-guide-ko-003"></p>
<pre><code class="language-bash"># 환경변수로 토큰과 계정 ID를 전달하며 배포 (Windows PowerShell 예시)
$env:CLOUDFLARE_API_TOKEN=&quot;발급받은_API_토큰&quot;
$env:CLOUDFLARE_ACCOUNT_ID=&quot;32자리_계정_ID&quot;
npx wrangler pages deploy .svelte-kit/cloudflare --project-name=my-blog-web --branch=production
</code></pre>
<ul>
<li><strong>어떻게 동작하나요?</strong>: Cloudflare 웹사이트에서 필요한 권한(Pages, D1, KV 등)을 가진 API 토큰을 미리 발급받아 환경변수나 설정 파일에 등록해 둡니다. 배포 명령을 실행할 때 이 토큰이 자동으로 주입되어 브라우저 창을 띄우지 않고 백그라운드에서 즉시 배포를 완료합니다.</li>
<li><strong>장점</strong>:<ul>
<li>인터넷 창이 번쩍거리며 열리지 않고 백그라운드에서 조용하고 빠르게 올라갑니다.</li>
<li>계정마다 서로 다른 토큰을 지정해 둘 수 있어서, <strong>여러 블로그나 사이트를 운영할 때 세션 충돌 없이 완벽하게 독립 배포</strong>할 수 있습니다.</li>
</ul>
</li>
<li><strong>알아둘 점</strong>: 최초 1회 Cloudflare 대시보드에서 API 토큰을 발급받는 과정이 필요합니다.</li>
</ul>
<hr>
<h2 id="3-글과-이미지는-어디에-저장될까요-d1과-kv의-쉬운-개념">💾 3. 글과 이미지는 어디에 저장될까요? (D1과 KV의 쉬운 개념)</h2>
<p>웹사이트 화면만 있다고 블로그가 돌아가는 것은 아닙니다. 작성한 글과 업로드한 사진을 안전하게 보관할 공간이 필요합니다. Cloudflare는 이를 위해 2가지 편리한 저장 공간을 제공합니다.<br><img src="https://sveltekitblog.com/images/posts/cloudflare-pages-deployment-guide/desktop/img-general-guide-cloudflare-pages-deployment-guide-ko-004.webp" alt="img-general-guide-cloudflare-pages-deployment-guide-ko-004"></p>
<ol>
<li><strong>Cloudflare D1 (글과 설정이 저장되는 관계형 데이터베이스)</strong>:<ul>
<li>엑셀 시트처럼 칸과 줄이 딱딱 맞춰진 표(Table) 형태의 관계형 데이터베이스(SQLite)입니다.</li>
<li>&quot;글 제목은 무엇인가?&quot;, &quot;어느 카테고리에 속하는가?&quot;, &quot;언제 작성되었는가?&quot;, &quot;사이트 이름은 무엇인가?&quot; 같은 정형화된 정보를 빠르고 안전하게 보관하고 검색합니다.</li>
</ul>
</li>
<li><strong>Workers KV (사진과 이미지를 저장하는 초고속 창고)</strong>:<ul>
<li>글에 첨부한 썸네일이나 이미지 파일처럼 용량이 크고 다양한 파일들을 전 세계 Cloudflare 창고에 넣어두고, 방문자가 요청할 때 1초도 안 걸려 즉시 꺼내 보여주는 역할을 합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="4-스벨트킷-블로그-엔진의-실제-구성-엿보기">🧩 4. 스벨트킷 블로그 엔진의 실제 구성 엿보기</h2>
<p>스벨트킷 블로그 엔진(SvelteKit Blog Engine)은 위에서 살펴본 Cloudflare의 장점들을 살려 아래와 같이 설계되어 있습니다.<br><img src="https://sveltekitblog.com/images/posts/cloudflare-pages-deployment-guide/desktop/img-general-guide-cloudflare-pages-deployment-guide-ko-005.webp" alt="img-general-guide-cloudflare-pages-deployment-guide-ko-005"></p>
<hr>
<h3 id="왜-방문자용-블로그와-관리자-페이지를-2개로-따로-쪼갰을까요">왜 방문자용 블로그와 관리자 페이지를 2개로 따로 쪼갰을까요?</h3>
<p>보통은 블로그와 관리자 화면을 하나의 프로그램으로 뭉쳐서 만듭니다. 하지만 스벨트킷 블로그 엔진은 이 둘을 <strong>완전히 독립된 2개의 Cloudflare Pages로 분리</strong>했습니다:</p>
<ol>
<li><strong>보안이 훨씬 안전해집니다</strong>: 관리자 로그인 창이나 글 작성/삭제 기능이 일반 방문자가 들어오는 블로그 주소에는 아예 들어있지 않습니다. 글을 쓰는 관리자 주소가 물리적으로 완전히 분리되어 있으므로 해킹 시도 자체를 원천 차단하기 좋습니다.</li>
<li><strong>방문자 화면이 엄청 가볍고 빨라집니다</strong>: 글을 편집하는 복잡한 에디터 도구들이 방문자 페이지에는 1바이트도 포함되지 않기 때문에, 핸드폰이나 느린 인터넷 환경에서도 블로그 글이 시원시원하게 순식간에 뜹니다.</li>
</ol>
<hr>
<h3 id="글을-쓰고-저장하는-과정">글을 쓰고 저장하는 과정</h3>
<ul>
<li>내가 <strong>관리자 화면</strong>에 들어가서 새 글을 쓰고 사진을 올리면, 그 내용은 Cloudflare의 **D1(데이터베이스)**과 **KV(사진 창고)**에 저장됩니다.</li>
<li>일반 독자가 <strong>블로그 웹사이트</strong>에 접속하면, 방금 저장된 D1과 KV에서 최신 글과 사진을 쏙 뽑아와서 예쁜 화면으로 보여줍니다.</li>
</ul>
<hr>
<h2 id="5-마치며">🚀 5. 마치며</h2>
<p>Cloudflare Pages는 예전처럼 복잡한 서버 명령어들을 외우거나 비싼 호스팅 비용을 낼 필요 없이, 누구나 가볍고 쾌적한 나만의 웹사이트를 운영할 수 있게 도와주는 훌륭한 기술입니다.</p>
<p>처음에는 **간편 브라우저 로그인(<code>wrangler login</code>)**으로 가볍게 블로그를 띄워보시고, 나중에 사이트 관리에 익숙해지거나 여러 개의 블로그를 운영하게 되면 <strong>API 토큰을 이용한 다계정 배포</strong>로 자연스럽게 확장해 보시는 것을 추천합니다.</p>
]]></content:encoded>
            <category>통합 가이드</category>
            <enclosure url="https://sveltekitblog.com/images/posts/cloudflare-pages-deployment-guide/desktop/img-general-guide-cloudflare-pages-deployment-guide-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[[개발 일지] 2026-07-10]]></title>
            <link>https://testblog-6br.pages.dev/devlog/devlog-2026-07-10</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/devlog-2026-07-10</guid>
            <pubDate>Fri, 10 Jul 2026 23:07:44 GMT</pubDate>
            <description><![CDATA[v1.0.0.2 & v1.0.0.3 릴리즈 및 신규 테마 적용]]></description>
            <content:encoded><![CDATA[<h2 id="v1002-업데이트-내역">v1.0.0.2 업데이트 내역</h2>
<p>이번 릴리즈에서는 관리자 포스트 에디터의 편의성과 기능성을 높이기 위한 여러 개선 사항들이 반영되었습니다.</p>
<h3 id="주요-변경-사항">🚀 주요 변경 사항</h3>
<ol>
<li><p><strong>본문 이미지 링크 삽입 및 편집 기능 추가</strong></p>
<ul>
<li>에디터 내에 이미지를 삽입하거나 편집할 때, 이미지에 커스텀 링크 주소(URL) 및 새 창 열기 설정을 연동할 수 있도록 모달 UI를 확장했습니다.</li>
<li>Tiptap 에디터 로드 시 이미지 링크가 강제로 제거되는 문제를 방지하기 위해 HTML 전/후처리(Pre/Post-processing) 기능을 연동했습니다.</li>
</ul>
</li>
<li><p><strong>포스트 작성/수정 미리보기 이미지 캡션 시각화</strong></p>
<ul>
<li>포스트 미리보기 탭에서 이미지 아래의 의미론적 캡션(figcaption)이 정상적으로 렌더링되도록 기존 본문 처리 유틸리티를 재사용하여 동기화했습니다.</li>
</ul>
</li>
<li><p><strong>다국어 지원 실시간 글자수 카운터 탑재</strong></p>
<ul>
<li>글 작성 및 수정 화면 하단에 순수 글자 수를 실시간으로 집계해 주는 카운터 UI를 추가했습니다.</li>
<li>HTML 태그 및 마크다운 문법 기호 등을 자동으로 정화하여 실제 텍스트의 글자 수만 정밀하게 카운팅하며, 다국어 설정을 완벽하게 지원합니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="v1003-업데이트-내역">v1.0.0.3 업데이트 내역</h2>
<p>이번 릴리즈에서는 위젯 시스템의 구조적 결함을 전면 수정하고, 위젯 목록 UI 스타일을 통일했습니다.</p>
<h3 id="버그-수정">🐛 버그 수정</h3>
<ol>
<li><p><strong>위젯 신규 생성 직후 배치 시 렌더링 크래시 수정</strong></p>
<ul>
<li>새로 만든 위젯을 즉시 레이아웃에 배치할 때 <code>master-</code> 접두사가 누락된 채 저장되어 렌더링이 붕괴되던 치명적 크래시를 수정했습니다.</li>
</ul>
</li>
<li><p><strong>데스크톱/모바일 레이아웃 배치 시 <code>config</code> 데이터 유실 수정</strong></p>
<ul>
<li>위젯을 배치하는 과정에서 설정값(<code>config</code>)이 전혀 전달되지 않아 배치된 위젯이 아무런 데이터 없이 빈 상태로 저장되던 버그를 수정했습니다.</li>
</ul>
</li>
<li><p><strong>위젯 편집 모달 오픈 시 설정값 초기화 버그 수정</strong></p>
<ul>
<li>배치된 위젯의 편집 버튼을 눌렀을 때 <code>config</code>가 이미 객체임에도 <code>JSON.parse</code>를 이중으로 시도하여 SyntaxError가 발생하고, 모달의 모든 설정(limit, shadow 등)이 기본값으로 초기화되던 버그를 수정했습니다.</li>
</ul>
</li>
<li><p><strong><code>limit</code> 비정상 입력 유효성 검사 강화</strong></p>
<ul>
<li>위젯 생성 및 편집 시 <code>limit</code> 값이 비정상(<code>NaN</code>, 1 미만)으로 저장될 수 있던 취약점을 차단하고, 해당 경우 기본값 <code>5</code>로 정규화하도록 수정했습니다.</li>
</ul>
</li>
<li><p><strong>동일 타입 위젯 다중 배치 시 데이터 조회 범위 오류 수정</strong></p>
<ul>
<li>같은 타입의 위젯(예: 인기 포스트)이 여러 개 배치된 경우, 첫 번째 위젯의 <code>limit</code> 기준으로만 DB를 조회하여 나머지 위젯이 잘린 데이터를 받던 구조적 버그를 수정했습니다. 이제 동일 타입 위젯 중 가장 큰 <code>limit</code> 기준으로 조회한 뒤, 각 위젯이 자신의 설정에 맞게 독립적으로 데이터를 표시합니다.</li>
</ul>
</li>
</ol>
<h3 id="스타일">🎨 스타일</h3>
<ol>
<li><strong>위젯 목록 구분선 스타일 통일</strong><ul>
<li><code>RecentPostsWidget</code>과 <code>PopularPostsWidget</code>의 항목 간 구분 방식이 달라 시각적 일관성이 없던 문제를 수정했습니다. 두 위젯 모두 <code>border-bottom</code> 구분선과 <code>0.5rem</code> 패딩으로 통일했습니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="레이아웃-및-디자인-마이너-조정">🎨 레이아웃 및 디자인 마이너 조정</h2>
<p>블로그 레이아웃과 색상 등 전반적인 스타일 요소를 마이너 조정하여, 화면 구성이 깨지지 않고 자연스럽게 어우러지도록 다듬었습니다.</p>
<h3 id="주요-스타일-조정-사항">🛠️ 주요 스타일 조정 사항</h3>
<ol>
<li><p><strong>테마 컬러 및 톤 조정</strong></p>
<ul>
<li>주황색 계열의 서브 컬러를 포인트로 잡고, 전체적인 배경색과 카드 영역의 테두리 및 그림자 값을 다듬어 시각적인 부담을 줄였습니다.</li>
</ul>
</li>
<li><p><strong>데스크톱 2단 구성 레이아웃 배치</strong></p>
<ul>
<li>본문 영역과 사이드바(카테고리, 최근/인기 포스트 등)의 가로 너비 비율을 재조정하여 좌우 여백과 콘텐츠 정렬이 어긋나지 않도록 맞추었습니다.</li>
</ul>
</li>
<li><p><strong>헤더/푸터 및 모바일 메뉴 정렬</strong></p>
<ul>
<li>상단 헤더의 투명도 조정과 함께 모바일 메뉴 아이콘의 정렬 상태 및 하단 푸터 영역의 레이아웃이 깔끔하게 표시되도록 정리했습니다.</li>
</ul>
</li>
</ol>
]]></content:encoded>
            <category>개발로그</category>
        </item>
        <item>
            <title><![CDATA[[개발 일지] 어쩌다가 철지난 블로그 엔진을 만들게 되었는가]]></title>
            <link>https://testblog-6br.pages.dev/devlog/devlog-why-i-built-a-blog-engine-in-2026</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/devlog-why-i-built-a-blog-engine-in-2026</guid>
            <pubDate>Thu, 09 Jul 2026 20:29:48 GMT</pubDate>
            <description><![CDATA[그냥 커피값이나 벌 생각으로 블로그 개설하려다가 블로그 엔진 만들게 된 이야기]]></description>
            <content:encoded><![CDATA[<h3 id="개인적인-생각을-정리하는-독백-형식의-글이라-평어로-작성된-점-양해-부탁드립니다"><strong>개인적인 생각을 정리하는 독백 형식의 글이라 평어로 작성된 점 양해 부탁드립니다</strong><br></h3><p>1.<br>상황이 이렇게 된 건 사실 유튜브의 탓이 크다(?).<br>뒤늦게 1인 개발자로 뭔가 해보려고 자료를 찾다보면 어마어마하게 많은 정보들이 쏟아져 나온다.<br>문제는 정보의 양은 넘쳐나는데 사실상 질은 엉망인 경우가 많다는거다.</p><p>2.<br>간단한 기초적인 내용이라면 정보가 많다고 해도 크게 문제될 건 없는데 조금만 전문적이거나 특수한 상황이 되면, 대체 이게 왜 검색에 걸려서 내가 이 웹에서 이걸 보고 있는지 도저히 이해가 안갈 정도로 완전 필요없는 내용의 정보를 접하게 되거나, 제목에는 내가 원하는 정보에 관한 내용이 들어가 있지만 정작 본문에는 그냥 문장 구조만 맞춘 의미없는 문자의 나열인 경우가 꽤 된다.<br>이게 유튜브 영상일 때는 진짜 화난다.</p><p>3.<br>그러다가 가끔씩 사막에서 오아시스 만나듯 알짜배기 정보를 줬던건 거의 대부분 개발자의 블로그 기록이었다.<br>정말 반갑고, 고맙고 그랬다.<br>그리고 성인 ADHD를 의심할만한 내 집중력은 '아 나도 개발자 블로그 하나 파서 누군가에게 도움이 되고, 내 커피값도 벌고 싶다(?)' 란 결정을 내리게 된다.</p><p>4.<br>그렇게 기존의 블로그 플랫폼들을 재점검 해봤지만 나에겐 뭔가 하나씩 치명적인 문제점들이 있었다.<br>우선 배포/유지하는데에 비용이 발생하는 것들은 모두 제외했다.<br>뭐 커피값이야 벌면 좋지만 사실상 그게 가능할 정도로 하려면 진짜 열심히 해야한다는 걸 아는데, 난 나를 잘 안다.<br>난 그렇게 열심히 못한다.<br>그렇기에 애초에 설치/유지 비용이 들어가면 절대 안된다는 결론을 시작부터 깔고 갔다.<br>그래서 처음 구상했던건 Proxmox로컬 서버에 워드프레스를 설치해서 쓰는거였다.<br>이렇게 쓰려면 최소한의 보안을 위해서 L2 스위치와 pfSense같은 방화벽 설치는 필수라고 봤다.<br>그렇게 어느 정도 구성을 하고나서 워드프레스를 다시 살펴보니, 커스텀이 너무 어려웠다.<br>이거 커스텀 하려면 PHP를 제대로 공부해야할것 같다는 판단이 들었을 때 이 방식은 포기하게 된다.</p><p>5.<br>그리고 빠르게 다른 방법들을 찾아봤지만 결국 뭔가 하나씩 걸렸다.<br>어떤 방법이건 설치를 하면 결국 그것에 대해 새로 공부를 해야한다는 점이 매우 피로감있게 다가왔다.<br>그 피로감이 '아 그냥 간단하게 내가 만들자' 란 생각으로 이끌었다.</p><p>6.<br>이때 이미 난 스벨트와 스벨트킷으로 이런저런 장난질을 많이 하고 있었던 터였다.<br>그리고 블로그를 만드는 건 매우 간단한 상황이었다.<br>문제는 내가 한 디자인을 내가 믿을수 없다는 거였다.<br>그래서 디자인을 바꾸기 쉽게 할 수 있는 구조로 만들자던게 결국 굴러굴러 여기까지 오게 됐다.</p><p>7.<br>실제로 디자인 에디터의 고도화 및 디버깅에 가장 많은 시간을 쏟았다.<br>최초에 '간단한 블로그 툴'을 만들자 하고 하루만에 코드의 90% 정도를 완성시켰다.<br>그리고 실제로 블로그를 만들어서 운영을 했다.<br>그러자 '이것도 필요하네?', '이 기능도 넣어야겠네', '이거 있으면 편하겠다', '이건 있어야지' 가 거의 무한으로 쏟아져나왔다.<br>그렇게 작년 11월말에 시작했던 프로젝트 아닌 프로젝트가 올해 3월에 기능 구현이 마무리가 되고 7월이 되어서야 <strong>일단</strong> 깃허브 오픈을 하게 됐다.</p><p>8.<br>정신 차려보니 기능이 너무 많아지고 코드도 복잡해져서 중간중간 모듈화하고 리팩토링을 진행했다.<br>시스템 자체를 갈아엎기도 여러번 했다.<br>최초에는 어드민은 Proxmox서버에 설치해서 로컬에서만 접속가능하게 하고 DB는 Supabase, 인증도 Supabase auth를 썼었다.<br>꽤나 심각한 콜드 스타트와 무료 플랜에서는 인증시에 프로젝트명이 노출되는 상황을 겪고 그냥 클라우드플레어 생태계를 쓰기로 하고 이전에 써봤던 Better-auth를 탑재하는 것으로 계획을 틀게 된다.<br>이후로도 내부적으로 로직이나 구조를 상당히 많이 바꾸게 된다.<br>게다가 윈도우에서 작업을 진행한 탓에 뭐 하나 고치면 실제 배포까지 해야 확인이 가능해서 진짜 어마어마한 삽질을 했다.</p><p>9.<br>거의 끝나갈 때 즈음엔 나 혼자서는 도저히 수정도 못할 정도로 코드가 너무 방대해져 있어서 직접 한것도 있지만 대부분 제미나이를 시켜서 진행했다.<br>거기다 문서화를 하지 않아서 하나하나 실제 기능과 비교해가며 문서화를 진행하다가 오류 발견하면 또 다시 수정작업에 들어갔다.<br>이렇게 3월부터 7월까지 문서화-&gt;오류 발견-&gt;수정의 무한 반복이었다.<br>그 와중에 '이건 있어야지'랑 '이 기능 있었는데 어디갔지?'가 막 튀어나와서 진짜 미치는 줄 알았다.</p><p>10.<br>엎친데 덮친 격으로 제미나이가 진짜 맛이 가버리는 사태가 매우 종종 발생했다.<br>코드 한 줄 고치면 되는 걸 상관도 없는 모듈을 뜯어고치질 않나 뭐 하여간 제미나이가 깽판만 안쳤어도 두 달은 일찍 끝났을거라 확신한다.<br>여튼 깃허브 오픈 직전까지 극심한 스트레스에 시달리다 그냥 '이건 있어야지'를 다 쳐내고 되어있는 거만 정리해서 올리기로 결정했다.<br>그게 v1.0.0.0이었다.</p><p>11.<br>그 후로 바로 v1.0.0.1이 나온다.<br>'있는 거'만 정리해서 올렸어야 하는데 정리가 제대로 안된 것들을 바로 알아챈거다.<br>아마 이런게 더 있을거라 확신이 들어서 매우 불안하기도 하다.<br>이미 올릴 때부터 직감해서 v1.0.0.x까지 해뒀던듯도 싶다.</p><p>12.<br>뭐 이렇게 해서 스벨트킷 블로그 엔진이 만들어졌습니다.<br>글마무리가 애매하니 PageSpeed Insights 점수나 올리고 마무리하겠습니다.<br>애드센스는 신청 후 대기 중이지만 일단 코드가 삽입되어 있고 CDN캐시도 2분 간격으로 설정되어 있어서 파되었다 재생성되는 사이에 테스트가 진행되면 성능에 영향을 주는건지 정확한 이유는 모르겠으나 현재 점수의 변동이 꽤나 큽니다.<br>평균 정도 나왔을 때 점수니 그냥 참고로만 봐주세요.<br>애드센스 승인되면 다시 PageSpeed Insights 보고서 올리겠습니다.<br>좋은 하루 되세요. :)</p><figure data-align="center"><img src="https://sveltekitblog.com/images/posts/devlog-why-i-built-a-blog-engine-in-2026/desktop/img-devlog-devlog-why-i-built-a-blog-engine-in-2026-ko-001.webp" alt="img-devlog-devlog-why-i-built-a-blog-engine-in-2026-ko-001" data-align="center" data-caption="모바일 점수. 애드센스(신청대기중) GA4, 구글 서치콘솔, 네이버 서치어드바이저 탑재 상태"><figcaption>모바일 점수. 애드센스(신청대기중) GA4, 구글 서치콘솔, 네이버 서치어드바이저 탑재 상태</figcaption></figure><figure data-align="center"><img src="https://sveltekitblog.com/images/posts/devlog-why-i-built-a-blog-engine-in-2026/desktop/img-devlog-devlog-why-i-built-a-blog-engine-in-2026-ko-002.webp" alt="img-devlog-devlog-why-i-built-a-blog-engine-in-2026-ko-002" data-align="center" data-caption="데스크탑 점수. 애드센스(신청 대기중), GA4, 구글 서치콘솔, 네이버 서치어드바이저"><figcaption>데스크탑 점수. 애드센스(신청 대기중), GA4, 구글 서치콘솔, 네이버 서치어드바이저</figcaption></figure><p></p>]]></content:encoded>
            <category>개발로그</category>
            <enclosure url="https://sveltekitblog.com/images/posts/devlog-why-i-built-a-blog-engine-in-2026/desktop/img-devlog-devlog-why-i-built-a-blog-engine-in-2026-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[[개발 일지] 2026-07-08]]></title>
            <link>https://testblog-6br.pages.dev/devlog/devlog-2026-07-08</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/devlog/devlog-2026-07-08</guid>
            <pubDate>Wed, 08 Jul 2026 14:00:14 GMT</pubDate>
            <description><![CDATA[[개발 일지] 2026년 07월 08일 - 업데이트 상황]]></description>
            <content:encoded><![CDATA[<a href="https://github.com/sveltekitblog/sveltekitblogengine" target="_blank" rel="noopener noreferrer"><figure data-align="center"><img src="https://sveltekitblog.com/images/posts/devlog-2026-07-08/desktop/img-devlog-devlog-2026-07-08-ko-001.webp" alt="img-devlog-devlog-2026-07-08-ko-001" data-align="center" data-caption="Sveltekit Blog Engine v1.0.0.1 릴리즈" data-link-url="https://github.com/sveltekitblog/sveltekitblogengine" data-link-target="_blank"><figcaption>Sveltekit Blog Engine v1.0.0.1 릴리즈</figcaption></figure></a><p>1.<br>Sveltekit Blog Engin v1.0.0.1 릴리즈 업데이트.<br>어드민 - 피드백 - 휴지통에 페이지네이션 및 일괄 삭제/복구 기능 추가.<br>다국어 설정(hreflang) 태그 바인딩 설정 추가.</p><figure data-align="center"><img src="https://sveltekitblog.com/images/posts/devlog-2026-07-08/desktop/img-devlog-devlog-2026-07-08-ko-002.webp" alt="img-devlog-devlog-2026-07-08-ko-002" data-align="center" data-caption="소셜 로그인 추가된 화면"><figcaption>소셜 로그인 추가된 화면</figcaption></figure><p></p><p><br>2.<br>소셜 로그인 추가 - 기존의 이메일 로그인에 Github, Google, Naver 추가.</p><p>3.<br>Sveltekit Blog Engin v1.0.0.0 릴리즈 때도 아무 언질이 없다가 v1.0.0.1을 올린 후에야 개발 일지라는 걸 써야겠다는 판단이 들어서 뒤늦게라도 올림.<br>우선 상세 메뉴얼부터 최대한 포스팅 후 개발 관련 이야기들을 포스팅할 예정.<br>상세 메뉴얼은 마크다운으로, 개발 일지는 텍스트 에디터로 작성함.</p><p>4.<br>이 포스팅 쓰면서 발견한 문제점.<br>첫째 - 이미지에 링크 거는 기능이 없다. <s>이 포스팅은 수동으로 추가함.</s> 기능 추가함.<br>둘째 - <s>까먹음(;;)</s> 본문 텍스트가 몇 글자인지 표기해주는 기능을 거의 프로젝트 시작할 때 완성했었는데 완전 갈아엎어지는 상황에 어디론가 떠내려간듯.</p><p>5.<br>기능 구현이 온전히 끝나야 상세 메뉴얼도 각잡고 쓸텐데 이거 뭐 메뉴얼 쓰다가 에러 발견, 포스팅 하다가 기능 누락 발견...<br>끝이 안 보임.</p>]]></content:encoded>
            <category>개발로그</category>
            <enclosure url="https://sveltekitblog.com/images/posts/devlog-2026-07-08/desktop/img-devlog-devlog-2026-07-08-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[웹 폰트 설정법 및 로딩 성능 최적화 매뉴얼]]></title>
            <link>https://testblog-6br.pages.dev/detail-manual/admin-font-settings</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/detail-manual/admin-font-settings</guid>
            <pubDate>Tue, 07 Jul 2026 06:36:18 GMT</pubDate>
            <description><![CDATA[SvelteKit 블로그 어드민에서 웹 폰트를 안전하게 구성하고, Cloudflare Pages 환경에서 로딩 속도를 향상시키기 위해 구현된 폰트 프록싱 및 캐싱 최적화 기술을 완벽히 마스터합니다.]]></description>
            <content:encoded><![CDATA[<p><img src="https://sveltekitblog.com/images/posts/setup-guide/desktop/img-detail-manual-setup-guide-ko-001.webp" alt="img-detail-manual-setup-guide-ko-001"></p>
<h1 id="블로그-어드민-웹-폰트-설정-및-로딩-최적화-상세-설명서">✍️ 블로그 어드민 웹 폰트 설정 및 로딩 최적화 상세 설명서</h1>
<p>본 프로젝트는 블로그의 시각적 완성도와 가독성을 높이기 위해 어드민 단에서 손쉽게 폰트를 커스터마이징할 수 있는 직관적인 편집 도구를 제공합니다. </p>
<p>단순히 폰트 이름을 적용하는 데 그치지 않고, 모바일 환경이나 네트워크 속도가 제한된 환경에서도 최고의 로딩 속도와 성능 점수(Lighthouse Performance)를 유지할 수 있도록 설계된 특별한 성능 최적화 엔진이 함께 동작합니다.</p>
<p>본 설명서는 어드민 디자인 에디터에서의 구체적인 폰트 설정 방법과, 내부적으로 폰트가 어떻게 최적화되어 방문자에게 빠르게 서빙되는지 동작 개념을 상세히 다룹니다.</p>
<hr>
<h2 id="1-디자인-에디터-내-폰트-설정-세부-항목">📂 1. 디자인 에디터 내 폰트 설정 세부 항목</h2>
<p>어드민 디자인 에디터 페이지의 설정 메뉴에서 블로그 레이아웃의 성격에 맞춰 글꼴 종류, 크기, 두께를 세밀하게 편집할 수 있습니다.</p>
<h3 id="1-1-글로벌-웹-폰트-설정-전체-페이지-영향">✍️ 1-1. 글로벌 웹 폰트 설정 (전체 페이지 영향)</h3>
<p>블로그의 전체적인 본문 영역과 기본 인터페이스 전반에 기본값으로 바인딩되는 글로벌 폰트를 관리합니다.</p>
<ul>
<li><strong>기본 글꼴 설정 (Font Family)</strong>:<ul>
<li>블로그의 가장 베이스가 되는 본문 서체를 지정합니다.</li>
<li><em>입력 방법</em>: 브라우저가 인식할 수 있는 글꼴 이름을 따옴표와 함께 적어줍니다. 비워두면 시스템 기본 폰트인 <code>Inter</code> 서체가 전체 블로그에 기본적으로 상속됩니다. (예: <code>&#39;Noto Serif KR&#39;, serif</code> 또는 <code>Pretendard</code>)</li>
</ul>
</li>
<li><strong>추가할 구글 웹 폰트 (Google Font Name)</strong>:<ul>
<li>로고 텍스트나 핵심 위젯 타이틀 등 특정 영역에 포인트로 입히고 싶은 구글 폰트의 영문 명칭을 등록합니다.</li>
<li><em>입력 방법</em>: 쉼표(<code>,</code>)나 세미콜론(<code>;</code>)을 사용해 여러 개의 폰트를 나열하여 등록할 수 있습니다. (예: <code>Noto Serif KR, Newsreader</code>)</li>
<li><em>연동 효과</em>: 이곳에 입력한 커스텀 폰트명들은 헤더의 로고 폰트 설정이나 각 영역의 개별 글꼴 선택 옵션 드롭다운 메뉴에 자동으로 생성되어 편리하게 매핑할 수 있게 됩니다.</li>
</ul>
</li>
<li><strong>기본 글자 크기 (Base Size) 및 모바일 기본 글자 크기 (Base Size - Mobile)</strong>:<ul>
<li>데스크톱과 모바일 환경 각각에서 서비스 전체 레이아웃의 기준이 되는 본문 글자 크기를 정의합니다.</li>
<li><em>입력 방법 및 팁</em>: <code>16px</code> 이나 <code>14px</code> 같은 정밀한 <strong>절대 단위(px)</strong> 뿐만 아니라, 상위 환경이나 사용자의 브라우저 기본 글 크기 설정에 맞추어 유연하게 변동하는 <strong>상대 단위(rem, em)</strong> 형식의 주입도 완벽히 호환됩니다. (예: 데스크톱 <code>1rem</code> 또는 <code>16px</code> / 모바일 <code>0.875rem</code> 또는 <code>14px</code>)</li>
</ul>
</li>
</ul>
<h3 id="1-2-헤더-및-로고-폰트-설정">🎨 1-2. 헤더 및 로고 폰트 설정</h3>
<p>상단 네비게이션 헤더에 노출되는 브랜딩 로고 텍스트에 독립적인 디자인을 부여합니다.</p>
<ul>
<li><strong>폰트 패밀리 (Font Family)</strong>:<ul>
<li>로고 텍스트에 입힐 서체를 드롭다운에서 선택합니다.</li>
<li>기본 설정인 <code>기본 폰트 상속 (Inherit)</code>을 고르면 전체 베이스 폰트를 따라가며, 글로벌 웹 폰트 설정의 <strong>[추가할 구글 웹 폰트]</strong> 영역에 지정해 두었던 폰트가 있을 경우 목록에 자동으로 표시되어 개별 적용이 가능합니다.</li>
</ul>
</li>
<li><strong>폰트 크기 및 모바일 폰트 크기</strong>:<ul>
<li>해상도 환경 변화에 따라 로고 글자가 찌그러지거나 레이아웃을 해치지 않도록 데스크톱 크기와 모바일 크기를 각각 다른 가독성 배율로 할당할 수 있습니다. (예: 데스크톱 <code>1.5rem</code> / 모바일 <code>1.2rem</code>)</li>
</ul>
</li>
<li><strong>폰트 굵기 (Font Weight)</strong>:<ul>
<li>로고의 시각적 존재감을 조절하기 위해 <code>100</code>부터 <code>900</code>까지 100 단위의 정밀한 두께 선택 옵션을 제공합니다.</li>
<li><em>주의 사항</em>: 적용된 폰트 종류에 따라 지원하는 굵기 단계가 서로 다릅니다. 선택한 웹 폰트가 특정 두께(예: 100 또는 900)를 제공하지 않을 경우 브라우저가 제공 가능한 가장 유사한 굵기로 대체 렌더링하므로, 사용하려는 서체의 굵기 제공 스펙을 미리 확인하는 것이 좋습니다.</li>
</ul>
</li>
</ul>
<h3 id="1-3-글로벌-위젯-스타일-설정">🧩 1-3. 글로벌 위젯 스타일 설정</h3>
<p>사이드바나 하단 영역 등에 배치되는 개별 위젯의 타이틀 및 구성 항목의 텍스트 스타일을 조정합니다.</p>
<ul>
<li><strong>글로벌 위젯 제목 스타일</strong>: 모든 위젯 상단에 노출되는 제목(타이틀) 영역의 폰트 종류, 글자 크기, 두께, 색상을 설정합니다.</li>
<li><strong>글로벌 위젯 항목 스타일</strong>: 위젯 카드를 구성하는 개별 내용물 및 링크 텍스트에 적용할 스타일을 설정합니다.</li>
<li><em>상속 규칙</em>: 글로벌 위젯 설정 역시 글꼴 이름을 공란으로 비워두게 되면 최상단 글로벌 폰트를 상속받아 깨짐 없는 통일성을 유지하게 됩니다.</li>
</ul>
<hr>
<h2 id="2-폰트-로딩-성능-최적화와-작동-메커니즘">⚡ 2. 폰트 로딩 성능 최적화와 작동 메커니즘</h2>
<p>블로그가 사용자 화면에 렌더링될 때 흔히 발생하는 폰트 로딩 지연 현상은 블로그 성능 점수를 크게 떨어뜨리는 주범입니다. 이를 예방하기 위해 본 엔진은 서버리스 환경에 특화된 고유의 폰트 가속 아키텍처를 내장하고 있습니다.</p>
<h3 id="2-1-시스템-내장-기본-글꼴-필터링">🛡️ 2-1. 시스템 내장 기본 글꼴 필터링</h3>
<p>방문자의 PC나 스마트폰 운영체제 내에 이미 기본 탑재되어 제공되는 글꼴(예: <code>Pretendard</code>, <code>Arial</code>, <code>system-ui</code> 등)의 경우, 블로그 서버가 구글 폰트 서버에 중복 요청을 보내거나 다운로드하지 않도록 서버 단에서 지능적으로 걸러내어 불필요한 네트워크 트래픽을 원천 차단합니다.</p>
<h3 id="2-2-외부-요청-차단-및-자체-프록시-중계">🔄 2-2. 외부 요청 차단 및 자체 프록시 중계</h3>
<p>일반적인 웹사이트처럼 외부 Google Fonts 주소(<code>fonts.googleapis.com</code> 및 <code>gstatic.com</code>)에 클라이언트 브라우저가 직접 접속하여 폰트를 가져오면 DNS 쿼리와 보안 인증을 새로 거쳐야 하므로 수백 밀리초의 렌더링 지연이 발생합니다.</p>
<ul>
<li><strong>중계 스타일시트 생성</strong>: 블로그 렌더링 시 외부 구글 서버 주소를 호출하는 대신 자체 엔진 API를 통해 폰트 스타일시트(CSS)를 우회하여 확보합니다.</li>
<li><strong>내부 링크 치환</strong>: 스타일시트 내부에서 실제 폰트 파일(.woff2)을 다운로드하는 경로를 분석한 뒤, 구글 서버의 외부 주소를 블로그 내부 도메인 프록시 주소로 <strong>일괄 교체</strong>합니다.</li>
<li><strong>보안 필터 탑재</strong>: 중계 API는 지정된 구글 공식 폰트 저장소 도메인 이외의 무단 외부 주소 접근을 엄격히 통제하여 임의의 유해 코드 릴레이가 일어날 수 없도록 안전하게 통제합니다.</li>
</ul>
<h3 id="2-3-cloudflare-pages-cdn-및-브라우저-로컬-캐싱">💾 2-3. Cloudflare Pages CDN 및 브라우저 로컬 캐싱</h3>
<p>자체 프록시를 통해 요청된 모든 폰트 명세 파일과 폰트 데이터(.woff2)는 Cloudflare의 글로벌 분산 에지 서버(CDN)에 1일 동안 영구 캐싱됩니다.</p>
<ul>
<li>또한 브라우저에게도 강력한 로컬 캐싱 명령 헤더를 실어 응답하므로, 사용자가 블로그 내의 다른 글을 클릭하거나 다음번에 다시 방문할 때는 인터넷 연결 속도와 관계없이 본인 기기 메모리에서 글꼴을 0ms 만에 즉시 렌더링하게 됩니다. </li>
<li>이를 통해 페이지 레이아웃이 폰트 로딩 속도 차이로 인해 덜컥거리며 깜빡이는 현상(CLS)을 완벽에 가깝게 차단해 줍니다.</li>
</ul>
<h3 id="2-4-pretendard-서체-특화-가속-적용">🚀 2-4. Pretendard 서체 특화 가속 적용</h3>
<p>한국어 가독성의 표준으로 자리 잡은 오픈소스 서체인 <code>Pretendard</code>를 기본 폰트로 선언할 경우, 블로그 렌더링 속도를 비약적으로 단축하기 위한 <strong>우회 가속 통로</strong>가 자동으로 개설됩니다.</p>
<ul>
<li><strong>병렬 다운로드 시너지</strong>: 구글 서버 및 중계 API 프록시망을 거치지 않고, 전 세계에 엣지가 분산된 초고속 오픈소스 CDN(jsDelivr) 정적 망을 통해 다른 스크립트와 함께 패러렐(Parallel)하게 폰트 파일을 끌어옵니다. </li>
<li><strong>렌더링 최적화</strong>: 한글 서체의 특성상 복잡하고 용량이 큰 바이너리를 가장 신속하게 확보하여, 첫 화면 진입 시 글자가 늦게 나타나거나 폰트가 나중에 바뀌는 현상을 근본적으로 예방합니다.</li>
</ul>
<hr>
<h2 id="3-실제-적용-시-주의-사항-및-추천-설정값">💡 3. 실제 적용 시 주의 사항 및 추천 설정값</h2>
<blockquote>
<p>[!IMPORTANT]<br><strong>💡 어드민 폰트 수정 시 꼭 기억해야 할 세 가지 행동 수칙</strong></p>
<ol>
<li><strong>구글 웹 폰트 이름의 정확한 철자 준수</strong>:<ul>
<li>추가할 구글 웹 폰트 항목에 입력하는 영어 이름은 구글 폰트 라이브러리에 등록된 공식 영문 명칭과 100% 동일해야 합니다. 철자가 틀리거나 띄어쓰기가 비정상적일 경우, 로딩 엔진이 폰트를 찾지 못해 시스템 기본 대체 글꼴(Sans-serif)로 화면이 강제 전환될 수 있습니다.</li>
</ul>
</li>
<li><strong>데스크톱과 모바일의 글자 크기 균형 맞추기</strong>:<ul>
<li>모바일 뷰에서도 데스크톱 기본 글자 크기를 그대로 가져가면 화면 가독성이 나빠집니다. 모바일 크기 항목에 반드시 <code>2px</code> 정도 더 작은 값을 개별 바인딩하여 반응형 인터페이스를 부드럽게 유지하십시오.</li>
</ul>
</li>
<li><strong>안정적인 한국어 기본 대체 폰트(Fallback) 선언</strong>:<ul>
<li>글로벌 폰트 필드에 커스텀 폰트를 적용할 때 <code>&#39;Noto Serif KR&#39;, serif</code>처럼 가장 뒤에 시스템 범용 대체 이름(<code>serif</code> 또는 <code>sans-serif</code>)을 함께 나열해주면 폰트 로드가 일시적으로 지연되는 찰나의 순간에도 글꼴이 깨지지 않고 안정적으로 화면을 렌더링할 수 있습니다.</li>
</ul>
</li>
</ol>
</blockquote>
]]></content:encoded>
            <category>상세 메뉴얼</category>
            <enclosure url="https://sveltekitblog.com/images/posts/setup-guide/desktop/img-detail-manual-setup-guide-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[CLI 환경에서 Cloudflare Wrangler 로그인 및 API 토큰 설정 상세 설명서]]></title>
            <link>https://testblog-6br.pages.dev/detail-manual/wrangler-login-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/detail-manual/wrangler-login-guide</guid>
            <pubDate>Mon, 06 Jul 2026 10:49:33 GMT</pubDate>
            <description><![CDATA[브라우저 연동이 불가능한 SSH 원격 서버 및 CI/CD 환경에서 API 토큰을 발급하여 Wrangler의 비대화형 인증 세션을 구축하는 방법을 완벽히 마스터합니다.]]></description>
            <content:encoded><![CDATA[<p><img src="https://sveltekitblog.com/images/posts/setup-guide/desktop/img-detail-manual-setup-guide-ko-001.webp" alt="img-detail-manual-setup-guide-ko-001"></p>
<h1 id="cli-환경에서-cloudflare-wrangler-로그인-및-api-토큰-설정-상세-설명서">🔑 CLI 환경에서 Cloudflare Wrangler 로그인 및 API 토큰 설정 상세 설명서</h1>
<p>본 프로젝트는 단일 명령어로 Cloudflare의 데이터베이스, 저장소, 빌드 파이프라인을 구축해주는 자동화 셋업 스크립트를 제공합니다. </p>
<p>이 스크립트는 내부적으로 Cloudflare의 명령행 도구인 <code>wrangler</code> CLI를 사용합니다. 일반적인 로컬 개발 PC 환경에서는 웹 브라우저를 통한 대화형 로그인(<code>npx wrangler login</code>)이 즉시 실행되지만, **브라우저를 열 수 없는 완전한 CLI 환경(예: 원격 SSH Linux 서버, headless 가상 머신, CI/CD 배포 파이프라인)**에서는 로그인 세션을 대화형으로 수립할 수 없는 제약이 발생합니다.</p>
<p>본 매뉴얼은 이러한 환경 제약을 극복하기 위해 <strong>Cloudflare API 토큰</strong>을 활용하여 Wrangler의 비대화형 인증 세션을 안전하게 구축하는 방법을 상세히 안내합니다.</p>
<hr>
<h2 id="1-도메인-설정-전후에-따른-필수-최소-권한-구분">📂 1. 도메인 설정 전후에 따른 필수 최소 권한 구분</h2>
<p>API 토큰을 발급받기 전에, 현재 본인의 인프라 배포 단계를 파악하는 것이 중요합니다. 불필요한 보안 위협을 낮추기 위해 도메인 연결 상태에 따라 필요한 권한을 분리하여 적용합니다.</p>
<h3 id="1-1-초기-배포-단계-자체-도메인-연동-전">🔌 1-1. 초기 배포 단계 (자체 도메인 연동 전)</h3>
<p>프로젝트를 처음 배포하여 Cloudflare Pages가 제공하는 기본 도메인인 <code>*.pages.dev</code> 주소만 사용할 경우, 브라우저를 통해 연동하는 일반 로그인 상태와 동일하게 <strong>Zone(도메인) 수준의 어떠한 권한도 필요치 않습니다.</strong> 오직 <strong>계정(Account) 수준의 핵심 리소스 권한</strong>만으로 셋업 스크립트 실행과 D1 마이그레이션이 문제없이 가동됩니다.</p>
<ul>
<li><strong>필수 최소 권한 목록</strong>:<ul>
<li><code>D1</code> — <strong>Edit</strong> (D1 DB 생성, 스키마 마이그레이션 실행 및 쿼리 작동용)</li>
<li><code>Workers KV Storage</code> — <strong>Edit</strong> (블로그 미디어 업로드용 KV 네임스페이스 제어용)</li>
<li><code>Cloudflare Pages</code> — <strong>Edit</strong> (모노레포의 블로그 및 어드민 프로젝트 배포용)</li>
<li><code>Workers Scripts</code> — <strong>Edit</strong> (Workers 스크립트 및 CLI 설정 제어용)</li>
</ul>
</li>
</ul>
<h3 id="1-2-자체-도메인-연동-및-cdn-에지-캐시-사용-단계-추후-설정">🌐 1-2. 자체 도메인 연동 및 CDN 에지 캐시 사용 단계 (추후 설정)</h3>
<p>블로그에 개인 도메인을 연결하고, 성능 고도화를 위해 에지 CDN 캐시를 강제 비우는(Purge) 기능 등을 활성화할 때 비로소 <strong>Zone 수준 권한</strong>이 요구됩니다.</p>
<ul>
<li><strong>추가 필수 권한</strong>:<ul>
<li><code>Zone: Cache Purge</code> — <strong>Edit</strong> (배포 완료 시점이나 콘텐츠 갱신 시 CDN 에지 서버의 캐시를 즉시 제거하기 위해 필요)</li>
</ul>
</li>
<li><strong>보안 모범 사례</strong>: 토큰의 스코프를 제한할 때, <strong>Zone Resources</strong>를 &#39;All Zones&#39;로 열지 말고 사용 중인 특정 고유 도메인(Zone)으로 스코프를 한정하여 토큰을 발급받는 것이 안전합니다.</li>
<li>*참고: 자체 도메인 세팅 방법 및 캐시 제어 메커니즘의 세부 정보는 추후 별도로 발행될 **&#39;자체 도메인 설정에 관한 상세 설명서&#39;*<em>에서 깊이 있게 다룰 예정이므로, 초기 구축 단계인 지금은 해당 설정을 생략하고 진행하셔도 무방합니다.</em></li>
</ul>
<hr>
<h2 id="2-cloudflare-api-토큰-생성-절차">🛠️ 2. Cloudflare API 토큰 생성 절차</h2>
<p>비대화형 로그인을 가능케 하는 토큰은 Cloudflare 대시보드에서 다음과 같은 순서로 간단히 생성할 수 있습니다.</p>
<ol>
<li><strong>Cloudflare 대시보드 로그인</strong>:<ul>
<li><a href="https://dash.cloudflare.com/">Cloudflare Dashboard</a>에 접속하여 로그인합니다.</li>
</ul>
</li>
<li><strong>토큰 생성 메뉴 진입</strong>:<ul>
<li>우측 상단 프로필 아이콘을 클릭하고 **[My Profile] (프로필)**을 선택합니다.</li>
<li>좌측 메뉴에서 **[API Tokens] (API 토큰)**을 클릭하고, 우측의 <strong>[Create Token] (토큰 생성)</strong> 버튼을 클릭합니다.</li>
</ul>
</li>
<li><strong>사용자 지정 토큰 설정</strong>:<ul>
<li>여러 템플릿 중 최하단의 <strong>[Create Custom Token] (사용자 지정 토큰 생성)</strong> 영역의 **[Get Started] (시작하기)**를 선택합니다.</li>
</ul>
</li>
<li><strong>권한(Permissions) 할당</strong>:<ul>
<li><strong>Token Name</strong>: 구분이 쉬운 이름 입력 (예: <code>sveltekit-blog-cli-token</code>)</li>
<li><strong>Permissions</strong>:<ul>
<li><code>계정</code> (Account) -&gt; <code>D1</code> -&gt; <code>편집</code> (Edit)</li>
<li><code>계정</code> (Account) -&gt; <code>Workers KV 저장소</code> (Workers KV Storage) -&gt; <code>편집</code> (Edit)</li>
<li><code>계정</code> (Account) -&gt; <code>Cloudflare Pages</code> -&gt; <code>편집</code> (Edit)</li>
<li><code>계정</code> (Account) -&gt; <code>Workers 스크립트</code> (Workers Scripts) -&gt; <code>편집</code> (Edit)</li>
</ul>
</li>
</ul>
</li>
<li><strong>스코프 제한</strong>:<ul>
<li><strong>계정 리소스</strong> (Account Resources): <code>포함</code> (Include) -&gt; 본인의 <code>Cloudflare 계정 선택</code> (권한 범위를 내 계정 내부로만 좁혀 보안 위협을 격리합니다.)</li>
</ul>
</li>
<li><strong>생성 완료 및 보관</strong>:<ul>
<li>**[Continue to summary] (요약 계속)**를 누른 뒤 **[Create Token] (토큰 생성)**을 클릭하여 생성된 고유 토큰 값(알파벳과 기호로 구성된 긴 키 문자열)을 안전한 곳에 복사해 둡니다. (이 값은 재확인이 불가하므로 유실 시 다시 발급받아야 합니다.)</li>
</ul>
</li>
</ol>
<hr>
<h2 id="3-발급된-api-토큰-적용-및-환경변수-주입법">💻 3. 발급된 API 토큰 적용 및 환경변수 주입법</h2>
<p>인터랙티브 브라우저 로그인 창을 띄울 수 없으므로, 시스템 환경변수에 발급받은 토큰 값을 주입하여 Wrangler가 자동으로 이를 조회하고 인증을 수립하게 유도해야 합니다.</p>
<h3 id="3-1-os-환경별-터미널-환경변수-주입-커맨드">3-1. OS 환경별 터미널 환경변수 주입 커맨드</h3>
<p>셋업 스크립트를 구동할 터미널 콘솔 창에 OS 유형별로 아래의 명령어를 입력합니다.</p>
<ul>
<li><strong>Linux / macOS / Git Bash (Bash Shell)</strong>:<pre><code class="language-bash">export CLOUDFLARE_API_TOKEN=&quot;발급받은_토큰_값&quot;
</code></pre>
</li>
<li><strong>Windows Command Prompt (CMD)</strong>:<pre><code class="language-cmd">set CLOUDFLARE_API_TOKEN=발급받은_토큰_값
</code></pre>
</li>
<li><strong>Windows PowerShell</strong>:<pre><code class="language-powershell">$env:CLOUDFLARE_API_TOKEN=&quot;발급받은_토큰_값&quot;
</code></pre>
</li>
</ul>
<blockquote>
<p>[!NOTE]<br><strong>💻 macOS 환경에서의 실행 및 테스트 안내</strong></p>
<ul>
<li>본 가이드는 <strong>Windows 10/11 및 Linux(Ubuntu 22.04 LTS)</strong> 물리적 콘솔 환경에서 직접 명령행 구동 및 토큰 연동 유효성을 사전에 전수 검증하였습니다.</li>
<li>다만 작성자 개인의 개발 장비 부재로 인해 macOS(맥북 환경)에서의 실기기 테스트는 거치지 못했습니다. 크로스 플랫폼 CLI 동작 원리상 macOS에서도 Bash/Zsh 터미널을 통해 동일하게 <code>export</code> 구문으로 완벽히 정상 가동될 것으로 판단됩니다.</li>
</ul>
</blockquote>
<h3 id="3-2-로컬-개발-환경-파일-연동-및-cicd-연동">3-2. 로컬 개발 환경 파일 연동 및 CI/CD 연동</h3>
<ul>
<li><strong>로컬 개발 (.dev.vars)</strong>:<br>매번 터미널 창을 열 때마다 환경변수를 치기 번거롭다면, <code>apps/blog/.dev.vars</code> 및 <code>apps/admin/.dev.vars</code> 파일에 아래와 같이 등록해 두면 로컬 <code>wrangler dev</code> 구동 시 해당 파일에서 토큰을 자동으로 파싱하여 바인딩 처리합니다.<pre><code class="language-env">CLOUDFLARE_API_TOKEN=&quot;발급받은_토큰_값&quot;
</code></pre>
</li>
<li><strong>CI/CD 파이프라인 (GitHub Actions)</strong>:<br>깃허브 레포지토리의 <code>Settings</code> -&gt; <code>Secrets and variables</code> -&gt; <code>Actions</code> -&gt; <code>Repository secrets</code> 메뉴로 가셔서 <code>CLOUDFLARE_API_TOKEN</code> 이라는 이름으로 발급받은 토큰을 추가한 뒤, yaml 워크플로우 파일에서 아래와 같이 활용할 수 있습니다.<pre><code class="language-yaml">- name: Build and Deploy SvelteKit Apps
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
  run: npm run deploy:blog
</code></pre>
</li>
</ul>
<hr>
<h2 id="4-토큰-인증-상태-검증-및-디버깅">🔍 4. 토큰 인증 상태 검증 및 디버깅</h2>
<p>토큰 환경변수가 시스템 세션 상에 안전하게 주입되어 정상적으로 바인딩되었는지 확인하기 위한 디버깅 커맨드입니다.</p>
<h3 id="4-1-wrangler-whoami">4-1. wrangler whoami</h3>
<p>터미널에서 아래 명령을 내려 토큰이 유효한 계정을 매칭하고 있는지 검증합니다.</p>
<pre><code class="language-bash">npx wrangler whoami
</code></pre>
<ul>
<li><strong>검증 통과 시</strong>: 현재 토큰이 소속된 계정 정보(Account Name, Account ID) 및 D1, KV 등의 제어 권한 바인딩 목록이 콘솔에 깔끔하게 성공 출력됩니다.</li>
<li><strong>로그인 세션 정리 (wrangler logout)</strong>:<br>만약 이전에 로컬 웹 브라우저 로그인 방식으로 맺어놓은 세션 찌꺼기가 남아 토큰과 간섭을 일으키는 것 같다면, 먼저 로그아웃 명령어를 수행하여 로컬 컴퓨터 내의 기존 인증 세션을 완전히 초기화한 후 환경변수를 다시 적용하는 것을 권장합니다.<pre><code class="language-bash">npx wrangler logout
</code></pre>
</li>
</ul>
<hr>
<h2 id="5-자주-마주치는-문제-및-해결-방안-troubleshooting">⚠️ 5. 자주 마주치는 문제 및 해결 방안 (Troubleshooting)</h2>
<h3 id="1-authentication-error-code-10000">🚨 1. Authentication Error (Code: 10000)</h3>
<ul>
<li><strong>원인</strong>: 셋업 스크립트 실행 중 Cloudflare 인증 실패 오류가 표시되는 상황입니다.</li>
<li><strong>해결</strong>: 복사하여 주입한 <code>CLOUDFLARE_API_TOKEN</code> 값 내에 오탈자나 공백이 포함되었는지 확인하세요. 또한, 대시보드에서 토큰을 발급받을 때 지정한 <code>Account Resources</code> 스코프가 현재 접속 중인 계정과 일치하는지 재검토해야 합니다.</li>
</ul>
<h3 id="2-forbidden-권한-거부-에러">🚨 2. Forbidden (권한 거부 에러)</h3>
<ul>
<li><strong>원인</strong>: D1 마이그레이션 중에는 에러가 없으나 Pages 배포 또는 KV 리소스 생성 중 에러가 발생하는 경우입니다.</li>
<li><strong>해결</strong>: 생성한 토큰의 Permissions 리스트에 <code>Cloudflare Pages -&gt; Edit</code> 또는 <code>Workers KV Storage -&gt; Edit</code> 권한 중 누락된 권한이 없는지 대시보드 API 토큰 목록에서 확인하고 권한을 추가 수정한 뒤 다시 환경변수 주입을 시도하세요.</li>
</ul>
]]></content:encoded>
            <category>상세 메뉴얼</category>
            <enclosure url="https://sveltekitblog.com/images/posts/setup-guide/desktop/img-detail-manual-setup-guide-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[SvelteKit Monorepo + Cloudflare 원클릭 배포 및 자동 셋업 상세 설명서]]></title>
            <link>https://testblog-6br.pages.dev/detail-manual/setup-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/detail-manual/setup-guide</guid>
            <pubDate>Mon, 06 Jul 2026 10:23:44 GMT</pubDate>
            <description><![CDATA[npm run setup 명령어를 통해 D1 DB, KV Namespace, Pages 프로젝트 생성부터 스키마 마이그레이션, 비밀 환경 변수 동기화 및 최종 배포까지 모든 과정을 원클릭으로 자동화하는 방식을 깊이 있게 다룹니다.]]></description>
            <content:encoded><![CDATA[<p><img src="https://sveltekitblog.com/images/posts/setup-guide/desktop/img-detail-manual-setup-guide-ko-001.webp" alt="img-detail-manual-setup-guide-ko-001"></p>
<h1 id="sveltekit-monorepo-cloudflare-원클릭-배포-및-자동-셋업-상세-설명서">🚀 SvelteKit Monorepo + Cloudflare 원클릭 배포 및 자동 셋업 상세 설명서</h1>
<p>본 프로젝트는 스벨트킷(SvelteKit) 모노레포 환경을 기반으로 하며, 서버리스 데이터베이스인 <strong>Cloudflare D1</strong>과 <strong>KV Namespace</strong>, 그리고 <strong>Cloudflare Pages</strong> 배포 인프라를 유기적으로 사용합니다.</p>
<p>모노레포의 다중 애플리케이션 구조에서 발생할 수 있는 데이터베이스 연결 유실, 환경 변수 불일치, 수동 CLI 바인딩 설정 복잡성을 최소화하기 위해 <strong>원클릭 자동화 셋업 스크립트</strong>가 기본 내장되어 있습니다. 이 가이드는 스크립트 실행 전 준비해야 하는 로컬 환경 설정부터 실행 시 주어지는 터미널 선택 프롬프트의 의미와 모드별 활용법을 상세히 소개합니다.</p>
<hr>
<h2 id="1-스크립트-실행-전-사전-준비사항">📂 1. 스크립트 실행 전 사전 준비사항</h2>
<p>셋업 스크립트는 실행 중 불필요한 오류로 작동이 멈추는 것을 방지하기 위해 <strong>Step 0 단계</strong>에서 필수 로컬 환경 변수(<code>.dev.vars</code>)가 올바르게 준비되었는지 사전 검증합니다. 스크립트를 구동하기 전에 반드시 아래의 조치를 완료해야 합니다.</p>
<h3 id="1-1-devvars-파일-생성-및-필수-값-설정">🔑 1-1. <code>.dev.vars</code> 파일 생성 및 필수 값 설정</h3>
<p>프로젝트 루트 기준, 블로그 앱과 어드민 앱 각각의 경로에 <code>.dev.vars.example</code> 파일을 복사하여 <code>.dev.vars</code> 파일을 생성하고 적절한 비밀값을 기입해 주세요.</p>
<ol>
<li><p><strong>블로그 서비스 환경 변수</strong></p>
<ul>
<li><strong>위치</strong>: <code>apps/blog/.dev.vars</code></li>
<li><strong>필수 설정 항목</strong>:<pre><code class="language-env">BETTER_AUTH_SECRET=&quot;임의의_긴_비밀_문자열&quot;
</code></pre>
<em>(인증 세션 및 토큰을 암호화하기 위해 사용되는 유일하고 복잡한 비밀 문자열을 기입합니다. 비어있거나 따옴표만 존재하면 검증이 실패합니다.)</em></li>
</ul>
</li>
<li><p><strong>어드민 서비스 환경 변수</strong></p>
<ul>
<li><strong>위치</strong>: <code>apps/admin/.dev.vars</code></li>
<li><strong>필수 설정 항목</strong>:<pre><code class="language-env">ADMIN_PASSWORD=&quot;어드민_접속_비밀번호&quot;
</code></pre>
<em>(어드민 대시보드 로그인 시 사용할 비밀번호를 기입합니다.)</em></li>
<li><strong>GA4 및 애드센스(AdSense) 더미 데이터 보기</strong>:<br>어드민 대시보드 메인 화면에서 Google Analytics 4(GA4) 방문자 지표 및 Google AdSense 실적 리포트의 **더미 데이터(시뮬레이션 예시)**를 확인하여 화면 출력을 테스트하고 싶다면, 로컬 <code>.dev.vars</code> 파일 및 원격 Cloudflare Pages 설정에서 구글 연동 관련 환경변수들(<code>GA4_PROPERTY_ID</code>, <code>ADSENSE_ACCOUNT_ID</code> 등)을 **등록하지 않거나 완전히 삭제(혹은 주석 처리)**해야 합니다. 관련 필수 변수 중 하나라도 비어 있으면 시스템이 자동으로 더미 데이터를 생성해 출력합니다.</li>
</ul>
</li>
</ol>
<blockquote>
<p>[!WARNING]<br>만약 파일이 없거나 필수 값이 비어 있다면 터미널에 아래와 같은 경고 메시지가 출력되며 셋업이 중단됩니다.</p>
<pre><code class="language-text">❌ apps/admin/.dev.vars 파일이 존재하지 않습니다.
   (apps/admin/.dev.vars.example 파일을 복사하여 생성하고 ADMIN_PASSWORD를 설정해주세요.)
</code></pre>
</blockquote>
<hr>
<h2 id="2-명령어-종류-및-실행-방법">🛠️ 2. 명령어 종류 및 실행 방법</h2>
<table>
<thead>
<tr>
<th align="left">명령어</th>
<th align="left">실행 목적</th>
<th align="left">특징</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><code>npm run setup</code></td>
<td align="left"><strong>기본 전체 자동 설치 및 배포</strong></td>
<td align="left">초기 기본 언어만 설정하면 리소스 생성부터 최종 빌드/배포까지 논스톱으로 자동 진행됩니다.</td>
</tr>
<tr>
<td align="left"><code>npm run setup:select</code></td>
<td align="left"><strong>사용자 지정 직접 선택 설정</strong></td>
<td align="left">배포 프로젝트명, 데이터 유지/삭제 여부, 리소스명 변경, CDN 캐시 퍼지 연동 등을 조율합니다.</td>
</tr>
<tr>
<td align="left"><code>npm run restore</code></td>
<td align="left"><strong>백업 복원 및 보안 동기화</strong></td>
<td align="left"><code>wrangler.backup.json</code> 백업 파일을 참조하여 인프라를 안전하게 재정렬하고 배포합니다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-실행-단계별-터미널-선택-가이드-prompt-guide">💬 3. 실행 단계별 터미널 선택 가이드 (Prompt Guide)</h2>
<p>스크립트 실행 중 터미널 화면에 나타나는 실제 한글/영어 선택 프롬프트와 각 선택지가 인프라에 미치는 영향에 대한 가이드입니다.</p>
<h3 id="3-1-공통-초기-프롬프트-npm-run-setup-amp-setupselect-공통">🌐 3-1. 공통 초기 프롬프트 (npm run setup &amp; setup:select 공통)</h3>
<h4 id="q1-기본-블로그-언어-선택-기본-콘텐츠-언어-설정">Q1. 기본 블로그 언어 선택 (기본 콘텐츠 언어 설정)</h4>
<p>D1 데이터베이스에 최초로 주입될 초기 가이드 게시글 및 기본 데이터의 언어를 결정합니다.</p>
<pre><code class="language-text">  [EN] Select default blog language:
  [KO] 기본 블로그 언어를 선택하세요:
  1) Korean / 한국어 (ko) [Default]
  2) English / 영어 (en)
  3) Japanese / 日本語 (ja)
  [EN] Choose option (1/2/3) [Default: 1]
  [KO] 옵션을 선택하세요 (1/2/3) [기본값: 1]
  &gt; 
</code></pre>
<ul>
<li><strong>가이드</strong>: 기본값인 <code>1</code>을 선택하면 한국어로 구성된 기본 시드 데이터가 생성되며, <code>2</code> 또는 <code>3</code> 입력 시 각각 영어/일본어로 번역된 초기 가이드 데이터가 주입됩니다.</li>
</ul>
<h4 id="q2-진행-방식-모드-선택">Q2. 진행 방식 모드 선택</h4>
<p>남은 셋업 과정을 자동으로 완전히 실행할지, 아니면 하나씩 커스텀하여 입력할지 결정합니다.</p>
<pre><code class="language-text">  [EN] Choose setup mode for the remaining steps:
  [KO] 나머지 설정의 진행 방식을 선택하세요:
  1) Full Auto Setup &amp; Deploy / 풀 자동 설정 및 배포 [Default]
  2) Custom Interactive Setup / 사용자 지정 직접 선택 설정
  [EN] Choose option (1/2) [Default: 1]
  [KO] 옵션을 선택하세요 (1/2) [기본값: 1]
  &gt; 
</code></pre>
<ul>
<li><strong>1) Full Auto Setup &amp; Deploy (기본값)</strong>: 블로그 및 어드민 배포명을 기본 규격에 맞춰 생성하고, Cloudflare 로그인 상태만 검증한 뒤 모든 리소스 바인딩과 마이그레이션, 배포를 일사천리로 마칩니다.</li>
<li><strong>2) Custom Interactive Setup</strong>: 상세 커스텀 설정 모드로 전환되며, 아래의 추가 질문들(Q3 ~ Q8)이 연이어 제공됩니다. (<code>npm run setup:select</code> 명령어 실행 시 이 모드로 바로 진입합니다.)</li>
</ul>
<hr>
<h3 id="3-2-상세-커스텀-설정-프롬프트-npm-run-setupselect-전용">🎨 3-2. 상세 커스텀 설정 프롬프트 (<code>npm run setup:select</code> 전용)</h3>
<blockquote>
<p>[!TIP]<br>상세 커스텀 설정 과정에서 나타나는 모든 질문 프롬프트는 값을 별도로 기입하지 않고 <strong>그냥 엔터(Enter)만 쳐서 넘어가도</strong> 대괄호 <code>[Default: ...]</code> 안에 적힌 안전한 기본값으로 자동 설정되므로 간편하게 진행할 수 있습니다.</p>
</blockquote>
<h4 id="q3-배포-프로젝트명-커스텀">Q3. 배포 프로젝트명 커스텀</h4>
<p>Cloudflare Pages 상에 고유하게 생성될 배포 프로젝트 이름을 지정합니다. (기본 도메인은 <code>https://[프로젝트명].pages.dev</code> 형태로 자동 설정됩니다.)</p>
<pre><code class="language-text">  [EN] Enter Blog project name [Default: svelteblog]
  [KO] 블로그 배포명을 입력하세요
  &gt; 

  [EN] Enter Admin project name [Default: svelteadmin]
  [KO] 어드민 배포명을 입력하세요
  &gt; 
</code></pre>
<ul>
<li><strong>가이드</strong>: 공백 상태로 엔터를 누르면 대괄호 내의 기본 이름이 사용됩니다. 본인의 Cloudflare 계정 내 타 프로젝트와 명칭이 충돌되지 않는 고유한 이름을 사용하고 싶다면 수동으로 기입합니다. (입력 시 <code>package.json</code>의 <code>deploy:blog</code>, <code>deploy:admin</code> 스크립트도 해당 프로젝트 명으로 동적 자동 수정됩니다.)</li>
</ul>
<h4 id="q4-cloudflare-계정-로그인-진행-여부">Q4. Cloudflare 계정 로그인 진행 여부</h4>
<p>Cloudflare API 제어를 위한 인증 과정을 실행합니다.</p>
<pre><code class="language-text">  [EN] Run wrangler login?
  [KO] Wrangler 로그인을 진행할까요?
  [EN] (Y/n) [Default: Y]
  [KO] (Y/n) [기본값: Y]
  &gt; 
</code></pre>
<ul>
<li><strong>가이드</strong>: 로그인이 되어 있지 않다면 브라우저를 띄워 Cloudflare 계정 연동을 진행합니다. 이미 유효한 로그인이 감지된 상태라면 스크립트가 알아서 해당 세션을 파악하고 넘어가지만, 다른 계정으로 다시 로그인하고 싶다면 <code>Y</code>를 입력할 수 있습니다.</li>
</ul>
<h4 id="q5-데이터-초기화-모드-설정-fresh-vs-keep">Q5. 데이터 초기화 모드 설정 (Fresh vs Keep)</h4>
<p>기존에 구축하여 사용 중이던 Cloudflare 리소스(D1, KV)의 보존 여부를 묻습니다.</p>
<pre><code class="language-text">  [EN] Select Initialization Mode:
    1: Fresh Install (DELETE existing Cloudflare D1/KV databases and start clean)
    2: Keep Existing Data (Use existing Cloudflare databases to preserve your posts/data)
  [KO] 초기화 모드 선택:
    1: 완전 초기화 (기존 클라우드플레어의 D1/KV 데이터베이스를 모두 삭제하고 새로 생성합니다. 데이터 유실 주의!)
    2: 기존 데이터 유지 (기존에 구축된 클라우드플레어 리소스를 그대로 유지하고 연동합니다.)

  [EN] Choose an option (1/2) [Default: 2]
  [KO] 옵션을 선택하세요 (1/2) [기본값: 2]
  &gt; 
</code></pre>
<ul>
<li><strong>Fresh Install (1)</strong>: 기존 Cloudflare에 올라간 D1 DB와 KV Namespace 데이터를 <strong>완전히 영구 삭제</strong>하고 새로 설치합니다. 테스트 중 완전히 빈 상태로 되돌리고 싶을 때만 선택해야 합니다.</li>
<li><strong>Keep Existing Data (2, 권장/기본값)</strong>: 이미 데이터베이스에 등록된 게시글, 회원 등의 데이터를 유지한 채로 새 배포 환경 정보 바인딩만 연결합니다.</li>
</ul>
<h4 id="q6-cloudflare-리소스명-직접-지정">Q6. Cloudflare 리소스명 직접 지정</h4>
<p>Cloudflare 대시보드에 실제로 표시될 D1 및 KV 인스턴스의 이름을 지정합니다.</p>
<pre><code class="language-text">  [EN] Enter Blog DB name [Default: blog-db-xxxxxx]
  [KO] 블로그 DB 이름을 입력하세요
  &gt; 

  [EN] Enter User DB name [Default: user-db-xxxxxx]
  [KO] 유저 DB 이름을 입력하세요
  &gt; 

  [EN] Enter Images KV name [Default: blog-images-kv-xxxxxx]
  [KO] 이미지 KV 이름을 입력하세요
  &gt; 
</code></pre>
<ul>
<li><strong>가이드</strong>: 별도로 고정하여 관리하고자 하는 DB 명칭이 있다면 입력해 주시고, 그렇지 않다면 그대로 엔터를 눌러 랜덤 서픽스(<code>xxxxxx</code>)가 부여된 고유 이름으로 자동 생성되도록 합니다.</li>
</ul>
<h4 id="q7-cloudflare-cdn-캐시-퍼지-등록">Q7. Cloudflare CDN 캐시 퍼지 등록</h4>
<p>블로그 게시글 수정/삭제 시 배포 도메인의 에지(Edge) 캐시를 즉시 무효화하여 독자에게 최신 본문을 즉시 서빙하기 위한 CDN 동기화 인증 수단을 등록합니다.</p>
<pre><code class="language-text">  [EN] Configure Cloudflare CDN cache purge credentials?
  [KO] CDN 캐시 퍼지 설정을 등록하시겠습니까?
  [EN] (y/N) [Default: N]
  [KO] (y/N) [기본값: N]
  &gt; 
</code></pre>
<ul>
<li><strong>가이드</strong>: <code>y</code>를 누르면 Cloudflare 대시보드 화면 우측 하단에서 획득 가능한 <code>Zone ID</code>와 <code>API Token</code> 입력 단계가 시작됩니다. 입력 시 로컬 <code>.dev.vars</code> 파일에 해당 키값이 안전하게 주입되고 Cloudflare Pages의 환경변수로도 벌크 자동 등록됩니다.</li>
</ul>
<h4 id="q8-빌드-및-즉시-배포-여부">Q8. 빌드 및 즉시 배포 여부</h4>
<p>인프라 설정 완료 후 즉시 프로젝트 빌드 및 Cloudflare Pages 서버로 배포 전송을 수행할지 결정합니다.</p>
<pre><code class="language-text">  [EN] Deploy directly?
  [KO] 지금 바로 배포하시겠습니까?
  [EN] (Y/n) [Default: Y]
  [KO] (Y/n) [기본값: Y]
  &gt; 
</code></pre>
<ul>
<li><strong>가이드</strong>: 로컬 설정을 완료하고 즉시 실 배포 사이트에 반영하고 싶다면 <code>Y</code>를 누릅니다. 빌드가 진행된 뒤 최종 서비스 도메인 URL이 화면에 출력됩니다. 만약 리소스 바인딩 파일(<code>wrangler.json</code>)만 세팅하고 싶다면 <code>n</code>을 입력합니다.</li>
</ul>
<hr>
<h3 id="3-3-배포-동기화-시-조건별-추가-프롬프트-ip-및-다국어-사전">🔒 3-3. 배포 동기화 시 조건별 추가 프롬프트 (IP 및 다국어 사전)</h3>
<p>빌드 및 원격 배포 단계(<code>sync-secrets.js</code>)에 진입하면 추가적으로 관리 환경 보안과 번역 사전 동기화를 위한 판단을 요청받게 됩니다.</p>
<h4 id="q9-어드민-접속-ip-불일치-감지-및-처리">Q9. 어드민 접속 IP 불일치 감지 및 처리</h4>
<p>어드민 서비스는 외부 공격을 차단하기 위해 화이트리스트 기반의 IP 접근 제어(<code>ALLOWED_IP</code>)를 사용합니다. 스크립트가 로컬 PC의 외부 퍼블릭 IP 주소를 실시간 자동 추적하여 기존 설정값과 비교하고 불일치 시 아래의 질문을 던집니다.</p>
<pre><code class="language-text">  1) Keep existing allowed IPs / 기존 목록 유지 (현재 기기 미등록) [Default]
  2) Add current machine IP to list / 현재 기기 IP를 목록에 추가 (복수 허용)
  3) Replace with current machine IP / 현재 기기 IP로만 갱신 (기존 목록 제거)

  [EN] Select option (1/2/3) [Default: 1]
  [KO] 옵션을 선택하세요 (1/2/3) [기본값: 1]
  &gt; 
</code></pre>
<ul>
<li><strong>1) Keep existing allowed IPs (기본값)</strong>: 현재 배포 기기의 IP를 어드민 접속 허용 목록에 추가하지 않고 기존 설정대로 둡니다. (만약 현재 사용 중인 인터넷망이 다른 곳이라면 배포 후 어드민 접속이 차단될 수 있습니다.)</li>
<li><strong>2) Add current machine IP to list</strong>: 기존에 입력되어 있던 다른 주소들을 콤마(<code>,</code>) 구분자로 남겨둔 채, 현재 배포 기기의 퍼블릭 IP를 허용 목록에 <strong>추가</strong>합니다. 여러 장소(예: 집, 사무실)에서 동시에 접근하고자 할 때 최적의 옵션입니다.</li>
<li><strong>3) Replace with current machine IP</strong>: 기존에 등록되었던 허용 목록을 싹 지우고, 오직 현재 접속 중인 이 기기의 외부 IP 주소로만 화이트리스트를 갱신합니다. 단일 환경 보안을 극대화할 때 유용합니다.</li>
</ul>
<h4 id="q10-데이터베이스-다국어-번역-사전ui-dictionary-갱신-여부">Q10. 데이터베이스 다국어 번역 사전(ui_dictionary) 갱신 여부</h4>
<p>어드민 앱 배포 시, 로컬 공유 모듈(<code>packages/shared/src/i18n/index.ts</code>)에 개발자가 정의한 다국어 기본 사전 개체(<code>fallbackDictionary</code>)를 원격 D1 데이터베이스의 <code>blog_settings</code> 테이블에 덮어쓸지 확인합니다.</p>
<pre><code class="language-text">[EN] Force sync remote D1 ui_dictionary with local index.ts? (Web changes will be lost)
[KO] D1 DB의 ui_dictionary를 로컬 index.ts 기준으로 강제 덮어쓰시겠습니까? (웹 수정본 유실 주의)
  [EN] Select option (y/N) [Default: N]
  [KO] 옵션을 선택하세요 (y/N) [기본값: N]
  &gt; 
</code></pre>
<ul>
<li><strong>가이드</strong>: 웹 어드민 화면 내의 &#39;설정 대시보드&#39; 등에서 사용자가 직접 번역 문구를 수정한 이력이 있다면, 로컬 소스코드로 덮어쓰는 순간 웹 수정본이 <strong>유실</strong>될 수 있으므로 <code>N</code>을 권장합니다. 로컬 소스코드의 번역 데이터 수정본을 강제로 원격 서버에 동기화하고자 할 때만 <code>y</code>를 누릅니다. (단, <code>npm run setup</code> 초기 실행 단계에서는 데이터 정합성을 위해 자동 동기화 처리됩니다.)</li>
</ul>
<hr>
<h2 id="4-백업-및-복원-메커니즘-npm-run-restore">🔄 4. 백업 및 복원 메커니즘 (<code>npm run restore</code>)</h2>
<p>셋업이 성공적으로 끝나면 프로젝트 루트에 <code>wrangler.backup.json</code> 파일이 자동 생성됩니다.</p>
<pre><code class="language-json">{
    &quot;d1&quot;: {
        &quot;BLOG_DB&quot;: { &quot;name&quot;: &quot;blog-db-xxxxxx&quot;, &quot;id&quot;: &quot;d1-uuid-value&quot; },
        &quot;USER_DB&quot;: { &quot;name&quot;: &quot;user-db-xxxxxx&quot;, &quot;id&quot;: &quot;d1-uuid-value&quot; }
    },
    &quot;kv&quot;: {
        &quot;IMAGES_KV&quot;: &quot;kv-namespace-id&quot;
    },
    &quot;blogProjectName&quot;: &quot;svelteblog&quot;,
    &quot;adminProjectName&quot;: &quot;svelteadmin&quot;
}
</code></pre>
<p>이 백업 파일이 존재하면 향후 코드 수정이나 기기 변경 시 <code>npm run restore</code> 명령어를 사용해 복구 모드를 진행할 수 있습니다.</p>
<ul>
<li>복원 모드에서는 휴먼 에러 방지를 위해 프로젝트 배포명과 리소스 ID 바인딩이 <strong>완전 자동 고정</strong>되어 즉각 연동됩니다.</li>
<li>또한 기존 데이터를 절대 건드리지 않도록 <strong>초기 시드 주입(Seeding) 단계를 자동으로 안전하게 생략</strong>하므로 기존 운영 중인 블로그 데이터 유실 없이 인프라 관계만 재생성해냅니다.</li>
</ul>
<hr>
<h2 id="5-자주-겪는-문제-및-문제-해결-troubleshooting">❓ 5. 자주 겪는 문제 및 문제 해결 (Troubleshooting)</h2>
<h3 id="1-wrangler-whoami-세션-및-권한-오류">🚨 1. Wrangler Whoami 세션 및 권한 오류</h3>
<ul>
<li><strong>현상</strong>: 스크립트 실행 중 <code>Authentication Error</code> 혹은 D1/KV 리소스 생성 명령어 시점에 세션 없음 에러가 표시되는 경우.</li>
<li><strong>해결</strong>: <code>npm run setup:select</code>를 구동하고 <strong>Q4. Wrangler 로그인</strong> 단계에서 <code>Y</code>를 입력해 브라우저를 통해 재인증을 마쳐야 합니다. 스크립트는 계정 보안을 위해 오직 최소 권한(<code>account:read</code>, <code>user:read</code>, <code>workers:write</code>, <code>pages:write</code>, <code>d1:write</code>, <code>workers_kv:write</code>)만을 요구하므로 안심하고 승인하시면 됩니다.</li>
</ul>
<h3 id="2-windows-환경의-스크립트-실행-권한-차단">🚨 2. Windows 환경의 스크립트 실행 권한 차단</h3>
<ul>
<li><strong>현상</strong>: 윈도우 파워쉘(PowerShell) 등에서 스크립트 무단 실행 정책 오류가 표시되는 경우.</li>
<li><strong>해결</strong>: 셋업 스크립트는 윈도우 OS 감지 시 자동으로 <code>npx</code> 대신 <code>npx.cmd</code>를 호출하고 쉘 제어 플래그를 변환하도록 자체 예외 설계되어 있습니다. 가급적이면 파워쉘보다 <strong>Git Bash</strong> 혹은 <strong>CMD</strong> 터미널을 사용하여 실행하는 것을 권장합니다.</li>
</ul>
]]></content:encoded>
            <category>상세 메뉴얼</category>
            <enclosure url="https://sveltekitblog.com/images/posts/setup-guide/desktop/img-detail-manual-setup-guide-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[블로그 및 관리자 기능 통합 소개]]></title>
            <link>https://testblog-6br.pages.dev/general-guide/general-integration-guide</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/general-guide/general-integration-guide</guid>
            <pubDate>Tue, 16 Jun 2026 15:10:13 GMT</pubDate>
            <description><![CDATA[블로그 앱의 레이아웃과 소통 기능, 그리고 관리자 모드의 다국어 글쓰기 및 실시간 디자인 설정 등 시스템이 제공하는 주요 기능 문서들을 통합 소개합니다.]]></description>
            <content:encoded><![CDATA[<p><img src="https://sveltekitblog.com/images/posts/general-integration-guide/desktop/img-general-guide-general-integration-guide-ko-001.webp" alt="img-general-guide-general-integration-guide-ko-001"></p>
<h1 id="블로그-및-관리자-기능-통합-소개">📖 블로그 및 관리자 기능 통합 소개</h1>
<p>본 문서는 블로그 서비스 및 관리자 기능(어드민)의 핵심 구성을 한눈에 파악할 수 있도록 개별 소개 문서들을 연계하여 안내합니다.</p>
<p><em>참고: 각 기능의 세부적인 연동 방법 및 상세 매뉴얼은 추후 별도 포스팅을 통해 순차적으로 안내될 예정입니다.</em></p>
<hr>
<h2 id="사용자-기능-소개-목록">📂 사용자 기능 소개 목록</h2>
<ul>
<li><strong>관리자(어드민) 기능 소개</strong><ul>
<li><strong><a href="../admin-guide/admin-install-and-deploy">CMD 원클릭 설치 및 클라우드플레어 배포 가이드</a></strong>: 터미널 명령어 한 줄로 클라우드플레어 에지 인프라를 구축하고 블로그를 배포하는 방법을 안내합니다.</li>
<li><strong><a href="../admin-guide/admin-getting-started">어드민 최초 진입 및 환경설정 시작하기</a></strong>: 마스터 비밀번호 로그인, 접속 허용 IP(화이트리스트) 관리 및 다국어 UI 사전 환경설정 개요를 다룹니다.</li>
<li><strong><a href="../admin-guide/admin-core-features">어드민 핵심 기능 및 듀얼 에디터 소개</a></strong>: HTML 비주얼 에디터와 마크다운 에디터를 전환하며 글을 쓰고, 기기별 레이아웃 위젯을 배치하는 방식을 소개합니다.</li>
<li><strong><a href="../admin-guide/admin-design-editor">실시간 디자인 에디터 및 배경 연출 소개</a></strong>: 테마 색상, 단색/그라디언트/배경 이미지/캔버스 스크립트 등 4가지 배경 설정을 재빌드 없이 실시간 반영하는 기능을 소개합니다.</li>
<li><strong><a href="../admin-guide/admin-faq">관리자 자주 묻는 질문 (FAQ) 및 트러블슈팅</a></strong>: 버전 업그레이드, 이미지 저장소 키 점검, 배포 후 오작동 해결 및 IP 보안 차단 해제법 등을 확인합니다.</li>
</ul>
</li>
<li><strong>블로그 기능 소개</strong><ul>
<li><strong><a href="../blog-guide/blog-getting-started">블로그 첫 화면 구성 및 로그인</a></strong>: 블로그 화면 레이아웃 구성과 이메일/소셜 가입 방식을 소개합니다.</li>
<li><strong><a href="../blog-guide/blog-core-features">블로그 핵심 기능 및 다국어 서비스 소개</a></strong>: 다국어 본문 실시간 전환 및 미번역 시 대체 처리 로직, 계층형 댓글과 비밀 방명록을 통한 소통 구조를 다룹니다.</li>
<li><strong><a href="../blog-guide/blog-faq">독자 정책 및 자주 묻는 질문 (FAQ)</a></strong>: 회원 탈퇴 시 작성 데이터 처리 원칙, 재가입 규정 및 차단(밴)/강퇴 조치에 관해 확인합니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="블로그-관리-기본-프로세스">🚀 블로그 관리 기본 프로세스</h2>
<p>블로그 관리자 권한을 설정하고 글 작성을 시작하는 기본 흐름의 개요입니다.</p>
<h3 id="1-ip-허용-목록-적용-및-로그인">1. IP 허용 목록 적용 및 로그인</h3>
<ul>
<li>처음 관리자 화면에 접근 시 보안을 위해 본인의 인터넷 공인 IP가 허용 목록(<code>ALLOWED_IP</code>)에 등록되어 있어야 접근이 허가됩니다. (미등록 시 403 Forbidden 페이지가 표시됩니다.)</li>
<li>로그인 화면에서 설정해 둔 마스터 비밀번호를 입력해 접속합니다.</li>
</ul>
<h3 id="2-다국어-글-일괄-저장">2. 다국어 글 일괄 저장</h3>
<ul>
<li>글쓰기 메뉴에서 제공되는 다국어 탭을 전환하며 본문을 각각 기재한 후, 하단 저장 버튼을 클릭해 데이터베이스에 다국어 글 묶음을 일괄 저장합니다.</li>
</ul>
<h3 id="3-실시간-테마-적용">3. 실시간 테마 적용</h3>
<ul>
<li>디자인 에디터에서 테마 색상, 폰트 종류, 배경 효과(Canvas, 이미지 등)를 저장하면 재빌드 과정 없이 방문자 브라우저 화면에 실시간으로 즉시 적용됩니다.</li>
</ul>
<hr>
<h2 id="마치며">💬 마치며</h2>
<p>본 문서에서 소개한 내용은 블로그와 어드민이 제공하는 기능들의 <strong>간단한 개요</strong>입니다. 실제로는 여기서 미처 다루지 못한 다양한 세부 기능과 편의 옵션들이 더 많이 준비되어 있으며, 직접 사용해 보시면서 하나씩 발견해 나가는 것도 색다른 재미가 될 것입니다.</p>
<p>앞으로 각 기능별 상세 사용법과 활용 팁을 담은 심화 가이드도 순차적으로 포스팅할 예정이니, 필요할 때 다시 방문해 주시면 감사하겠습니다. 이용 중 궁금한 점이나 문의 사항이 있으시다면, 방명록이나 댓글을 통해 언제든 편하게 남겨 주세요.</p>
]]></content:encoded>
            <category>통합 가이드</category>
            <enclosure url="https://sveltekitblog.com/images/posts/general-integration-guide/desktop/img-general-guide-general-integration-guide-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[독자 정책 및 자주 묻는 질문 (FAQ)]]></title>
            <link>https://testblog-6br.pages.dev/blog-guide/blog-faq</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/blog-guide/blog-faq</guid>
            <pubDate>Tue, 16 Jun 2026 15:09:13 GMT</pubDate>
            <description><![CDATA[회원 탈퇴 시의 작성 데이터(댓글, 방명록) 처리 정책 및 이용 제한 정책의 기본 원칙을 소개합니다.]]></description>
            <content:encoded><![CDATA[<p><img src="https://sveltekitblog.com/images/posts/blog-faq/desktop/img-blog-guide-blog-faq-ko-001.webp" alt="img-blog-guide-blog-faq-ko-001"></p>
<h1 id="독자-정책-및-자주-묻는-질문-faq">❓ 독자 정책 및 자주 묻는 질문 (FAQ)</h1>
<p>본 문서는 블로그 서비스 이용 시 발생할 수 있는 회원 탈퇴에 따른 개인정보 및 작성 데이터 처리 방식, 차단(밴) 및 강제 탈퇴(강퇴) 정책 등의 기본 원칙을 소개합니다.</p>
<hr>
<h2 id="q1-회원-탈퇴-시-기존에-작성했던-댓글과-방명록은-어떻게-되나요">🚪 Q1. 회원 탈퇴 시 기존에 작성했던 댓글과 방명록은 어떻게 되나요?</h2>
<p>블로그 시스템은 독자분의 개인정보 보호와 사이트 내 대화 흐름의 무결성을 동시에 유지하기 위해 **&#39;작성자 익명화 처리 정책&#39;**을 적용하고 있습니다.</p>
<h3 id="탈퇴-시-개인정보-처리-원칙">💡 탈퇴 시 개인정보 처리 원칙</h3>
<ol>
<li><strong>활성 로그인 계정 및 인증 정보 파기</strong>:<ul>
<li>회원 탈퇴 진행 시 실제 로그인을 위한 활성 세션과 소셜 로그인 연동 정보는 인증 시스템에서 즉시 안전하게 파기됩니다. 다만, 부정 가입 및 부정 이용 방지, 보안 감사(Audit) 목적으로 탈퇴 당시의 이메일 및 소셜 ID 연동 정보가 격리된 내부 탈퇴 로그 테이블(<code>deleted_users</code>)에 백업 보관됩니다. 이 로그 정보는 시스템 내부 관리용이며, 외부 화면상에서는 작성자 추적이 불가능하도록 완전히 식별 차단 및 익명화(알 수 없음) 처리됩니다.</li>
</ul>
</li>
<li><strong>작성 데이터 보존</strong>:<ul>
<li>작성하셨던 본문 댓글과 방명록 글은 기존 토론 맥락과 대화 흐름을 보존하기 위해 텍스트 내용은 화면에 유지됩니다.</li>
</ul>
</li>
<li><strong>닉네임 익명화 처리</strong>:<ul>
<li>작성자 닉네임과 계정 간의 연결 정보가 해제되며, 작성자 표시명은 **&quot;알 수 없음&quot;**으로 자동 일괄 전환됩니다. 이에 따라 이전 작성자가 실제로 누구였인지 시스템적으로 역추적하는 것이 방지됩니다.</li>
</ul>
</li>
</ol>
<ul>
<li><em>참고: 관리자가 회원 정보를 직접 완전히 삭제하는 경우에는 작성하셨던 댓글과 글이 함께 지워질 수 있습니다.</em></li>
</ul>
<hr>
<h2 id="q2-탈퇴-후-즉시-재가입이-가능한가요">🚪 Q2. 탈퇴 후 즉시 재가입이 가능한가요?</h2>
<ul>
<li><strong>자진 탈퇴 시</strong>:<ul>
<li>본인의 의사로 탈퇴를 완료한 경우, 별도의 유예 기간 없이 동일한 이메일 또는 소셜 계정을 사용하여 즉시 재가입 및 신규 이용이 가능합니다.</li>
</ul>
</li>
<li><strong>활동 차단(밴 - Ban) 상태인 경우</strong>:<ul>
<li>운영 정책 위반으로 계정이 차단(Ban)된 동안에는 이미 해당 이메일로 가입된 정보가 데이터베이스에 존재하므로 동일한 이메일로 새로 가입(재가입)하는 것이 원천적으로 불가능합니다.</li>
</ul>
</li>
<li><strong>강제 탈퇴(강퇴 - Kick) 처리가 된 경우</strong>:<ul>
<li>관리자에 의해 계정이 강제 삭제(Hard Delete)된 경우, 인증 테이블에서 기존 정보가 완전히 삭제되므로 동일한 이메일로 즉시 재가입이 가능해집니다.</li>
<li>시스템 내부 탈퇴 로그(<code>deleted_users</code>)에 강퇴 당시의 이메일 및 소셜 ID 정보가 보존되기는 하나, 현재 회원가입 시점에 이 로그를 대조하여 자동으로 가입을 제한하는 시스템적 차단 로직은 구현되어 있지 않습니다.</li>
<li>따라서, 동일 인물의 악의적인 활동 재발을 물리적으로 원천 차단하기 위해서는 계정을 강제 삭제(강퇴)하는 대신 <strong>&#39;영구 차단(밴 - Ban)&#39;</strong> 상태로 계정을 유지해야 합니다. (밴 상태에서는 이메일이 데이터베이스에 남아있어 중복 가입 제약조건에 의해 동일 이메일 재가입이 차단됩니다.)</li>
</ul>
</li>
</ul>
<hr>
<h2 id="q3-활동이-차단밴당하거나-계정이-강제-탈퇴강퇴되었습니다">🚫 Q3. 활동이 차단(밴)당하거나 계정이 강제 탈퇴(강퇴)되었습니다.</h2>
<p>스팸 성격의 광고 유포, 욕설 및 비방 등 운영 정책을 위반하여 관리자로부터 활동 제한 또는 계정 삭제 처리가 적용된 경우의 확인 사항입니다.</p>
<h3 id="제재-수위별-특징-및-확인-사항">💡 제재 수위별 특징 및 확인 사항</h3>
<ol>
<li><strong>활동 차단 (밴 - Ban)</strong>:<ul>
<li><strong>상태</strong>: 기존 로그인 상태 및 회원 정보는 유지되나, 댓글 작성, 대댓글 등록, 방명록 등록 등 사이트 내 모든 작성 권한이 임시 혹은 무기한 정지됩니다.</li>
<li><strong>원인 및 사유 확인</strong>: 차단이 적용되면 댓글 작성 폼 영역에 **&quot;차단된 유저는 글을 작성할 수 없습니다.&quot;**라는 안내 배너가 나타나며, 차단 세부 사유와 정지 만료일(정지 해제 시간)이 명시적으로 노출됩니다.</li>
</ul>
</li>
<li><strong>강제 탈퇴 (강퇴 - Kick/Delete)</strong>:<ul>
<li><strong>상태</strong>: 로그인 계정 자체가 데이터베이스에서 완전히 삭제(Hard Delete)된 상태입니다. </li>
<li><strong>동작</strong>: 강퇴 처리가 완료되는 즉시 활성 로그인 세션이 강제로 종료되며, 이후 로그인을 시도할 경우 가입 이력이 없는 신규 회원(존재하지 않는 계정)으로 안내됩니다. 관리자가 삭제 유형을 어떻게 설정하느냐에 따라 강퇴 시 기존에 작성했던 댓글들이 모두 한꺼번에 강제 폭파되거나, 작성자 이름만 &#39;알 수 없음&#39;으로 익명 처리될 수 있습니다.</li>
</ul>
</li>
</ol>
]]></content:encoded>
            <category>사용자 가이드</category>
            <enclosure url="https://sveltekitblog.com/images/posts/blog-faq/desktop/img-blog-guide-blog-faq-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[블로그 핵심 기능 및 다국어 서비스 소개]]></title>
            <link>https://testblog-6br.pages.dev/blog-guide/blog-core-features</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/blog-guide/blog-core-features</guid>
            <pubDate>Tue, 16 Jun 2026 15:08:16 GMT</pubDate>
            <description><![CDATA[다국어 본문 실시간 전환 및 미번역 시 대체 처리 로직, 계층형 댓글 구조와 비밀 방명록 등 블로그 서비스의 주요 사용자 기능을 소개합니다.]]></description>
            <content:encoded><![CDATA[<h1 id="블로그-핵심-기능-및-다국어-서비스-소개">🌐 블로그 핵심 기능 및 다국어 서비스 소개</h1>
<p>본 문서는 일반 방문자가 블로그의 다국어 번역 글을 조회하고, 댓글 및 방명록을 통해 다른 독자들과 이야기를 나누는 핵심 기능의 명세를 소개합니다.</p>
<hr>
<h2 id="1-다국어-본문-실시간-전환-i18n">🌐 1. 다국어 본문 실시간 전환 (i18n)</h2>
<p>이 블로그는 메뉴나 버튼 등의 기본 UI뿐 아니라, <strong>작성된 포스트 본문 내용 자체</strong>도 여러 언어 버전으로 전환하여 읽을 수 있는 실시간 다국어 번역 뷰어를 제공합니다.</p>
<h3 id="동작-방식-및-특징">⚙️ 동작 방식 및 특징</h3>
<ul>
<li><strong>언어별 URL 라우팅</strong>: 상단 지구본 아이콘 또는 글 제목 옆의 언어 선택 버튼을 클릭하면, URL 주소에 언어 접두사(예: <code>/en</code>, <code>/ja</code>)가 붙으며 해당 언어의 전용 페이지로 즉시 라우팅됩니다. 기본 제공되는 언어(한국어, 영어, 일본어) 외에도, 관리자가 다국어 번역 사전(딕셔너리)을 추가 정의하면 새로운 외국어 버전을 제한 없이 추가로 발행할 수 있습니다.</li>
</ul>
<img src="https://sveltekitblog.com/images/posts/blog-core-features/desktop/img-blog-guide-blog-core-features-ko-001.webp" alt="img-blog-guide-blog-core-features-ko-001" data-align="center" />
<img src="https://sveltekitblog.com/images/posts/blog-core-features/desktop/img-blog-guide-blog-core-features-ko-002.webp" alt="img-blog-guide-blog-core-features-ko-002" data-align="center" />
<figure data-align="center"><img src="https://sveltekitblog.com/images/posts/blog-core-features/desktop/img-blog-guide-blog-core-features-ko-003.webp" alt="img-blog-guide-blog-core-features-ko-003" data-align="center" data-caption="다국어로 포스팅된 글은 해당 언어 버튼을 누르는 즉시 번역본 페이지로 이동합니다." /><figcaption>다국어로 포스팅된 글은 해당 언어 버튼을 누르는 즉시 번역본 페이지로 이동합니다.</figcaption></figure><ul>
<li><strong>수동 작성 데이터 로드 (실시간 자동 번역이 아님)</strong>: 시스템이 텍스트를 기계적으로 실시간 자동 번역해 주는 것이 아닙니다. 작성자가 각 언어 탭에 번역된 포스트 본문 데이터를 직접 작성하여 개별 저장해 두면(외부 AI나 번역기로 가공하여 데이터베이스에 각각 등록), 방문자가 선택한 언어에 대응하는 포스트 데이터를 데이터베이스에서 정확하게 호출하여 화면에 렌더링하는 방식입니다.</li>
<li><strong>본문 및 메타데이터 동시 로드</strong>: 단순 텍스트 교체에 그치지 않고, 데이터베이스에 각 언어별로 등록된 제목, 요약, 태그, 본문 HTML이 완전히 선택된 언어의 개별 데이터로 교체되어 출력됩니다.</li>
<li><strong>미번역 글 대체(Fallback) 처리</strong>: 작성자가 특정 언어의 번역 글을 등록하지 않은 경우에는 방문자가 글을 읽지 못하는 상황을 방지하기 위해, 번역이 준비되지 않았다는 안내와 함께 기본 작성 언어(예: 한국어 본문)로 본문 내용을 대체하여 매끄럽게 보여줍니다.</li>
</ul>
<hr>
<h2 id="2-댓글-및-답글대댓글-기능">💬 2. 댓글 및 답글(대댓글) 기능</h2>
<p>각 게시글 하단에는 비로그인 방문자 또는 회원이 의견을 공유할 수 있는 댓글 영역이 배치되어 있습니다.</p>
<ul>
<li><strong>의견 공유</strong>: 로그인한 사용자는 댓글 입력란에 본문을 작성하고 등록하여 즉시 생각을 공유할 수 있습니다.</li>
<li><strong>계층형 답글(대댓글)</strong>: 특정 댓글 하단에 답글을 달 수 있어 토론 흐름을 트리 구조로 한눈에 알아보기 쉽게 정렬합니다.</li>
<li><strong>보안 및 무결성 유지</strong>: 본인이 작성한 댓글은 삭제가 가능합니다. 단, 이미 하위에 대댓글이 달린 댓글을 삭제하는 경우 전체 토론 맥락이 깨지는 것을 방지하기 위해 &quot;삭제된 댓글입니다.&quot;라는 대체 문구로 마스킹 처리되어 계층 구조를 보존합니다.</li>
</ul>
<hr>
<h2 id="3-방명록-및-비밀글-작성">📖 3. 방명록 및 비밀글 작성</h2>
<p>블로그 방명록 공간(<code>Guestbook</code>)을 통해 사이트 개설자와 소통할 수 있는 기능을 제공합니다.</p>
<h3 id="비밀-방명록-기능">⚙️ 비밀 방명록 기능</h3>
<ul>
<li><strong>비밀글 쓰기</strong>: 글 작성 시 <strong>[🔒 비밀글로 작성]</strong> 체크박스를 활성화하면 보안 글 작성이 가능합니다.</li>
<li><strong>노출 권한 차단</strong>: 비밀글로 저장된 방명록은 제3자(다른 방문자 또는 비로그인 타인)의 목록에는 노출되지 않고 완전히 배제됩니다.</li>
<li><strong>안전한 대화</strong>: 오직 해당 방명록을 작성한 본인(로그인 상태)과 사이트 관리자에게만 본문 내용이 보이며, 안전하게 소통을 나눌 수 있습니다.</li>
</ul>
]]></content:encoded>
            <category>사용자 가이드</category>
            <enclosure url="https://sveltekitblog.com/images/posts/blog-core-features/desktop/img-blog-guide-blog-core-features-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[블로그 첫 화면 구성 및 로그인 시작하기]]></title>
            <link>https://testblog-6br.pages.dev/blog-guide/blog-getting-started</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/blog-guide/blog-getting-started</guid>
            <pubDate>Tue, 16 Jun 2026 15:04:15 GMT</pubDate>
            <description><![CDATA[블로그 첫 화면의 기본 레이아웃 구성과 회원가입 및 로그인 방식의 개요를 소개합니다.]]></description>
            <content:encoded><![CDATA[<h1 id="블로그-첫-화면-구성-및-로그인-시작하기">🌐 블로그 첫 화면 구성 및 로그인 시작하기</h1>
<p>본 문서는 블로그(<code>apps/blog</code>) 첫 화면의 레이아웃 구성과 기본적인 회원가입 및 로그인 방식을 간략히 소개합니다.</p>
<hr>
<h2 id="1-블로그-화면-구성-및-레이아웃">🎨 1. 블로그 화면 구성 및 레이아웃</h2>
<p>블로그 화면은 방문자가 정보를 신속하게 찾을 수 있도록 직관적인 레이아웃(헤더, 본문, 사이드바, 푸터)으로 설계되어 있습니다.</p>
<ol>
<li><strong>상단 네비게이션 헤더 (Header)</strong>:<ul>
<li><strong>사이트 로고</strong>: 클릭 시 첫 홈화면으로 복귀합니다.</li>
<li><strong>단축 메뉴 바</strong>: 카테고리 바로가기나 외부 채널 연결 등 주요 링크들이 나열됩니다.</li>
<li><strong>다국어 스위칭</strong>: 지구본 아이콘 클릭 시 사이트 전체의 메뉴 언어 및 게시글 번역본이 실시간으로 전환되어 표시됩니다.</li>
</ul>
</li>
<li><strong>사이드바 영역 (Sidebar)</strong>:<ul>
<li>PC 화면에서는 카테고리 목록, 블로그 운영자 프로필 카드, 인기 태그 등이 노출되어 독자의 탐색을 돕습니다.</li>
<li>모바일 기기 접속 시에는 본문 가독성을 위해 자동으로 감춰집니다.</li>
</ul>
</li>
<li><strong>메인 콘텐츠 영역 (Main Content)</strong>:<ul>
<li>최신 발행 게시글 목록과 카테고리 필터가 카드 디자인 형식으로 배치되어 있습니다.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="2-회원가입-및-로그인">🔑 2. 회원가입 및 로그인</h2>
<p>댓글 작성 및 방명록 참여를 위한 계정 생성과 로그인 기능을 지원합니다.</p>
<h3 id="로그인-및-가입-지원-방식">⚙️ 로그인 및 가입 지원 방식</h3>
<ul>
<li><strong>기본 이메일 로그인/가입 (Default)</strong>:<ul>
<li>설치 직후 기본 활성화되는 로그인 방식입니다. 이메일 주소, 디스플레이 네임(닉네임), 그리고 비밀번호를 등록하여 신규로 가입할 수 있으며, 가입 즉시 자동 로그인이 적용됩니다.</li>
</ul>
</li>
</ul>
<figure data-align="center"><img src="https://sveltekitblog.com/images/posts/blog-getting-started/desktop/img-blog-guide-blog-getting-started-ko-001.webp" alt="img-blog-guide-blog-getting-started-ko-001" data-align="center" data-caption="기본 이메일 로그인 화면" /><figcaption>기본 이메일 로그인 화면</figcaption></figure><ul>
<li><strong>소셜 로그인 (Better-Auth)</strong>:<ul>
<li>내부적으로 Better-Auth 엔진을 탑재하고 있어 구글(Google), 깃허브(GitHub), 카카오, 네이버 등을 포함한 <strong>총 21종의 소셜 프로바이더 로그인</strong>을 지원합니다.</li>
<li>다만, 소셜 로그인은 사이트 구축 후 관리자가 각 소셜 플랫폼에서 획득한 클라이언트 ID 및 비밀키를 설정 환경변수에 수동으로 주입해야만 로그인 화면에 노출되고 작동합니다. (소셜 프로바이더별 구체적인 연동 가이드는 추후 별도 문서로 안내됩니다.)</li>
</ul>
</li>
</ul>
<figure data-align="center"><img src="https://sveltekitblog.com/images/posts/blog-getting-started/desktop/img-blog-guide-blog-getting-started-ko-002.webp" alt="img-blog-guide-blog-getting-started-ko-002" data-align="center" data-caption="소셜 + 이메일 로그인 화면" /><figcaption>소셜 + 이메일 로그인 화면</figcaption></figure><ul>
<li><strong>로그인 상태 표시</strong>:<ul>
<li>로그인이 완료되면 오른쪽 위 버튼이 사용자의 프로필 이미지 아이콘과 마이페이지 바로가기로 변경됩니다.</li>
<li><em>참고: 사용자 프로필 사진을 실제 연동하여 표시하는 기능은 현재 완벽히 작동하지 않을 수 있으며, 기본 이미지 아이콘 등으로 대체 표시될 수 있습니다.</em></li>
</ul>
</li>
</ul>
]]></content:encoded>
            <category>사용자 가이드</category>
            <enclosure url="https://sveltekitblog.com/images/posts/blog-getting-started/desktop/img-blog-guide-blog-getting-started-ko-001.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[어드민 자주 묻는 질문 (FAQ) 및 트러블슈팅]]></title>
            <link>https://testblog-6br.pages.dev/admin-guide/admin-faq</link>
            <guid isPermaLink="true">https://testblog-6br.pages.dev/admin-guide/admin-faq</guid>
            <pubDate>Tue, 16 Jun 2026 15:01:11 GMT</pubDate>
            <description><![CDATA[설치 및 배포 중 발생하는 오류 해결, 버전 업그레이드, 이미지 저장소 필수 키 리스트 및 IP 보안 차단 해제법을 확인합니다.]]></description>
            <content:encoded><![CDATA[<img src="https://sveltekitblog.com/images/posts/admin-faq/desktop/img-admin-guide-admin-faq-ko-001.webp" alt="img-admin-guide-admin-faq-ko-001" data-align="center"><h1 id="어드민-자주-묻는-질문-faq-및-트러블슈팅">❓ 어드민 자주 묻는 질문 (FAQ) 및 트러블슈팅</h1>
<p>본 문서는 블로그 설치, 배포, 운영 단계에서 발생할 수 있는 주요 트러블슈팅 해결 방안과 새 버전 업그레이드 가이드를 제공합니다.</p>
<hr>
<h2 id="q1-최신-버전으로-안전하게-업그레이드upgrade하거나-데이터를-복원하는-방법을-알려주세요">🗄️ Q1. 최신 버전으로 안전하게 업그레이드(Upgrade)하거나 데이터를 복원하는 방법을 알려주세요.</h2>
<p>블로그 데이터베이스의 정합성을 지키고 예기치 못한 빌드 크래시를 방지하기 위해 아래의 안전한 업그레이드 절차를 준수하는 것을 강력히 권장합니다.</p>
<h3 id="1-안전한-새-버전-업그레이드-검증-절차-권장">1. 안전한 새 버전 업그레이드 검증 절차 (권장)</h3>
<p>기존 DB와의 스키마 불일치나 템플릿 변경에 따른 충돌을 예방하기 위해, 즉시 본 서버에 배포하는 대신 테스트 환경에서 선검증을 거치십시오. (Git 미사용자 포함)</p>
<ol>
<li><strong>설정 백업 보존</strong>:<ul>
<li>기존 개발 폴더 안의 <code>wrangler.backup.json</code> 및 <code>.dev.vars</code> 파일을 안전한 임시 폴더에 복사해 둡니다.</li>
<li><em>※ <code>.dev.vars</code> 파일은 중요 인증 정보(비밀번호, OAuth 등)를 담고 있으나 보안상 Git 관리 대상에서 제외(<code>.gitignore</code>)되어 있으므로, 코드를 새로 가져오거나 초기화할 때 유실되기 쉽습니다. 반드시 백업을 받아두어야 합니다.</em></li>
</ul>
</li>
<li><strong>기존 운영 데이터 백업</strong>:<ul>
<li>현재 가동 중인 블로그 어드민의 <code>콘텐츠 백업</code> 메뉴에서 전체 포스트 및 설정 백업 파일을 다운로드해 둡니다.</li>
</ul>
</li>
<li><strong>격리된 폴더에 새 버전 클론/다운로드</strong>:<ul>
<li>기존 폴더를 덮어쓰지 말고, <strong>완전히 새로운 디렉토리</strong>에 최신 릴리스 소스코드를 내려받거나 클론합니다.</li>
</ul>
</li>
<li><strong>임시 테스트 서버 배포</strong>:<ul>
<li>새 폴더로 이동해 의존성을 설치(<code>npm install</code>)하고 임시 테스트용 데이터베이스를 생성하여 테스트 배포를 수행합니다.</li>
</ul>
</li>
<li><strong>백업 복원 테스트</strong>:<ul>
<li>새로 임시 배포된 테스트 어드민에 접속하여, 2단계에서 다운로드했던 백업 파일을 로드해 데이터가 유실 없이 잘 나타나고 에러가 없는지 꼼꼼하게 검증합니다.</li>
</ul>
</li>
<li><strong>본 서버 정식 업데이트 적용</strong>:<ul>
<li>테스트 서버에서 모든 기능이 정상 작동함을 확인한 뒤에만, 기존 가동 폴더로 돌아와 코드를 갱신(<code>git pull</code> 또는 소스 덮어쓰기)하고 개별 앱 배포 명령어(<code>npm run deploy:blog</code>, <code>npm run deploy:admin</code>)를 실행하여 정식 업그레이드를 마칩니다.</li>
</ul>
</li>
</ol>
<h3 id="2-데이터-유실-시-최후의-복구-수단">2. 데이터 유실 시 최후의 복구 수단</h3>
<ul>
<li><strong><code>npm run restore</code> 명령어 사용</strong>:<ul>
<li>만약 인프라 설정이나 데이터가 심각하게 훼손되어 원천 복구가 필요한 경우에만, 백업해 두었던 <code>wrangler.backup.json</code> 파일을 기반으로 전체 인프라를 복구 재구성하는 <strong>최후의 데이터 복구 수단</strong>으로 실행하십시오.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="q2-r2-supabase-imagekit-이미지-저장소-변경-시-입력해야-할-필수-키는-무엇인가요">📂 Q2. R2, Supabase, ImageKit 이미지 저장소 변경 시 입력해야 할 필수 키는 무엇인가요?</h2>
<p>각 외부 이미지 저장소 사용 시 누락 없이 입력해야 하는 필수 환경변수 및 세팅 체크리스트입니다.</p>
<table>
<thead>
<tr>
<th align="left">저장소 타입</th>
<th align="left">필수 입력 항목 / 설정</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>Cloudflare R2</strong></td>
<td align="left"><code>IMAGES</code> R2 버킷 바인딩</td>
<td align="left"><code>wrangler.json</code> 내 R2 버킷 정보와 매핑이 필요합니다.</td>
</tr>
<tr>
<td align="left"><strong>Supabase Storage</strong></td>
<td align="left"><code>supabase_storage_url</code><br><code>supabase_storage_key</code><br><code>supabase_storage_bucket</code></td>
<td align="left">API URL 및 Service Role Key가 정확해야 하며, Supabase Storage 내 버킷 권한 정책이 **Public(공개)**으로 설정되어 있어야 이미지가 깨지지 않습니다.</td>
</tr>
<tr>
<td align="left"><strong>ImageKit.io</strong></td>
<td align="left"><code>imagekit_url_endpoint</code><br><code>imagekit_public_key</code><br><code>imagekit_private_key</code></td>
<td align="left">엔드포인트 URL 형식을 점검하고 도메인 보안(CORS) 허용을 설정해야 합니다.</td>
</tr>
</tbody></table>
<hr>
<h2 id="q3-대시보드-통계-그래프가-데모-데이터로만-표시됩니다">📊 Q3. 대시보드 통계 그래프가 데모 데이터로만 표시됩니다.</h2>
<ul>
<li><strong>원인</strong>: 구글 애널리틱스 4(GA4) API 환경변수가 입력되지 않았거나 비정상적인 경우, 오류 방지를 위해 임시 데모 데이터가 대체 표시됩니다.</li>
<li><strong>설정 방법</strong>: Cloudflare Pages 설정에 아래 환경변수를 입력하고 다시 배포합니다.<ul>
<li><code>GA4_PROPERTY_ID</code>: 구글 애널리틱스 속성 ID</li>
<li><code>GA4_CLIENT_EMAIL</code>: 구글 클라우드 서비스 계정 이메일</li>
<li><code>GA4_PRIVATE_KEY</code>: 구글 서비스 계정 비공개 키</li>
</ul>
</li>
<li><strong>주의 사항</strong>: <code>.dev.vars</code> 파일에 <code>GA4_PRIVATE_KEY</code>를 작성할 때 개행 코드(<code>\n</code>) 형식이 깨지지 않도록 반드시 전체 키 문자열을 큰따옴표(<code>&quot;</code>)로 묶어 등록해야 합니다.</li>
</ul>
<hr>
<h2 id="q4-최초-setup-배포-후-어드민-접속이-안-되거나-일부-데이터-동기화가-누락된-것-같습니다">⚡ Q4. 최초 setup 배포 후 어드민 접속이 안 되거나 일부 데이터 동기화가 누락된 것 같습니다.</h2>
<ul>
<li><strong>원인</strong>: 원클릭 셋업 단계에서 환경변수 설정 유도가 정상적으로 완료되었더라도, 예상치 못한 원격 통신 장애나 기타 시스템 에러로 인해 암호화 키 또는 비밀 환경변수(Secrets)들의 동기화가 일부 누락되거나 오염된 상태로 배포가 진행될 수 있습니다.</li>
<li><strong>해결법</strong>: 각 앱의 <code>.dev.vars</code> 설정값 상태를 다시 한번 육안으로 점검한 후, 각 폴더로 이동해 수동으로 개별 배포 명령어인 <code>npm run deploy:blog</code>와 <code>npm run deploy:admin</code>을 각각 <strong>1회 실행</strong>해 주십시오. 로컬의 <code>.dev.vars</code> 파일에 들어있던 비밀 환경변수들이 정상적으로 덮어씌워지면서 오작동이 해결됩니다.</li>
</ul>
<hr>
<h2 id="q5-어드민-페이지-접근-시-quotforbidden-ip-not-allowedquot-또는-403-에러가-뜹니다">🔒 Q5. 어드민 페이지 접근 시 &quot;Forbidden (IP Not Allowed)&quot; 또는 403 에러가 뜹니다.</h2>
<p>이 블로그의 보안 사양상 배포 스크립트 실행 과정에서 배포 PC의 공인 IP 주소를 자동으로 감지하여 접속 허용 IP(<code>ALLOWED_IP</code>)로 Pages Secret에 자동 주입합니다.</p>
<h3 id="어드민-보안-권고-사항">⚠️ 어드민 보안 권고 사항</h3>
<ul>
<li>어드민 계정 탈취 및 비인가 접속을 예방하기 위해, 외부 공공장소(카페, 도서관 등)나 신뢰성이 보장되지 않는 <strong>공용 PC</strong>에서의 어드민 접속 및 관리 작업은 <strong>보안상 강력히 제한할 것을 권장</strong>합니다.</li>
</ul>
<h3 id="상황별-대처-요령">💡 상황별 대처 요령</h3>
<ul>
<li><strong>상황 A. 집이나 사무실에서 인터넷 공유기가 재부팅되어 IP가 바뀐 경우</strong>:<ul>
<li>기존 작업을 진행하던 집/사무실의 메인 개발 PC 터미널에서 <code>npm run deploy:admin</code>을 1회 실행해주면 변경된 공인 IP를 자동으로 재감지하여 배포가 갱신되면서 즉시 접속이 재개됩니다.</li>
</ul>
</li>
<li><strong>상황 B. 피치 못하게 외부 장소로 개발 환경을 옮겨 장기 포스팅해야 하는 경우</strong>:<ul>
<li>기존 배포 정보가 담긴 백업 설정 파일(<code>wrangler.backup.json</code> 등)을 가지고 새로운 장소의 PC에서 원클릭 셋업 명령어로 새로 빌드/배포를 실행해 접속 권한을 획득하는 방법이 가장 간단합니다.</li>
<li><strong>※ 주의:</strong> 외부 작업을 마친 후 다시 원래 집이나 사무실로 귀환하여 블로그를 관리할 때는, <strong>반드시 기존 본래 개발 PC에서 <code>npm run deploy:admin</code>을 다시 실행하여 허용 IP 주소를 다시 원래 고정 IP로 복원 배포</strong>해주어야 정상 제어가 가능해집니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="q6-better-auth소셜-로그인-및-회원가입-관련-오류나-로그인-무한-루프가-발생합니다">🔐 Q6. Better Auth(소셜 로그인 및 회원가입) 관련 오류나 로그인 무한 루프가 발생합니다.</h2>
<ul>
<li><strong>원인</strong>: 인증 보안에 사용되는 환경변수 정보가 누락되었거나 비정상일 때 세션 검증이 실패하여 계속 로그인 루프에 빠지게 됩니다.</li>
<li><strong>해결법</strong>: <ol>
<li><code>apps/blog/.dev.vars</code> 파일을 열고 <code>BETTER_AUTH_SECRET</code> 키값에 <strong>최소 32자 이상의 안전한 무작위 비밀 문자열</strong>이 정상 입력되었는지 점검합니다.</li>
<li>비밀값을 올바르게 설정한 후 <code>npm run deploy:blog</code>를 실행해 주면 변경된 보안키가 원격 Pages Secret으로 동기화되어 인증 기능이 정상으로 돌아옵니다.</li>
</ol>
</li>
</ul>
]]></content:encoded>
            <category>관리자 가이드</category>
            <enclosure url="https://sveltekitblog.com/images/posts/admin-faq/desktop/img-admin-guide-admin-faq-ko-001.webp" length="0" type="image/webp"/>
        </item>
    </channel>
</rss>