📚 이 글은 “개발자가 AI 자동화로 부업하는 실전 기록” 시리즈입니다.
- 📌 1편: AI 자동화로 개발자 부업하는 법 — 실패 3번 하고 나서야 알게 된 것들
- 📌 2편: WordPress + Claude API Day 1 세팅 기록
- 📌 3편: Slack 봇으로 블로그 자동 발행 트리거 만들기
- 📌 4편: Threads API 연동으로 블로그 발행과 SNS 자동 배포 연결하기
- 📌 5편: Notion API로 블로그 발행 기록 자동화하기
- 📌 6편: Windows Task Scheduler로 블로그 자동 발행 스케줄러 만들기
- 📌 7편: 블로그 SEO 최적화 실전 — Yoast SEO + 키워드 전략
- 📌 8편: 블로그 자동화 2주 운영 결과 공개
- 📌 9편: 쿠팡파트너스 가입부터 자동화까지
- 📌 10편: 구글 애드센스 신청 도전기
- 📌 11편: 구글 애드센스 신청 후기
- 📌 12편: 네이버 서치어드바이저 등록 후기
- 📌 13편: 블로그 자동화 한 달 총정리
- 📌 14편: Threads 마케팅으로 블로그 트래픽 늘리기
- 📌 15편: AI 자동화 부업 2개월 솔직 후기
- 📌 16편: 구글 애드센스 승인 결과 공개
- 📌 17편: WordPress 플러그인 최적화 실전
- 📌 18편: 카카오 애드핏 도전기
- 📌 19편: Python Pillow로 블로그 썸네일 자동 생성 — 이미지 템플릿 자동화 실전기 (현재 글)
- 📌 20편: 블로그 자동화 3개월 결과 공개 — 데이터로 보는 현실 (예정)
- 📌 21편: 구글 Search Console 활용법 — 검색 유입 데이터로 콘텐츠 전략 세우기 (예정)
← 18편: 카카오 애드핏 도전기 — 한국형 블로그 광고 수익화 채널 추가하기
솔직히 말하면, 나는 꽤 오랫동안 썸네일을 수작업으로 만들어 왔다. 글 발행은 자동화했으면서 정작 이미지는 Canva를 열고, 텍스트 치고, 내보내고, 업로드하는 과정을 매번 손으로 반복하고 있었다. 한 편당 5~10분이면 되지만, 그 5분이 쌓이면 결국 자동화의 의미가 퇴색된다는 걸 어느 날 갑자기 깨달았다.
“글 쓰는 것도 AI가 하고, 발행도 자동인데… 왜 썸네일만 내가 클릭하고 있지?”
그래서 이번 편은 Python Pillow로 블로그 이미지 자동 생성 시스템을 만든 이야기다. 완벽한 디자인 도구를 만들겠다는 게 아니라, 일단 돌아가는 썸네일 자동화를 구축하는 것이 목표였다. 결과부터 말하면 — 지금은 글 제목 하나만 넘겨주면 PNG 파일이 뚝딱 나온다.
썸네일을 자동화하려 했던 진짜 이유
처음에는 “어차피 Canva로 금방 만드는데” 싶었다. 실제로도 그랬다. 템플릿 하나 저장해 두고, 텍스트 바꾸고, 내보내면 끝. 불편하지도 않았다.
근데 문제는 이게 자동화 파이프라인의 병목이 된다는 점이었다. 내 시스템은 Slack에서 명령어를 보내면 글이 생성되고 WordPress에 발행되는 구조인데, 썸네일은 항상 내가 끼어들어야 했다. 자다가 새벽에 스케줄러가 돌아서 글이 나가도 썸네일은 없는 상태로 올라가거나, 내가 깨어있을 때만 제대로 올라가는 어정쩡한 상황이 반복됐다.
그리고 솔직히 말하면 또 다른 이유도 있었다. 썸네일이 없거나 허접하면 SNS 공유할 때 미리보기가 진짜 초라해 보인다. 애드센스 승인 받고 나서 방문자 유입이 조금씩 생기기 시작했는데, 그때부터 더 신경 쓰이기 시작했다. 사람들이 공유한 링크를 클릭할 때 첫인상이 바로 썸네일이니까.
썸네일 수작업은 사소해 보이지만, 자동화 파이프라인 전체를 막는 결정적인 병목이었다.
Python Pillow를 선택한 이유 — 다른 옵션들과 비교
썸네일을 코드로 만들겠다고 마음먹었을 때 몇 가지 선택지를 놓고 고민했다. 각각의 장단점을 표로 정리해 봤다.
| 방법 | 장점 | 단점 | 결론 |
|---|---|---|---|
| Canva API | 디자인 품질 높음, 템플릿 풍부 | 무료 플랜 API 제한, 월 비용 추가, 연동 복잡 | ❌ 비용·복잡도 부담 |
| DALL-E / Stable Diffusion | AI 이미지로 퀄리티 높음 | API 비용 크고, 텍스트 렌더링 불안정, 속도 느림 | ❌ 오버스펙 |
| Python Pillow | 무료, 빠름, 완전 커스텀 가능, 의존성 최소 | 디자인 직접 구현해야 함, 폰트 직접 관리 | ✅ 채택 |
| matplotlib | 그래프·차트 포함 가능 | 이미지 생성 목적으론 과함, 스타일 제한 | ❌ 부적합 |
결국 Pillow가 정답이었다. 비용이 0원이고, 내 파이썬 환경에 그냥 설치해서 바로 쓸 수 있고, 텍스트 위치나 색상, 배경 등 모든 걸 코드로 제어할 수 있다. 디자인을 잘 못해도 일관된 브랜드 느낌을 유지하는 건 얼마든지 가능하다.
처음에 “Pillow로 만들면 너무 촌스럽지 않을까” 걱정했는데, 배경색 + 그라디언트 + 적절한 폰트 조합만 잘 잡으면 충분히 봐줄 만한 결과물이 나온다. 완벽한 디자인이 아니라 없는 것보다 확실히 나은 것을 목표로 했고, 그건 달성했다.
Pillow는 무료에 빠르고 완전 커스텀 가능 — 자동화용 썸네일에는 이게 최선이다.
템플릿 설계 — 뭘 어디에 놓을지 먼저 정했다
코드 짜기 전에 먼저 템플릿 구조를 머릿속으로 그렸다. 처음에 바로 코드부터 열었다가 나중에 다 뒤엎는 실수를 이미 3번은 한 터라, 이번엔 구성부터 정리했다.
내가 목표로 잡은 썸네일 스펙은 이렇다.
- ✅ 크기: 1200 × 630px (Open Graph 기준 최적 비율)
- ✅ 배경: 진한 네이비 계열 단색 + 하단 포인트 라인
- ✅ 제목 텍스트: 흰색, 중앙 정렬, 자동 줄바꿈
- ✅ 브랜드 태그: 우하단에 “devYul” 고정 표시
- ✅ 카테고리 뱃지: 좌상단에 카테고리명 작은 박스로 표시
- ✅ 파일명: 슬러그 기반으로 자동 생성
이 정도면 충분하다고 판단했다. 블로그 카드나 SNS 공유 미리보기에서 텍스트가 잘 보이고 브랜드 일관성만 유지되면 그게 썸네일의 역할을 다하는 거니까.
한 가지 꽤 고생한 부분이 있었는데, 한글 폰트 문제였다. 기본 Pillow는 시스템 기본 폰트를 쓰는데, 한글이 깨지거나 너무 작게 나오는 경우가 있다. 나는 Noto Sans KR 폰트를 직접 다운받아서 프로젝트 폴더에 넣고 경로를 지정하는 방식으로 해결했다. 이 부분에서 한 시간 넘게 삽질했는데, 나중에 보면 별거 아닌데 처음엔 진짜 막막했다.
fonts/ 디렉토리에 넣어두는 방식이 가장 안정적입니다. 시스템 폰트 경로는 OS마다 달라서 이식성이 떨어집니다.또 한 가지 신경 쓴 게 텍스트 자동 줄바꿈이었다. 제목이 짧으면 한 줄, 길면 두 줄이나 세 줄로 자연스럽게 나눠져야 하는데, Pillow는 자동 줄바꿈 기능이 없어서 직접 구현해야 했다. 최대 글자 수 기준으로 텍스트를 잘라 여러 줄로 나누는 함수를 만들었다.
템플릿 구조를 코드보다 먼저 설계하면 나중에 뒤엎는 시간이 줄어든다.
실전 코드 — 썸네일 생성 함수 전체 구현
⚙️ 사전 준비: pip install Pillow 로 설치 후, 프로젝트 루트에 fonts/ 폴더를 만들고 Google Fonts에서 Noto Sans KR의 NotoSansKR-Bold.ttf와 NotoSansKR-Regular.ttf를 다운받아 넣어두세요. thumbnails/ 폴더도 미리 생성해 두세요.
from PIL import Image, ImageDraw, ImageFont
import os
import re
# 설정값
THUMBNAIL_DIR = "thumbnails"
FONT_BOLD = "fonts/NotoSansKR-Bold.ttf"
FONT_REGULAR = "fonts/NotoSansKR-Regular.ttf"
WIDTH, HEIGHT = 1200, 630
# 배경 및 색상 테마
BG_COLOR = (18, 32, 58) # 진한 네이비
ACCENT_COLOR = (0, 140, 255) # 포인트 블루
TEXT_COLOR = (255, 255, 255) # 흰색
TAG_BG_COLOR = (0, 140, 255) # 뱃지 배경
BRAND_COLOR = (180, 200, 230) # 브랜드 텍스트 색상 (연한 파랑)
def wrap_text(text, font, draw, max_width):
"""텍스트를 max_width 기준으로 자동 줄바꿈"""
words = text.split()
lines = []
current_line = ""
for word in words:
test_line = current_line + (" " if current_line else "") + word
bbox = draw.textbbox((0, 0), test_line, font=font)
line_width = bbox[2] - bbox[0]
if line_width <= max_width:
current_line = test_line
else:
if current_line:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
return lines
def title_to_slug(title):
"""제목을 파일명용 슬러그로 변환"""
slug = title.lower()
slug = re.sub(r'[^\w\s-]', '', slug)
slug = re.sub(r'[\s_]+', '-', slug)
slug = slug.strip('-')
return slug[:60] # 너무 길면 잘라냄
def generate_thumbnail(title, category="개발 실전 기록", output_dir=THUMBNAIL_DIR):
"""
블로그 썸네일 자동 생성
:param title: 블로그 글 제목
:param category: 카테고리 (뱃지에 표시)
:param output_dir: 저장 경로
:return: 생성된 파일 경로
"""
os.makedirs(output_dir, exist_ok=True)
# 이미지 생성
img = Image.new("RGB", (WIDTH, HEIGHT), BG_COLOR)
draw = ImageDraw.Draw(img)
# 폰트 로드
font_title = ImageFont.truetype(FONT_BOLD, 64)
font_brand = ImageFont.truetype(FONT_REGULAR, 28)
font_badge = ImageFont.truetype(FONT_BOLD, 26)
# 하단 포인트 라인
draw.rectangle([(0, HEIGHT - 8), (WIDTH, HEIGHT)], fill=ACCENT_COLOR)
# 좌측 세로 포인트 라인
draw.rectangle([(0, 0), (6, HEIGHT)], fill=ACCENT_COLOR)
# 카테고리 뱃지 (좌상단)
badge_padding = (20, 10)
badge_bbox = draw.textbbox((0, 0), category, font=font_badge)
badge_w = badge_bbox[2] - badge_bbox[0] + badge_padding[0] * 2
badge_h = badge_bbox[3] - badge_bbox[1] + badge_padding[1] * 2
badge_x, badge_y = 60, 60
draw.rectangle(
[(badge_x, badge_y), (badge_x + badge_w, badge_y + badge_h)],
fill=TAG_BG_COLOR,
outline=None
)
draw.text(
(badge_x + badge_padding[0], badge_y + badge_padding[1]),
category,
font=font_badge,
fill=TEXT_COLOR
)
# 제목 텍스트 (중앙 정렬, 자동 줄바꿈)
max_text_width = WIDTH - 120 # 좌우 여백 60px씩
lines = wrap_text(title, font_title, draw, max_text_width)
# 전체 텍스트 높이 계산
line_height = 80
total_text_height = len(lines) * line_height
start_y = (HEIGHT - total_text_height) // 2 + 20 # 약간 아래로
for i, line in enumerate(lines):
bbox = draw.textbbox((0, 0), line, font=font_title)
line_w = bbox[2] - bbox[0]
x = (WIDTH - line_w) // 2
y = start_y + i * line_height
draw.text((x, y), line, font=font_title, fill=TEXT_COLOR)
# 브랜드 태그 (우하단)
brand_text = "devYul.com"
brand_bbox = draw.textbbox((0, 0), brand_text, font=font_brand)
brand_w = brand_bbox[2] - brand_bbox[0]
draw.text(
(WIDTH - brand_w - 60, HEIGHT - 60),
brand_text,
font=font_brand,
fill=BRAND_COLOR
)
# 파일 저장
slug = title_to_slug(title)
filename = f"{slug}-thumbnail.png"
filepath = os.path.join(output_dir, filename)
img.save(filepath, "PNG", optimize=True)
print(f"✅ 썸네일 생성 완료: {filepath}")
return filepath
# 사용 예시
if __name__ == "__main__":
path = generate_thumbnail(
title="Python Pillow로 블로그 썸네일 자동 생성하기",
category="개발 실전 기록"
)
print(f"저장 경로: {path}")
처음 돌렸을 때 결과물이 나왔는데 솔직히 "오… 이게 되네?" 싶어서 뿌듯했다. 텍스트 줄바꿈이 제대로 안 돼서 제목이 잘리는 버그가 처음에 있었는데, `wrap_text` 함수에서 단어 단위로 자르는 로직을 손보니 해결됐다.
Pillow로 썸네일을 만드는 핵심은 텍스트 자동 줄바꿈과 중앙 정렬 계산 — 이 두 가지만 잘 잡으면 나머지는 쉽다.
WordPress REST API와 연동 — 이미지 자동 업로드까지
이미지 파일만 만들어서 로컬에 저장하면 반쪽짜리 자동화다. 진짜로 쓸모 있으려면 WordPress 미디어 라이브러리에 자동 업로드되고, 글의 피처드 이미지(대표 이미지)로 자동 설정되어야 한다.
WordPress REST API는 `/wp-json/wp/v2/media` 엔드포인트로 이미지 업로드를 지원한다. 여기서 중요한 게 인증인데, 나는 JWT Authentication for WP REST API 플러그인을 쓰고 있어서 토큰 기반으로 처리한다.
⚙️ 사전 준비: WordPress에 JWT Authentication for WP REST API 플러그인이 설치·활성화되어 있어야 합니다. .env 파일에 WP_URL, WP_USER, WP_PASSWORD가 설정되어 있어야 합니다.
import requests
import os
from dotenv import load_dotenv
load_dotenv()
WP_URL = os.getenv("WP_URL") # 예: https://devyul.com
WP_USER = os.getenv("WP_USER")
WP_PASSWORD = os.getenv("WP_PASSWORD")
def get_jwt_token():
"""JWT 토큰 발급"""
response = requests.post(
f"{WP_URL}/wp-json/jwt-auth/v1/token",
json={"username": WP_USER, "password": WP_PASSWORD}
)
response.raise_for_status()
return response.json()["token"]
def upload_thumbnail_to_wp(image_path, post_title):
"""
썸네일 이미지를 WordPress 미디어 라이브러리에 업로드
:param image_path: 로컬 이미지 파일 경로
:param post_title: 이미지 alt 텍스트 및 캡션에 사용할 제목
:return: WordPress 미디어 ID
"""
token = get_jwt_token()
headers = {
"Authorization": f"Bearer {token}",
"Content-Disposition": f'attachment; filename="{os.path.basename(image_path)}"',
"Content-Type": "image/png"
}
with open(image_path, "rb") as f:
image_data = f.read()
response = requests.post(
f"{WP_URL}/wp-json/wp/v2/media",
headers=headers,
data=image_data
)
response.raise_for_status()
media = response.json()
media_id = media["id"]
# alt 텍스트 업데이트
requests.post(
f"{WP_URL}/wp-json/wp/v2/media/{media_id}",
headers={"Authorization": f"Bearer {token}"},
json={"alt_text": post_title, "caption": f"{post_title} | devYul"}
)
print(f"✅ 미디어 업로드 완료 — ID: {media_id}")
return media_id
def set_featured_image(post_id, media_id):
"""
WordPress 글의 피처드 이미지 설정
:param post_id: 글 ID
:param media_id: 미디어 ID
"""
token = get_jwt_token()
response = requests.post(
f"{WP_URL}/wp-json/wp/v2/posts/{post_id}",
headers={"Authorization": f"Bearer {token}"},
json={"featured_media": media_id}
)
response.raise_for_status()
print(f"✅ 피처드 이미지 설정 완료 — Post ID: {post_id}, Media ID: {media_id}")
# 전체 흐름 통합 예시
def create_and_set_thumbnail(post_id, post_title, category="개발 실전 기록"):
"""글 ID와 제목을 받아 썸네일 생성 → 업로드 → 피처드 이미지 설정까지 원스텝"""
from thumbnail_generator import generate_thumbnail # 첫 번째 코드 파일
# 1. 썸네일 생성
image_path = generate_thumbnail(title=post_title, category=category)
# 2. WordPress에 업로드
media_id = upload_thumbnail_to_wp(image_path, post_title)
# 3. 피처드 이미지 설정
set_featured_image(post_id, media_id)
return media_id
이 두 파일을 연결하면 전체 흐름이 완성된다. 글 발행 스크립트에서 `create_and_set_thumbnail(post_id, title)`을 한 줄만 추가하면, 글이 발행되는 순간 썸네일도 자동으로 생성되고 피처드 이미지로 설정된다. 처음 이게 동작했을 때 진짜 신기했다. Slack에서 명령어 치면 글도 올라가고 썸네일도 붙어서 나오는 걸 보니까 이게 자동화구나 싶었다.
글 발행 스크립트에 함수 한 줄만 추가하면 썸네일 생성부터 피처드 이미지 설정까지 자동으로 끝난다.
실제 운영해보니 — 잘 됐던 것과 아쉬운 점
이 시스템을 약 2주 정도 실제 블로그에 붙여서 써봤다. 솔직하게 정리해 보면:
잘 됐던 것들:
- ✅ 매번 Canva 열고 닫는 5~10분이 완전히 사라졌다. 이것만으로도 충분히 값어치 한다.
- ✅ 새벽에 스케줄러가 돌아도 썸네일이 자동으로 붙어서 발행된다.
- ✅ 모든 글의 썸네일 디자인이 완전히 일관된다. 오히려 Canva로 만들 때보다 브랜드 통일성이 더 잘 유지된다.
- ✅ SNS 공유 시 Open Graph 이미지가 제대로 뜬다. 이게 체감상 클릭률에 영향 있는 것 같다.
아쉬운 점과 개선하고 싶은 것들:
- ⚠️ 이미지 자체는 솔직히 심플하다 못해 단순하다. 텍스트 + 단색 배경이라 눈에 확 들어오는 비주얼은 아니다.
- ⚠️ 제목이 너무 길면 (25자 이상) 세 줄로 나뉘어서 글자가 좀 작아 보인다. 글자 크기 동적 조절 로직이 필요하다.
- ⚠️ 카테고리별로 배경색 테마를 다르게 가져가면 더 좋겠다는 생각이 든다.
두 번째 문제인 글자 크기 자동 조절은 이미 개선 방향을 잡아뒀다. 줄 수에 따라 폰트 사이즈를 64 → 52 → 44로 단계적으로 줄이는 방식이다. 아직 적용 전이지만 이번 편 글 올리고 나서 바로 붙여볼 계획이다.
전체적으로는 "일단 돌아가는 것"이라는 기준에서는 합격점이다. 완벽한 디자인 도구를 만들겠다고 2달 설계만 하다가 글 0편 쓴 과거의 나를 생각하면, 이 정도 결과물을 2일 만에 붙인 게 훨씬 낫다.
완벽한 썸네일보다 자동으로 생성되는 썸네일이 낫다 — 일관성과 자동화가 디자인 품질보다 먼저다.
현재 솔직 평가 — 썸네일 자동화 이후 달라진 것
썸네일 자동화를 붙이고 나서 블로그 운영이 실제로 조금 달라졌다. 작은 변화지만 의미가 있다.
가장 크게 달라진 건 심리적 마찰이 줄었다는 것이다. 글을 쓰고 발행 명령을 날릴 때 "아, 썸네일도 만들어야 하는데"라는 생각이 없어졌다. 사소한 것 같아도 이런 마찰들이 쌓이면 결국 발행 빈도에 영향을 준다.
그리고 Threads나 SNS에 자동 공유될 때 미리보기 이미지가 있고 없고의 차이가 꽤 크다는 걸 다시 한번 확인했다. 텍스트만 있는 게시물보다 이미지가 붙은 게시물이 체감상 더 많이 노출되는 것 같다.
비용 측면에서는 Pillow는 완전 무료라 추가 지출이 0원이다. 현재 월 고정비용인 Hostinger($4.99/월) + Claude API(사용량 기반, 월 약 5,000~10,000원)에서 전혀 변화가 없다.
앞으로는 카테고리별 색상 테마 추가, 글자 크기 동적 조절, 그리고 가능하면 배경에 흐릿한 패턴이나 텍스처를 넣는 것도 시도해볼 생각이다. 하지만 그건 지금 시스템이 잘 돌아가면서 여유가 생겼을 때 얘기다.
📌 다음 편: 블로그 자동화 3개월 결과 공개 — 데이터로 보는 현실
- 자동화 3개월 운영 실제 트래픽 수치 공개
- 애드센스 + 애드핏 수익 현황 (숫자로)
- 잘 됐던 글 vs 잘 안 됐던 글 패턴 분석
- 3개월 지나고 나서 바꾸고 싶은 것들