[Side Project] ๐ช Velog ๊ธ์ ์๋์ผ๋ก GitHub Pages ๋ธ๋ก๊ทธ๋ก ๋๊ธฐํํ๊ธฐ (with Jekyll + GitHub Actions) + Discord ์๋ฆผ๊น์ง ์ ์กํ๊ธฐ!
October 28, 2025
๋ด๊ฐ ์ง๊ธ๊น์ง ์์ฑํ ๊ฐ๋ฐ ๋ธ๋ก๊ทธ + ํฌํธํด๋ฆฌ์ค + ๊ธฐํ ๋ด ์๊ฐ๋ฅผ ํ ๊ณณ์์ ๋ณด์ฌ์ฃผ๊ณ ์ถ์ด์ Jekyll ๋ธ๋ก๊ทธ๋ฅผ ๋ง๋ค์ด์ผ๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์๋ค. ๊ทธ๋ฆฌ๊ณ Velog์ ์ฌ๋ฆฐ ๊ธ์ด ์๋์ผ๋ก ๋ด ๊ฐ์ธ ๋ธ๋ก๊ทธ(GitHub Pages)์ ๋ฐ์๋๋ค๋ฉด ์ด๋จ๊น? ์ด๋ฒ ๊ธ์์๋ Velog โ Jekyll โ GitHub Pages ์๋ ๋ฐฐํฌ ์์คํ ์ ๋ง๋ค์ด๋ณธ ๊ณผ์ ์ ์ ๋ฆฌํ๋ค. ํ ๋ฒ ์ค์ ํ๋ฉด ์ดํ์ Velog์ ๊ธ์ ์ฌ๋ฆฌ๋ ๊ฒ๋ง์ผ๋ก ๋ธ๋ก๊ทธ๊ฐ ์๋ ๊ฐฑ์ ๋๋ค.
๐ ๋ชฉํ ๊ตฌ์กฐ Velog โ (RSS ํ์ฑ) โณ Python ๋ณํ ์คํฌ๋ฆฝํธ (์ด๋ฏธ์ง ํฌํจ) โณ GitHub Push โณ GitHub Actions โ Jekyll ๋น๋ & ๋ฐฐํฌ
โ๏ธ 1. Jekyll ๋ธ๋ก๊ทธ ์ด๊ธฐ ์ธํ
๋จผ์ GitHub Pages์ฉ Jekyll ๋ธ๋ก๊ทธ๋ฅผ ์์ฑํ๋ค.
gem install jekyll bundler
jekyll new myblog
cd myblog
bundle exec jekyll serve
๋ก์ปฌ ์๋ฒ(http://127.0.0.1:4000/)์์ ํ ๋ง์ ๊ตฌ์ฑ์ ํ์ธํ ๋ค, aneomagig.github.io๋ผ๋ GitHub ์ ์ฅ์๋ฅผ ๋ง๋ค์ด ์ฐ๊ฒฐํ๋ค.
git init
git remote add origin https://github.com/aneomagig/aneomagig.github.io
git branch -M main
git push -u origin main
๐งฉ 2. Velog ๊ธ์ ์๋์ผ๋ก ๋ณํํ๋ Python ์คํฌ๋ฆฝํธ ์์ฑ
Velog๋ RSS ํผ๋๋ฅผ ์ ๊ณตํ๋ฏ๋ก, ์ด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํฌ์คํธ๋ฅผ Markdown์ผ๋ก ๋ณํํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์์ฑํ๋ค.
import feedparser, os, re, requests
from bs4 import BeautifulSoup
from datetime import datetime
USERNAME = "hosooinmymind"
RSS_URL = f"https://v2.velog.io/rss/@{USERNAME}"
POSTS_DIR = "_posts"
IMG_DIR = f"assets/images/{USERNAME}"
HEADERS = {"User-Agent": "Mozilla/5.0", "Referer": "https://velog.io/"}
os.makedirs(POSTS_DIR, exist_ok=True)
os.makedirs(IMG_DIR, exist_ok=True)
feed = feedparser.parse(RSS_URL)
for entry in feed.entries:
title = entry.title.strip()
slug = re.sub(r'[^a-zA-Z0-9๊ฐ-ํฃ]+', '-', title).strip('-')
date_parsed = datetime(*entry.published_parsed[:6])
date_filename = date_parsed.strftime("%Y-%m-%d")
date_str = date_parsed.strftime("%Y-%m-%d %H:%M:%S +0900")
filename = f"{POSTS_DIR}/{date_filename}-{slug}.md"
soup = BeautifulSoup(entry.description, "html.parser")
# ์ด๋ฏธ์ง ๋ก์ปฌ ์ ์ฅ
for img in soup.find_all("img"):
img_url = img.get("src")
if not img_url or not img_url.startswith("http"):
continue
rel_path = img_url.split("https://velog.velcdn.com/")[-1]
local_path = os.path.join(IMG_DIR, rel_path)
os.makedirs(os.path.dirname(local_path), exist_ok=True)
if not os.path.exists(local_path):
r = requests.get(img_url, headers=HEADERS)
if r.status_code == 200:
with open(local_path, "wb") as f:
f.write(r.content)
img["src"] = f"/{local_path.replace(os.sep, '/')}"
markdown = f"""---
layout: post
title: "{title}"
date: {date_str}
categories: velog
---
{str(soup)}
"""
with open(filename, "w", encoding="utf-8") as f:
f.write(markdown)
์ด ์คํฌ๋ฆฝํธ๋
- RSS์์ ํฌ์คํธ ๋ด์ฉ์ ํ์ฑ
- Velog ์ด๋ฏธ์ง(velcdn.com)๋ฅผ ๋ก์ปฌ๋ก ๋ค์ด๋ก๋
- Jekyll์ฉ _posts/YYYY-MM-DD-title.md ํ์ผ๋ก ์ ์ฅ ๊น์ง ์๋ ์ํํ๋ค.
โ๏ธ 3. GitHub Actions๋ก ์์ ์๋ํ
๋งค์ผ ์๋ฒฝ์ ์ด ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋๋๋ก GitHub Actions๋ฅผ ์ค์ ํ๋ค. ์๋ ํ์ผ์ ์์ฑํ๋ค.
๐ .github/workflows/velog-sync.yml
name: ๐ช Velog โ Jekyll Auto Sync
on:
schedule:
- cron: '0 9 * * *' # ๋งค์ผ ํ๊ตญ์๊ฐ ์คํ 6์ ์คํ
workflow_dispatch: # ์๋ ์คํ๋ ๊ฐ๋ฅ
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: ๐ฆ Checkout repository
uses: actions/checkout@v4
- name: ๐ Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: โ๏ธ Install dependencies
run: pip install feedparser requests beautifulsoup4
- name: ๐ Run Velog sync script
run: python velog_to_jekyll_images_fixed.py
- name: ๐งพ Commit & Push changes
run: |
git config user.name "Velog Sync Bot"
git config user.email "actions@github.com"
git add .
git diff --quiet && git diff --staged --quiet || git commit -m "๐ช Auto-sync Velog posts"
git push
์ด์ ๋งค์ผ ์ง์ ๋ ์๊ฐ๋ง๋ค
- RSS โ ํฌ์คํธ ๋ณํ
- ์๋ ์ปค๋ฐ & push
- GitHub Pages ์๋ ๋น๋ ๊ฐ ์ ๋ถ ํด๋ผ์ฐ๋์์ ๋์๊ฐ๋ค.
๐งฉ ์ด๋ฏธ์ง ๊นจ์ง ๋ฌธ์ ์ ํด๊ฒฐ ๊ณผ์
์ฒ์์ Velog์์ ๊ฐ์ ธ์จ ๊ธ๋ค์ด Jekyll ๋ธ๋ก๊ทธ์ ์ ํ์๋์์ง๋ง, ๋ณธ๋ฌธ์ ํฌํจ๋ ์ด๋ฏธ์ง๋ค์ด ์ ๋ถ ๊นจ์ ธ ์์๋ค. ๋ธ๋ผ์ฐ์ ์ฝ์์ ์ด์ด๋ณด๋ ๊ฒฝ๋ก๊ฐ ์ด๋ ๊ฒ ๋์ด ์์๋ค. /assets/images/hosooinmymind/image.png โ 404 (Not Found)
์ด์ ๋ฅผ ๋ถ์ํด๋ณด๋ Velog RSS์์ ์ ๊ณตํ๋ ๊ฒฝ๋ก๊ฐ ์ ๋์ฃผ์(https://velog.velcdn.com/โฆ)์ธ๋ฐ, RSS ๋ณํ ํ ๋จ์ํ Markdown๋ง ์ ์ฅํ๋ฉด Jekyll์ด ๋น๋ ์ ์ด ์ธ๋ถ ์ด๋ฏธ์ง๋ฅผ ๊ทธ๋๋ก ๋ณต์ฌํ์ง ๋ชปํ๋ ๊ฒ ์์ธ์ด์๋ค.
โ ์๋ชป๋ ๋ฐฉ์
์ฒ์์ ๋จ์ํ ๋ฅผ ์ ์งํ์ง๋ง, GitHub Pages๋ velog.velcdn.com ๋๋ฉ์ธ์์ ์ด๋ฏธ์ง๋ฅผ ๋ถ๋ฌ์ฌ ๋ CORS ์ ์ฑ
์ด๋ HTTPSโHTTP ํผํฉ ์ฝํ
์ธ ์ฐจ๋จ์ผ๋ก ์ธํด ์ด๋ฏธ์ง๋ฅผ ๋ ๋๋งํ์ง ๋ชปํ๋ค.
โ ํด๊ฒฐ ๋ฐฉ๋ฒ: ์ด๋ฏธ์ง ๋ค์ด๋ก๋ & ๊ฒฝ๋ก ๋ณํ
๊ฒฐ๊ตญ Python ์คํฌ๋ฆฝํธ์์ ์ด๋ฏธ์ง ํ์ผ์ ๋ก์ปฌ๋ก ๋ค์ด๋ก๋ํ ๋ค, ๋ชจ๋ ์ src๋ฅผ ๋ธ๋ก๊ทธ ๋ด๋ถ ๊ฒฝ๋ก๋ก ๋ฐ๊พธ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐํ๋ค.
๋ณ๊ฒฝ๋ ํต์ฌ ๋ถ๋ถ์ ์๋์ ๊ฐ๋ค ๐
for img in soup.find_all("img"):
img_url = img.get("src")
if not img_url or not img_url.startswith("http"):
continue
# Velog ์ด๋ฏธ์ง ๋๋ฉ์ธ ๊ธฐ์ค์ผ๋ก ํ์ผ ๊ฒฝ๋ก ์ฌ๊ตฌ์ฑ
rel_path = img_url.split("https://velog.velcdn.com/")[-1]
local_path = os.path.join(IMG_DIR, rel_path)
os.makedirs(os.path.dirname(local_path), exist_ok=True)
# ์ด๋ฏธ์ง ํ์ผ ์ ์ฅ
if not os.path.exists(local_path):
r = requests.get(img_url, headers={"User-Agent": "Mozilla/5.0"})
if r.status_code == 200:
with open(local_path, "wb") as f:
f.write(r.content)
# HTML ๊ฒฝ๋ก ์์
img["src"] = f"/{local_path.replace(os.sep, '/')}"
์ด ์ฝ๋๋ ๋ค์์ ์ํํ๋ค:
โ RSS์ ํ๊ทธ์์ ์๋ณธ src ์ถ์ถ https://velog.velcdn.com/โฆ
โก ์ด๋ฏธ์ง ํ์ผ์ assets/images/USERNAME/ ์๋์ ์ ์ฅ ๋ก์ปฌ ์ ์ ํ์ผ๋ก ๋ณํ
โข HTML ๋ด src ๊ฒฝ๋ก๋ฅผ /assets/โฆ ๋ก ๊ต์ฒด Jekyll์์ ๋ก๋ ๊ฐ๋ฅํ๊ฒ ๋ณ๊ฒฝ
๊ฒฐ๊ณผ์ ์ผ๋ก GitHub Pages์์๋ ๋ชจ๋ ์ด๋ฏธ์ง๊ฐ ๊นจ์ง์ง ์๊ณ ํ์๋์๋ค. ํนํ requests.get()์ User-Agent ํค๋๋ฅผ ์ถ๊ฐํ ๊ฒ์ด ์ค์ํ๋ค. Velog์ CDN์ด ๊ธฐ๋ณธ ์์ฒญ(ํค๋ ์๋ ์์ฒญ)์ ์ฐจ๋จํ๊ธฐ ๋๋ฌธ์ด๋ค.
๐ง ๋ฐฐ์ด ์
๋จ์ํ RSS ํฌ๋กค๋ง๋ง์ผ๋ก๋ ์์ ํ ๋ธ๋ก๊ทธ ๋ฐฑ์ ์ด ๋์ง ์๋๋ค. ์ด๋ฏธ์ง ๋ฆฌ์์ค๋ฅผ ํจ๊ป ๊ด๋ฆฌํด์ผ โ์คํ๋ผ์ธ์์๋ ๋ ๋ฆฝ์ ์ธ ๋ธ๋ก๊ทธโ๊ฐ ๊ฐ๋ฅํ๋ค. Jekyll๊ณผ ๊ฐ์ ์ ์ ๋ธ๋ก๊ทธ๋ ๋ชจ๋ ๋ฆฌ์์ค๊ฐ /assets ๊ฒฝ๋ก์ ํฌํจ๋์ด์ผ ํจ์ ๊ธฐ์ตํ์.
๐ก 4. ์์ฑ๋ ์๋ํ ํ๋ฆ
๐ ๋งค์ผ 1ํ GitHub Actions ํธ๋ฆฌ๊ฑฐ ๐ช Python ์คํฌ๋ฆฝํธ ์คํ -> Velog RSS ํ์ฑ & ์ด๋ฏธ์ง ๋ก์ปฌ ์ ์ฅ ๐พ _posts/ ๊ฐฑ์ -> ์ ๊ธ ์๋ ์ถ๊ฐ ๐ GitHub Pages ๋น๋ -> Jekyll ์ฌ์ดํธ ์ฌ๋ฐฐํฌ ์๋ฃ
๐ ๊ฒฐ๊ณผ
์ด์ Velog์ ์ ๊ธ์ ์ฌ๋ฆฌ๋ฉด ๋ค์ ๋ ์๋์ผ๋ก ๋ด Jekyll ๋ธ๋ก๊ทธ์๋ ์ฌ๋ผ์จ๋ค. ์ด๋ฏธ์ง๋ ๊นจ์ง์ง ์๊ณ , ์๋ฌธ ๊ตฌ์กฐ ๊ทธ๋๋ก ์ ์ง๋๋ค. Velog์ GitHub Pages๋ฅผ ํจ๊ป ์ด์ํ๋ ๊ฐ๋ฐ ๊ธฐ๋ก๊ณผ ๊ฐ์ธ ๋ธ๋ก๊ทธ๋ฅผ ํ๋์ ์ํ๊ณ์ฒ๋ผ ๋ค๋ฃฐ ์ ์๊ฒ ๋์๋ค.
Part 2. Discord ์๋ฆผ ์ค์
๋ ์๋ํ ์๋์ ๋งค์ผ ๋ด๊ฐ ๋ฒจ๋ก๊ทธ๋ฅผ ๋๊ธฐํํ๋ ๊ฑด ๋ง์ด ์ ๋๋ค๊ณ ์๊ฐ ์๋์ผ๋ก ํ๋ฃจ์ ํ ๋ฒ์ฉ ๋๊ธฐํํ๊ณ ๋์ค์ฝ๋ ์๋ฆผ๊น์ง ์ค๋๋ก ์ค์ ํ๋ค.
1๏ธโฃ ๋ชฉํ
- Velog RSS ํผ๋๋ฅผ ์ด์ฉํด ์๋์ผ๋ก ํฌ์คํธ๋ฅผ ์์ฑ
- ๊ฐ ๊ธ์ ์ด๋ฏธ์ง๋ ํจ๊ป ๋ค์ด๋ก๋ํ์ฌ _posts ํด๋์ ์ ์ฅ
- GitHub Actions๋ก ๋งค์ผ ์๋ ์คํ
- ๋ง์ง๋ง์ผ๋ก ๋์ค์ฝ๋ ์นํ ์๋ฆผ์ ๋ณด๋ด๋๋ก ์ค์
2๏ธโฃ Velog RSS ํฌ๋กค๋ง ์คํฌ๋ฆฝํธ
RSS ์ฃผ์ ์์ ๐ https://v2.velog.io/rss/hosooinmymind ์ด ํผ๋๋ฅผ ์ฝ์ด์ Jekyll์ฉ ๋งํฌ๋ค์ด ํฌ์คํธ๋ก ๋ณํํ๋ค.
velog_to_jekyl_images.py
import feedparser, os, re, requests
from datetime import datetime
from bs4 import BeautifulSoup
VELG_FEED_URL = "https://v2.velog.io/rss/hosooinmymind"
SAVE_DIR = "_posts"
IMAGE_DIR = "assets/images/hosooinmymind"
os.makedirs(SAVE_DIR, exist_ok=True)
os.makedirs(IMAGE_DIR, exist_ok=True)
feed = feedparser.parse(VELG_FEED_URL)
for entry in feed.entries:
title = re.sub(r'[\\/*?:"<>|]', '', entry.title.strip())
date = datetime(*entry.published_parsed[:6])
filename = f"{SAVE_DIR}/{date.strftime('%Y-%m-%d')}-{title.replace(' ', '-')}.md"
soup = BeautifulSoup(entry.description, "html.parser")
for img in soup.find_all("img"):
src = img["src"]
if src.startswith("https://velog.velcdn.com"):
img_name = src.split("/")[-2] + ".png"
save_path = f"{IMAGE_DIR}/{img_name}"
if not os.path.exists(save_path):
with open(save_path, "wb") as f:
f.write(requests.get(src).content)
img["src"] = f"/{save_path}"
post = f"""---
layout: post
title: "{entry.title}"
date: {date.strftime('%Y-%m-%d %H:%M:%S')} +0900
categories: velog
---
{soup}
"""
with open(filename, "w", encoding="utf-8") as f:
f.write(post)
3๏ธโฃ GitHub Actions ์๋ ์คํ ์ธํ
.github/workflows/velog-sync.yml ์๋์ผ๋ก ์ ์คํฌ๋ฆฝํธ๋ฅผ ์คํํ๋ ์ํฌํ๋ก ํ์ผ์ด๋ค.
name: Sync Velog posts
permissions:
contents: write # โ
์ ์ฅ์์ ํธ์ํ ๊ถํ ๋ถ์ฌ
on:
workflow_dispatch:
schedule:
- cron: "0 15 * * *" # ๋งค์ผ 00์ KST ์คํ
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: ๐ฆ Checkout repository
uses: actions/checkout@v3
- name: ๐ Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: โ๏ธ Install dependencies
run: |
pip install feedparser requests beautifulsoup4
- name: ๐ Run Velog sync script
run: python velog_to_jekyl_images.py
- name: ๐ช Commit & Push changes
run: |
git config user.name "Velog Sync Bot"
git config user.email "actions@github.com"
git add .
git commit -m "๐ช Auto-sync Velog posts" || echo "No changes to commit"
git push
- name: ๐ Send Discord notification
if: success()
run: |
curl -H "Content-Type: application/json" \
-d '{"content": "โ
Velog ๋๊ธฐํ ์๋ฃ! ์๋ก์ด ํฌ์คํธ๊ฐ ๋ธ๋ก๊ทธ์ ๋ฐ์๋์์ต๋๋ค."}' \
$
5๏ธโฃ Discord Webhook ์ค์
๋์ค์ฝ๋ ์๋ฒ์์ โ ์๋ฒ ์ค์ โ ํตํฉ โ Webhook โ ์ ์นํ ๋ง๋ค๊ธฐ ๋ณต์ฌํ URL์ GitHub Secrets์ ์ถ๊ฐ ๐น GitHub โ Settings โ Secrets โ Actions โ New repository secret
6๏ธโฃ ํ ์คํธ ์คํ
Actions โ Velog Sync โ Run workflow ํด๋ฆญ
๋ชจ๋ ๋จ๊ณ๊ฐ ์ด๋ก์ โ
์ด๋ฉด ์ฑ๊ณต.
๋์ค์ฝ๋์๋ ์ด๋ฐ ์๋ฆผ์ด ๋์ฐฉํ๋ค:

โจ ๋ง๋ฌด๋ฆฌ
์ด์ Velog์ ๊ธ์ ์ฌ๋ฆฌ๋ฉด GitHub Pages์ ์๋ ๋ฐ์๋๊ณ , ๋งค์ผ ์์ ์ ์ ๊ธ์ด ์๋์ผ๋ก ์ ๋ฐ์ดํธ๋๋ค.