Quartz 블로그 개설 방법

마지막 업데이트: 2026-01-26
예상 소요 시간: 2-3시간 (TrueNAS 설치 제외)

목차

  1. 개요
  2. 사전 준비
  3. 1단계: Git Server 구축
  4. 2단계: Quartz 설정
  5. 3단계: Cloudflare Pages 연동
  6. 4단계: 자동 배포 설정
  7. 테스트 및 배포
  8. 트러블슈팅

개요

이 가이드는 다음과 같은 방식으로 독립적인 블로그를 구축하는 방법을 설명합니다:

  • Git Server: Self-hosted Gitea (TrueNAS 또는 VPS)
  • Static Site Generator: Quartz 4
  • Content Source: 옵시디언 Vault (Git Submodule)
  • CI/CD: Gitea Actions
  • Hosting: Cloudflare Pages (무료)

장점

  • ✅ 완전한 데이터 소유권
  • ✅ 플랫폼 종속성 없음
  • ✅ 옵시디언에서 작성 → 자동 배포
  • ✅ 무료 호스팅 (Cloudflare Pages)
  • ✅ 빠른 속도 (CDN 활용)

사전 준비

필수 계정

  • Cloudflare 계정 (무료)
  • GitHub 계정 (Quartz fork용, 선택)

필수 소프트웨어

  • Git
  • Node.js (v18 이상)
  • 옵시디언 (또는 다른 마크다운 에디터)

필수 인프라 (택 1)

옵션 A: Self-hosted (권장)

  • TrueNAS Scale + Gitea App
  • Linux VPS + Docker

옵션 B: GitHub (대안)

  • GitHub 계정 (Private Repository 가능)

1단계: Git Server 구축

A. TrueNAS + Gitea 방식 (Self-hosted)

1.1 Gitea 설치

# TrueNAS Scale Web UI에서
Apps Available Applications 검색: "gitea"

설정 값:

Service Port: 3000 (HTTP)
SSH Port: 2222
Database: SQLite (간단) 또는 PostgreSQL (권장)

설치 후 http://YOUR_NAS_IP:3000에서 접속하여 초기 설정 완료

1.2 Gitea Act Runner 설치

# TrueNAS Apps에서
Apps Available Applications 검색: "gitea-act-runner"

연동 설정:

  1. Gitea 관리자 페이지 → Site Administration → Actions → Runners
  2. “Create new Runner” 클릭
  3. Registration Token 복사
  4. Act Runner 설치 시 Token 입력

검증:

# Gitea Actions 탭에서 Runner 상태 확인
Settings Actions Runners

1.3 첫 저장소 생성

  1. Gitea에서 새 저장소 생성: quartz-blog
  2. Private/Public 선택
  3. Initialize with README 체크 해제

B. GitHub 방식 (대안)

GitHub를 사용할 경우:

  1. GitHub에서 Private Repository 생성
  2. GitHub Actions는 기본 제공되므로 별도 설정 불필요
  3. 아래 단계에서 Gitea → GitHub로 치환하여 진행

2단계: Quartz 설정

2.1 Quartz 저장소 가져오기

Gitea 마이그레이션 기능 사용:

Gitea Web UI → 오른쪽 상단 "+" → "New Migration"

Clone Address: https://github.com/jackyzha0/quartz.git
Owner: [Your Username]
Repository Name: quartz-blog
Visibility: Private

또는 로컬에서 Clone:

# Quartz 공식 저장소 clone
git clone https://github.com/jackyzha0/quartz.git quartz-blog
cd quartz-blog
 
# Remote를 내 Gitea로 변경
git remote remove origin
git remote add origin http://YOUR_GITEA_IP:3000/username/quartz-blog.git
git push -u origin main

2.2 Content Submodule 추가

Content 저장소 준비:

# 옵시디언 Vault를 Git 저장소로 만들기
cd ~/Documents/ObsidianVault  # 실제 경로로 변경
git init
git add .
git commit -m "Initial commit"
 
# Gitea에 푸시
git remote add origin http://YOUR_GITEA_IP:3000/username/obsidian-vault.git
git push -u origin main

Quartz에 Submodule로 추가:

cd ~/quartz-blog
git submodule add http://YOUR_GITEA_IP:3000/username/obsidian-vault.git content
git commit -m "Add content submodule"
git push

디렉토리 구조:

quartz-blog/
├── content/                    # Submodule (옵시디언 Vault)
│   ├── 4_Project Notes/
│   │   └── joshua_blog/       # 블로그로 공개할 노트들
│   │       ├── index.md
│   │       ├── 개발/
│   │       └── toy/
│   ├── Assets/                # 이미지 등
│   └── (기타 private 노트들)
├── quartz/                    # Quartz 엔진
├── quartz.config.ts          # 설정 파일
└── package.json

2.3 Quartz 설정 파일 수정

quartz.config.ts:

import { QuartzConfig } from "./quartz/cfg"
import * as Plugin from "./quartz/plugins"
 
const config: QuartzConfig = {
  configuration: {
    pageTitle: "내 블로그 제목",
    enableSPA: true,
    enablePopovers: true,
    analytics: {
      provider: "google",  // 선택사항
      tagId: "G-XXXXXXXXXX",
    },
    locale: "ko-KR",
    baseUrl: "your-site.pages.dev",  // 나중에 Cloudflare 주소로 변경
    ignorePatterns: ["Private", "Templates", ".obsidian"],
    defaultDateType: "created",
    theme: {
      fontOrigin: "googleFonts",
      cdnCaching: true,
      typography: {
        header: "Noto Sans KR",
        body: "Noto Sans KR",
        code: "D2Coding",
      },
      colors: {
        lightMode: {
          light: "#faf8f8",
          lightgray: "#e5e5e5",
          gray: "#b8b8b8",
          darkgray: "#4e4e4e",
          dark: "#2b2b2b",
          secondary: "#284b63",
          tertiary: "#84a59d",
          highlight: "rgba(143, 159, 169, 0.15)",
        },
        darkMode: {
          light: "#161618",
          lightgray: "#393639",
          gray: "#646464",
          darkgray: "#d4d4d4",
          dark: "#ebebec",
          secondary: "#7b97aa",
          tertiary: "#84a59d",
          highlight: "rgba(143, 159, 169, 0.15)",
        },
      },
    },
  },
  plugins: {
    transformers: [
      Plugin.FrontMatter(),
      Plugin.CreatedModifiedDate({
        priority: ["frontmatter", "filesystem"],
      }),
      Plugin.Latex({ renderEngine: "katex" }),
      Plugin.SyntaxHighlighting({
        theme: {
          light: "github-light",
          dark: "github-dark",
        },
        keepBackground: false,
      }),
      Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
      Plugin.GitHubFlavoredMarkdown(),
      Plugin.TableOfContents(),
      Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
      Plugin.Description(),
    ],
    filters: [Plugin.RemoveDrafts()],
    emitters: [
      Plugin.AliasRedirects(),
      Plugin.ComponentResources(),
      Plugin.ContentPage(),
      Plugin.FolderPage(),
      Plugin.TagPage(),
      Plugin.ContentIndex({
        enableSiteMap: true,
        enableRSS: true,
      }),
      Plugin.Assets(),
      Plugin.Static(),
      Plugin.NotFoundPage(),
    ],
  },
}
 
export default config

2.4 로컬에서 테스트

# 의존성 설치
npm install
 
# 특정 디렉토리만 빌드
npx quartz build --directory "content/4_Project Notes/joshua_blog"
 
# 로컬 서버 실행
npx quartz serve
 
# 브라우저에서 http://localhost:8080 확인

주의사항:

  • 이미지 경로: 옵시디언의 [[이미지.png]] 형식은 ![](이미지.png)로 자동 변환됨
  • Wikilinks: [[노트 제목]] 형식 지원
  • Assets 폴더는 Symlink로 연결 (빌드 시 자동 처리)

3단계: Cloudflare Pages 연동

3.1 Cloudflare 계정 생성

  1. https://dash.cloudflare.com 접속
  2. 무료 계정 생성

3.2 Pages 프로젝트 생성

Workers & Pages → Create application → Pages → Connect to Git

중요: Git 연결하지 말고 “Direct Upload” 선택!

  • Gitea/Self-hosted Git은 직접 연결 불가
  • Wrangler CLI로 업로드할 예정

프로젝트 이름 설정:

Project name: my-quartz-blog

생성 완료 후:

URL: https://my-quartz-blog.pages.dev

3.3 API Token 생성

Cloudflare Dashboard → My Profile → API Tokens → Create Token

권한 설정:

Token name: Quartz Deploy Token
Permissions:
  - Account / Cloudflare Pages / Edit
  
Zone Resources:
  - All zones from an account / [Your Account]

생성 후 Token 복사 (다시 볼 수 없음!)

3.4 Account ID 확인

Cloudflare Dashboard → Workers & Pages → 우측 사이드바
Account ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

4단계: 자동 배포 설정

4.1 Gitea Secrets 설정

Gitea: quartz-blog 저장소 → Settings → Secrets → Actions

추가할 Secrets:
1. ACTIONS_TOKEN
   - Value: Gitea Personal Access Token
   - 생성: Gitea → Settings → Applications → Generate Token
   - Scopes: repo (all)

2. CLOUDFLARE_API_TOKEN
   - Value: (3.3에서 생성한 Token)

3. CLOUDFLARE_ACCOUNT_ID
   - Value: (3.4에서 확인한 ID)

4.2 GitHub Actions Workflow 작성

.gitea/workflows/deploy.yml 생성:

name: Deploy to Cloudflare Pages
 
on:
  workflow_dispatch:  # 수동 실행 버튼
  schedule:
    - cron: '0 17 * * *'  # 매일 UTC 17:00 (한국시간 새벽 2시)
  push:
    branches:
      - main  # main 브랜치 푸시 시 자동 실행
 
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    
    steps:
      # 1. 저장소 체크아웃
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          submodules: recursive  # Submodule도 함께 clone
          fetch-depth: 0         # 전체 히스토리 (날짜 정보 필요)
          token: ${{ secrets.ACTIONS_TOKEN }}
      
      # 2. Content Submodule 최신화
      - name: Update content submodule
        run: |
          git submodule update --remote --merge content
          git config user.name "Gitea Actions"
          git config user.email "actions@gitea.local"
          
          # 변경사항이 있으면 커밋
          if [[ -n $(git status -s) ]]; then
            git add content
            git commit -m "chore: update content submodule to latest"
            git push https://${{ secrets.ACTIONS_TOKEN }}@YOUR_GITEA_DOMAIN/username/quartz-blog.git HEAD:main
          fi
      
      # 3. Node.js 설정
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
      
      # 4. 의존성 설치
      - name: Install dependencies
        run: npm install
      
      # 5. Assets Symlink 생성
      # (옵시디언 Assets 폴더를 블로그 폴더에서 참조 가능하게)
      - name: Create Assets symlink
        run: |
          ln -s "$(pwd)/content/Assets" "content/4_Project Notes/joshua_blog/Assets"
      
      # 6. Quartz 빌드
      - name: Build Quartz
        run: |
          npx quartz build --directory "content/4_Project Notes/joshua_blog"
      
      # 7. Symlink 제거 (배포 전 정리)
      - name: Remove Assets symlink
        run: |
          rm "content/4_Project Notes/joshua_blog/Assets"
      
      # 8. Cloudflare Pages 배포
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy public --project-name=my-quartz-blog

주의사항:

  • YOUR_GITEA_DOMAIN: 실제 Gitea 주소로 변경
  • username/quartz-blog: 실제 저장소 경로로 변경
  • my-quartz-blog: Cloudflare Pages 프로젝트 이름과 일치시키기

4.3 Workflow 파일 푸시

git add .gitea/workflows/deploy.yml
git commit -m "Add deployment workflow"
git push

테스트 및 배포

첫 배포 테스트

Gitea: quartz-blog 저장소 → Actions 탭
→ "Deploy to Cloudflare Pages" 선택
→ "Run workflow" 버튼 클릭

실행 로그 확인:

  1. Checkout ✓
  2. Update submodule ✓
  3. Setup Node.js ✓
  4. Install dependencies ✓
  5. Create symlink ✓
  6. Build Quartz ✓
  7. Remove symlink ✓
  8. Deploy to Cloudflare ✓

배포 확인

https://my-quartz-blog.pages.dev

접속하여 블로그 정상 작동 확인

자동 배포 확인

# 옵시디언 Vault에서 새 노트 작성
cd ~/Documents/ObsidianVault
git add .
git commit -m "Add new post"
git push
 
# 다음날 새벽 2시에 자동으로 블로그 업데이트됨
# 또는 Gitea Actions에서 수동 실행

트러블슈팅

문제 1: Submodule이 업데이트 안 됨

증상:

git submodule update --remote
Already up to date.
# 하지만 실제로는 새 커밋이 있음

해결:

# Submodule 초기화 다시 하기
git submodule deinit -f content
git rm -rf content
git submodule add http://YOUR_GITEA_IP:3000/username/obsidian-vault.git content
git submodule update --init --recursive

문제 2: 빌드 시 이미지가 안 보임

증상:

404 Error: image.png not found

원인:

  • Assets 폴더 경로 문제
  • Symlink가 제대로 생성 안 됨

해결:

# 로컬에서 테스트
cd quartz-blog
ln -s "$(pwd)/content/Assets" "content/4_Project Notes/joshua_blog/Assets"
npx quartz build --directory "content/4_Project Notes/joshua_blog"
 
# public 폴더에서 이미지 확인
ls -la public/Assets

문제 3: Cloudflare 배포 실패

증상:

Error: Authentication error

해결:

  1. Cloudflare API Token 재생성
  2. Gitea Secrets에서 CLOUDFLARE_API_TOKEN 업데이트
  3. Workflow 다시 실행

증상:

Error: Project not found

해결:

  • deploy.yml--project-name 확인
  • Cloudflare Pages에서 프로젝트 이름 다시 확인

문제 4: 한글 URL이 깨짐

증상:

/개발/toy/파일명 → /%EA%B0%9C%EB%B0%9C/...

해결:

// quartz.config.ts
configuration: {
  ...
  slugifyFn: (str: string) => {
    // 한글 URL 유지
    return str
      .toLowerCase()
      .replace(/\s+/g, '-')
  }
}

문제 5: Gitea Actions Runner가 작동 안 함

증상:

Workflow waiting for runner...

해결:

# TrueNAS에서 Gitea Act Runner 상태 확인
Apps gitea-act-runner Logs
 
# Runner 재시작
Apps gitea-act-runner Stop Start
 
# Gitea에서 Runner 등록 상태 확인
Site Administration Actions Runners

추가 설정 (선택사항)

Custom Domain 연결

Cloudflare Pages → my-quartz-blog → Custom domains
→ Add a custom domain: blog.example.com

DNS 설정:

Type: CNAME
Name: blog
Content: my-quartz-blog.pages.dev

quartz.config.ts 업데이트:

baseUrl: "blog.example.com",

Google Analytics 추가

// quartz.config.ts
analytics: {
  provider: "google",
  tagId: "G-XXXXXXXXXX",
}

댓글 시스템 (Giscus)

// quartz.config.ts
plugins: {
  emitters: [
    Plugin.ContentPage({
      enableGiscus: true,
      giscus: {
        repo: "username/repo",
        repoId: "R_...",
        category: "General",
        categoryId: "DIC_...",
      }
    }),
  ]
}

체크리스트

설정 완료 확인

  • Gitea 서버 실행 중
  • Gitea Act Runner 등록됨
  • Quartz 저장소 생성
  • Content submodule 추가
  • Cloudflare Pages 프로젝트 생성
  • API Token 발급
  • Secrets 설정 완료
  • Workflow 파일 작성
  • 첫 배포 성공
  • 도메인 접속 확인
  • 자동 배포 테스트

일상 워크플로우

  1. 옵시디언에서 노트 작성
  2. Git commit & push
  3. (선택) Gitea Actions에서 수동 실행
  4. (자동) 매일 새벽 2시 자동 배포
  5. 블로그 확인

참고 자료


마치며

이 설정을 통해 다음과 같은 이점을 얻을 수 있습니다:

  1. 완전한 소유권: 모든 데이터가 내 서버에 있음
  2. 자동화: 옵시디언에서 작성하면 자동으로 배포
  3. 무료 호스팅: Cloudflare Pages 무료 플랜 활용
  4. 빠른 속도: Cloudflare CDN으로 전세계 어디서나 빠름
  5. 확장성: 필요 시 커스터마이징 가능

relates