📚 이 글은 “개발자가 AI 자동화로 부업하는 실전 기록” 시리즈입니다.
- 📌 1편: 왜 티스토리·네이버를 포기하고 WordPress로 갔나
- 📌 2편: Claude API + WordPress REST API로 글 자동 발행 만들기
- 📌 3편: Slack 봇으로 어디서든 글 발행 명령 내리기
- 📌 4편: Notion API로 블로그 발행 기록 자동화 — 포트폴리오 대시보드 만들기 (현재 글)
- 📌 5편: 수익화 첫 성과 — 애드센스 승인까지의 현실적인 기록 (예정)
← 3편: Slack 봇으로 어디서든 글 발행 명령 내리기
글을 쓰고 나서 드는 허탈함이 있다. “나 도대체 지금까지 몇 편이나 쓴 거지?” 싶어서 WordPress 관리자 페이지를 열어보면, 제목만 쭉 나열된 목록이 전부다. 발행 날짜, 어떤 키워드로 썼는지, Claude가 생성했는지 내가 직접 썼는지, 조회수는 어떤지 — 이런 맥락이 하나도 없다.
처음엔 그냥 넘겼다. 어차피 자동화가 다 해주는데 뭘 기록까지 해, 라고 생각했으니까. 그런데 한 달쯤 지나니까 문제가 보이기 시작했다. 같은 주제를 중복으로 발행한 적이 두 번이나 있었고, 어떤 키워드가 잘 됐는지 전혀 모르겠고, 무엇보다 “나 이거 꽤 열심히 했는데” 싶은데 그걸 보여줄 수 있는 게 아무것도 없었다.
그때 Notion이 생각났다. 이미 개인 업무 기록은 다 Notion에 하고 있었으니까. 어차피 Python 스크립트가 글을 발행하는 시점을 알고 있다면, 그 순간에 Notion에도 같이 기록하면 되지 않나? 그렇게 시작한 작업이 생각보다 훨씬 쓸만한 결과물이 됐다.
왜 굳이 Notion인가 — 기록 도구 선택의 이유
솔직히 처음엔 구글 스프레드시트도 고려했다. Python에서 gspread 라이브러리로 쓰는 게 Notion API보다 훨씬 쉽거든. 근데 두 가지 이유로 Notion으로 결정했다.
첫 번째는 시각적인 포트폴리오 문제다. 스프레드시트는 내가 보려고 쓰는 도구지만, Notion 데이터베이스는 링크 하나로 외부에 공유할 수 있다. “저 AI 자동화 블로그 운영하고 있어요”라고 말할 때, 실제로 운영 중인 대시보드를 보여줄 수 있다는 게 완전히 다른 얘기다. 프리랜서로 일하면서 포트폴리오의 힘을 너무 잘 알기 때문에 이게 결정적이었다.
두 번째는 필터링과 뷰 전환이다. 같은 데이터를 캘린더 뷰로도 보고, 갤러리 뷰로도 보고, 특정 키워드만 필터링해서 볼 수 있다는 게 스프레드시트로는 따라오기 힘들다.
| 항목 | Google Sheets | Notion DB |
|---|---|---|
| 설정 난이도 | 쉬움 (gspread) | 중간 (API 토큰 + DB ID) |
| 외부 공유 | 가능하나 보기 불편 | 깔끔한 공개 페이지 |
| 뷰 전환 | 불가 | 테이블·캘린더·갤러리 |
| 포트폴리오 활용 | 낮음 | 높음 |
| 비용 | 무료 | 무료 (개인 플랜) |
Notion은 단순한 기록 도구가 아니라, 외부에 보여줄 수 있는 포트폴리오 도구다.
Notion API 연동 전 준비 — 막혔던 부분 솔직하게
Notion API 문서는 생각보다 잘 돼 있다. 근데 처음에 나를 30분 정도 헤매게 만든 게 하나 있었는데, 바로 인테그레이션과 데이터베이스 연결이다.
API 토큰 만드는 건 금방 된다. Notion 설정 → 인테그레이션 → 새 인테그레이션 만들면 끝이다. 근데 이걸 만들고 나서 데이터베이스에 접근하려고 하면 403 에러가 뜬다. 나는 처음에 이게 토큰 문제인 줄 알고 세 번이나 새로 만들었다. 아니었다. 인테그레이션을 만든 다음에 Notion 데이터베이스 페이지에서 직접 “연결 추가”를 눌러서 해당 인테그레이션을 선택해줘야 한다. 이 과정을 빠뜨리면 토큰이 맞아도 접근이 안 된다.
데이터베이스 ID도 처음에 어디서 찾는지 몰랐다. Notion 데이터베이스 페이지를 열고 우측 상단 “공유” 버튼 옆 `…` 메뉴에서 “링크 복사”를 누르면 URL이 나오는데, 여기서 워크스페이스 이름 뒤에 오는 32자리 문자열이 DB ID다.
?v= 앞까지의 32자리 영문숫자입니다. 하이픈이 포함된 경우 제거하지 않아도 됩니다. 예: https://notion.so/workspace/1a2b3c4d...?v=...에서 1a2b3c4d... 부분.인테그레이션 생성 후 데이터베이스에 직접 연결 추가하는 단계를 빠뜨리면 반드시 403 에러가 난다. 이 한 줄이 30분을 아껴준다.
Notion 데이터베이스 구조 설계 — 어떤 필드를 넣을까
처음엔 욕심부려서 필드를 10개 넘게 만들었다. 태그, 조회수, 수익 예상치, 경쟁 키워드 난이도까지. 근데 자동화로 채울 수 없는 필드는 결국 공란으로 남는다는 걸 금방 깨달았다. 빈 칸이 많은 데이터베이스는 쓸수록 의욕을 꺾는다.
그래서 원칙을 하나 세웠다. “자동화로 자동 입력되는 필드만 만든다.” 수동으로 채워야 하는 필드는 나중에 필요할 때 추가하는 걸로 미뤘다. 결과적으로 아래 6개 필드로 정착했다.
- ✅ 글 제목 (Title) — WordPress에서 발행된 제목 그대로
- ✅ 발행 일자 (Date) — WordPress REST API 응답의 date 필드
- ✅ WordPress URL (URL) — 발행된 글의 permalink
- ✅ 핵심 키워드 (Text) — Claude에게 글 생성 요청 시 사용한 키워드
- ✅ 생성 방식 (Select) — AI 생성 / 직접 작성 / AI+수정
- ✅ 카테고리 (Select) — WordPress 카테고리와 동일하게
이 6개 중 발행 일자, WordPress URL, 카테고리는 WordPress REST API 응답에서 가져오고, 핵심 키워드와 생성 방식은 글 생성 요청 시 이미 변수로 가지고 있으니 그대로 넘기면 된다.
자동화로 채울 수 없는 필드는 처음부터 만들지 않는 것이 낫다. 빈 칸이 많은 데이터베이스는 오히려 시스템을 포기하게 만든다.
Python 코드 — WordPress 발행 직후 Notion에 기록하기
이제 실제 코드다. 구조는 간단하다. WordPress REST API로 글 발행이 성공하면, 응답 JSON에서 필요한 데이터를 뽑아서 Notion API로 새 페이지(= 새 행)를 만드는 것이다.
⚙️ 사전 준비: .env 파일에 NOTION_TOKEN(인테그레이션 시크릿), NOTION_DB_ID(데이터베이스 ID), WP_URL, WP_JWT_TOKEN이 설정되어 있어야 합니다. Notion 데이터베이스에 인테그레이션 연결이 완료된 상태여야 합니다.
import os
import requests
from datetime import datetime
from dotenv import load_dotenv
load_dotenv()
NOTION_TOKEN = os.getenv("NOTION_TOKEN")
NOTION_DB_ID = os.getenv("NOTION_DB_ID")
WP_URL = os.getenv("WP_URL")
WP_JWT_TOKEN = os.getenv("WP_JWT_TOKEN")
NOTION_HEADERS = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28"
}
def publish_to_wordpress(title: str, content: str, category_id: int) -> dict:
"""WordPress REST API로 글 발행 후 응답 반환"""
headers = {
"Authorization": f"Bearer {WP_JWT_TOKEN}",
"Content-Type": "application/json"
}
payload = {
"title": title,
"content": content,
"status": "publish",
"categories": [category_id]
}
response = requests.post(
f"{WP_URL}/wp-json/wp/v2/posts",
json=payload,
headers=headers
)
response.raise_for_status()
return response.json()
def log_to_notion(
title: str,
wp_response: dict,
keyword: str,
generation_type: str,
category_name: str
) -> None:
"""WordPress 발행 결과를 Notion 데이터베이스에 기록"""
published_date = wp_response.get("date", datetime.now().isoformat())
# WordPress는 'date' 필드가 로컬 시간 기준이므로 그대로 사용
# Notion Date 필드는 ISO 8601 형식 필요
if "T" not in published_date:
published_date = published_date + "T00:00:00"
notion_payload = {
"parent": {"database_id": NOTION_DB_ID},
"properties": {
"글 제목": {
"title": [{"text": {"content": title}}]
},
"발행 일자": {
"date": {"start": published_date}
},
"WordPress URL": {
"url": wp_response.get("link", "")
},
"핵심 키워드": {
"rich_text": [{"text": {"content": keyword}}]
},
"생성 방식": {
"select": {"name": generation_type}
},
"카테고리": {
"select": {"name": category_name}
}
}
}
response = requests.post(
"https://api.notion.com/v1/pages",
json=notion_payload,
headers=NOTION_HEADERS
)
if response.status_code == 200:
print(f"✅ Notion 기록 완료: {title}")
else:
print(f"❌ Notion 기록 실패: {response.status_code} - {response.text}")
# 사용 예시
if __name__ == "__main__":
keyword = "AI 자동화 개발자 부업"
title = f"{keyword} — 실전 가이드"
content = ""
wp_result = publish_to_wordpress(title, content, category_id=3)
log_to_notion(
title=title,
wp_response=wp_result,
keyword=keyword,
generation_type="AI 생성",
category_name="AI 자동화 부업"
)
코드 자체는 복잡하지 않다. 핵심은 `publish_to_wordpress`가 성공했을 때만 `log_to_notion`을 호출한다는 흐름이다. Notion 기록 실패가 WordPress 발행을 롤백하지는 않게 설계했다. 기록 실패는 그냥 로그로 남기고 넘어가는 게 낫다. 발행이 더 중요하니까.
Notion 기록 실패가 글 발행 자체를 막아선 안 된다. 두 기능의 실패는 서로 독립적으로 처리하자.
Slack 봇과 연결 — 발행 알림에 Notion 링크까지 포함하기
3편에서 만든 Slack 봇이 발행 완료 메시지를 보내준다. 여기에 Notion 대시보드 링크도 같이 붙이면 훨씬 유용해진다. Notion 응답 JSON에는 새로 만들어진 페이지의 URL이 포함돼 있어서 그걸 Slack 메시지에 넣으면 된다.
⚙️ 사전 준비: 3편의 Slack 봇 코드가 동작하는 상태여야 합니다. slack-bolt 라이브러리와 SLACK_BOT_TOKEN, SLACK_APP_TOKEN이 .env에 설정되어 있어야 합니다. Socket Mode가 활성화된 Slack 앱이 필요합니다.
import requests
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import os
from dotenv import load_dotenv
load_dotenv()
app = App(token=os.getenv("SLACK_BOT_TOKEN"))
def log_to_notion_and_get_url(
title: str,
wp_response: dict,
keyword: str,
generation_type: str,
category_name: str
) -> str:
"""Notion에 기록 후 생성된 페이지 URL 반환"""
NOTION_TOKEN = os.getenv("NOTION_TOKEN")
NOTION_DB_ID = os.getenv("NOTION_DB_ID")
headers = {
"Authorization": f"Bearer {NOTION_TOKEN}",
"Content-Type": "application/json",
"Notion-Version": "2022-06-28"
}
published_date = wp_response.get("date", "")
if "T" not in published_date:
published_date += "T00:00:00"
payload = {
"parent": {"database_id": NOTION_DB_ID},
"properties": {
"글 제목": {"title": [{"text": {"content": title}}]},
"발행 일자": {"date": {"start": published_date}},
"WordPress URL": {"url": wp_response.get("link", "")},
"핵심 키워드": {"rich_text": [{"text": {"content": keyword}}]},
"생성 방식": {"select": {"name": generation_type}},
"카테고리": {"select": {"name": category_name}}
}
}
response = requests.post(
"https://api.notion.com/v1/pages",
json=payload,
headers=headers
)
if response.status_code == 200:
notion_page_url = response.json().get("url", "")
return notion_page_url
return ""
@app.message("발행")
def handle_publish_command(message, say):
"""'발행' 키워드 포함 메시지 처리 — 실제 발행 로직은 생략"""
# 실제로는 Claude API 호출 → WordPress 발행 → Notion 기록 순서
# 여기서는 Slack 알림 부분만 예시로 표현
say("⏳ 글 생성 중입니다...")
# 발행 완료 후 Notion URL 포함 알림 예시
notion_url = "https://notion.so/your-page-url" # 실제 반환값 사용
wp_url = "https://your-blog.com/sample-post"
say(
f"✅ *발행 완료!*\n"
f"📝 글 보기: {wp_url}\n"
f"📊 Notion 기록: {notion_url}"
)
if __name__ == "__main__":
handler = SocketModeHandler(app, os.getenv("SLACK_APP_TOKEN"))
handler.start()
Slack에서 발행 명령을 내리면, 완료 알림에 WordPress 글 링크와 Notion 페이지 링크가 함께 온다. 스마트폰에서 Slack 알림 확인하고, 바로 Notion 앱 열어서 기록 확인하는 흐름이 만들어진다. 이게 생각보다 뿌듯하다. 뭔가 제대로 된 시스템 같은 느낌이 든다.
Slack 알림에 Notion 링크를 붙이면 발행 → 확인 → 기록 조회까지 한 흐름으로 연결된다.
실제로 써보니 — 한 달 후 솔직한 평가
이 시스템을 붙이고 한 달이 지났다. 가장 크게 달라진 게 뭐냐면, 중복 발행이 완전히 사라졌다. 이전에 같은 키워드로 비슷한 글을 두 번 냈던 건 기억이 없어서 그랬던 거다. 이제 Notion에서 키워드 필터 걸면 바로 보인다.
두 번째는 성과 추적이 되기 시작했다는 것이다. 아직 조회수 자동 연동은 안 했지만, 최소한 “어떤 키워드로 글을 많이 썼나”, “AI 생성과 직접 작성 비율이 어떻게 되나”는 바로 파악된다.
세 번째가 가장 의외였는데, 포트폴리오로 실제로 쓰게 됐다. 최근에 콘텐츠 자동화 관련 프리랜서 문의가 왔을 때, Notion 대시보드 링크를 공유했더니 “실제로 운영 중이군요”라는 반응이 바로 왔다. 숫자와 날짜가 찍힌 데이터베이스가 말로 설명하는 것보다 훨씬 설득력 있다.
한 가지 아쉬운 점은 Notion API의 속도다. 가끔 응답이 2~3초 걸릴 때가 있다. WordPress 발행이 끝나고 Notion 기록까지 완료되는 데 총 4~5초가 걸리는 경우가 있는데, Slack 봇을 통해 명령을 내리면 그 사이에 아무 피드백이 없어서 “혹시 실패한 건가?” 싶은 적이 있었다. 이건 중간에 “기록 중…” 메시지를 하나 더 보내는 걸로 해결했다.
기록 시스템은 만들고 끝이 아니라, 실제로 볼 때 의미 있어야 한다. 포트폴리오로 쓸 수 있다는 게 이 시스템의 진짜 가치다.
이 자동화 시스템, 지금까지 고정비가 얼마나 드나
한 달이 지난 시점에서 비용을 정리해봤다. 숨기고 싶은 마음도 있지만, 이 블로그의 원칙이 솔직하게 다 공개하는 거니까.
| 항목 | 월 비용 | 비고 |
|---|---|---|
| Hostinger 비즈니스 플랜 | 약 6,900원 | 도메인 포함, 연 결제 기준 |
| Claude API (claude-sonnet-4-6) | 약 7,000~21,000원 | 글 30~90편 기준, 사용량 따라 변동 |
| Notion API | 0원 | 개인 플랜 무료 |
| 합계 | 약 14,000~28,000원 | Notion 추가해도 비용 변화 없음 |
Notion 연동은 비용을 전혀 올리지 않았다. Notion API는 개인 플랜에서 무료로 사용할 수 있고, API 호출 제한도 초당 3회 정도인데 글 발행 용도로는 전혀 문제없다. 공짜로 포트폴리오 대시보드가 생