시리즈: LangGraph 콘텐츠 자동화 | 2편 이전 편:
LangGraph로 콘텐츠 자동화 파이프라인 만들기 — 리서치부터 초안 발행까지 실전 코드 공개
LangGraph로 콘텐츠 자동화 파이프라인 만들기 — 리서치부터 초안 발행까지 실전 코드 공개
매일 블로그 글 쓰는 게 힘드셨죠? 저도 그랬어요. 그래서 AI 에이전트한테 시켰습니다.1일 1포스팅. 말은 쉬운데, 실제로 유지하다 보면 한계가 와요.주제 잡고 → 자료 찾고 → 구조 잡고 → 글
cherrycoding0.tistory.com
1편을 따라하다 보면 생기는 문제가 있어요.
"코드는 이해했는데, 이거 어떻게 자동으로 돌려요?"
저도 처음엔 그게 문제였어요. 파이프라인 자체는 로컬에서 잘 돌아가는데, 결국 내가 터미널 열고 python main.py 직접 쳐야 하는 구조였거든요. 그러면 자동화가 아니잖아요.
그래서 서버에 올리기로 했어요.
처음엔 Vercel 생각했어요. 프론트엔드 배포로 익숙하니까요. 근데 바로 문제가 생겼어요. LangGraph 파이프라인은 API 요청 한 번 받고 끝나는 게 아니라, 에이전트가 여러 단계를 거치면서 수십 초 이상 실행돼요. Vercel은 기본 타임아웃이 10초, Pro 플랜도 최대 300초인데 — 그것도 매번 핑을 쳐서 살려야 하는 서버리스 구조라 장기 실행 작업에는 적합하지 않아요.
그래서 Railway로 갔어요.
오늘은 Railway 배포 전체 과정 + 실제 운영하면서 생긴 문제들을 다 털어놓을게요. 삽질한 것까지 포함해서요.

Railway가 뭔데요? 왜 선택했어요?
Railway | The all-in-one intelligent cloud provider
Railway is a full-stack cloud for deploying web apps, servers, databases, and more with automatic scaling, monitoring, and security.
railway.com
Railway는 백엔드, 풀스택 앱을 클라우드에 배포할 수 있는 플랫폼이에요. Heroku랑 비슷한 개념인데, UI가 훨씬 깔끔하고 설정이 간단해요.
제가 Railway를 고른 이유는 세 가지예요.
첫째, **항상 떠 있는 서버(persistent server)**예요. Vercel처럼 요청이 없으면 내려가는 구조가 아니라, 항상 프로세스가 살아있어요. LangGraph처럼 오래 걸리는 작업에는 이게 필수예요.
둘째, cron 스케줄링을 기본으로 지원해요. Railway에서 cron job을 UI에서 설정할 수 있어요. GitHub Actions로 트리거하는 방법도 있지만, 그것보다 훨씬 간단해요.
셋째, 무료 크레딧이 월 $5 있어요. 가벼운 자동화 파이프라인이라면 무료로 돌릴 수 있어요. 저는 3개월째 무료 플랜으로 버티고 있어요 (트래픽이 없으니까요).
단점도 있어요. 무료 플랜은 sleepmode가 있고, 로그 보존 기간이 짧아요. 근데 개인 프로젝트 수준에서는 충분해요.
배포 전에 이것부터 챙겼어요
1. requirements.txt 정리
로컬에서 개발할 때는 pip install로 그때그때 설치했는데, 서버에선 그게 안 되니까 의존성을 파일로 정리해야 해요.
pip freeze > requirements.txt
근데 이걸 그대로 쓰면 안 돼요. pip freeze는 가상환경에 깔린 모든 패키지를 다 뱉어내거든요. 필요한 것만 추려야 해요.
제 파이프라인 기준으로 핵심 패키지는 이거예요.
langgraph>=0.2.0
langchain>=0.2.0
langchain-anthropic>=0.1.0
anthropic>=0.30.0
python-dotenv>=1.0.0
requests>=2.31.0
schedule>=1.2.0
버전 고정(==)보다 최소 버전(>=) 방식으로 쓰는 게 나중에 업데이트 대응할 때 편해요.
2. Procfile 만들기
Railway는 앱 실행 명령어를 Procfile이라는 파일로 받아요.
worker: python main.py
web:이 아니라 worker:로 써야 해요. HTTP 요청 받는 서버가 아니라 백그라운드에서 계속 돌아가는 워커니까요. 처음에 web:으로 썼다가 Railway가 포트 바인딩을 기다리다가 계속 실패했어요. 이거 찾는 데 30분 날렸어요.
3. 환경변수 분리
로컬에서는 .env 파일로 API 키를 관리했는데, 서버에서는 .env 파일을 올리면 안 돼요. .gitignore에 .env 있는지 반드시 확인하고, Railway 대시보드에서 환경변수를 직접 입력해야 해요.
제가 쓰는 환경변수 목록이에요.
ANTHROPIC_API_KEY=sk-ant-...
NOTION_API_KEY=secret_...
TISTORY_ACCESS_TOKEN=...
TARGET_BLOG_ID=cherrycoding0
RUN_SCHEDULE=06:00
python-dotenv 쓰면 로컬에서는 .env에서 읽고, Railway에서는 환경변수로 자동으로 읽어요. 코드 수정 없이 환경만 바꾸면 돼요.
from dotenv import load_dotenv
import os
load_dotenv() # 로컬에선 .env, Railway에선 environment variable
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
실제 배포 과정 — 이 순서대로 하면 돼요
Step 1. GitHub 레포지토리 연결
Railway는 GitHub 레포 연결로 배포해요. 코드 push하면 자동으로 재배포되는 구조예요.
Railway 대시보드 → New Project → Deploy from GitHub repo → 레포 선택
이게 전부예요. 설정 파일 없어도 Railway가 Python 프로젝트를 자동 감지하고 requirements.txt를 보고 패키지 설치해요.
Step 2. 환경변수 입력
배포된 서비스 클릭 → Variables 탭 → 환경변수 하나씩 입력
이때 주의할 게 있어요. Railway Variables에서 줄바꿈이 포함된 값(예: PEM 키)을 입력할 때 포맷이 깨지는 경우가 있어요. 그럴 때는 Base64로 인코딩해서 넣고 코드에서 디코딩하는 방식을 쓰는 게 안전해요.
Step 3. 배포 확인
Variables 저장하면 자동으로 재배포가 시작돼요. Deployments 탭에서 로그 실시간으로 볼 수 있어요.
처음 배포 성공했을 때 로그에 이게 찍히면 돼요.
✅ LangGraph pipeline initialized
✅ Schedule set: 06:00 daily
⏳ Waiting for next run...
Step 4. cron 스케줄링 설정
schedule 라이브러리로 코드 안에서 직접 시간을 잡아도 되고, Railway의 Cron Job 기능을 써도 돼요.
저는 코드 안에서 잡아요. 이유는 배포 환경 바뀌어도 스케줄 로직이 코드에 붙어 있으면 관리가 편하거든요.
import schedule
import time
def run_pipeline():
print("🚀 Pipeline started")
# LangGraph 파이프라인 실행 코드
result = content_pipeline.invoke({"topic": get_today_topic()})
publish_to_tistory(result)
print("✅ Pipeline completed")
schedule.every().day.at("06:00").do(run_pipeline)
while True:
schedule.run_pending()
time.sleep(60)
매일 오전 6시에 실행되고, 1분마다 스케줄 체크해요. Railway는 항상 프로세스가 살아있으니까 이 방식으로 cron 구현이 돼요.
운영하면서 진짜 생긴 문제들
배포하고 끝이 아니에요. 실제로 돌리면서 예상 못 한 일들이 생겨요. 저도 겪었던 것들 다 공유할게요.
문제 1. Claude API Rate Limit에 걸렸어요
LangGraph 파이프라인이 내부적으로 Claude API를 여러 번 호출해요. 리서치 단계, 아웃라인 단계, 초안 작성 단계마다 한 번씩이라면 한 실행에 최소 3~4번 호출하는 거예요.
그런데 무료/낮은 티어에서는 분당 호출 횟수 제한이 있어요. 특히 파이프라인 내에서 병렬 호출이 발생하면 RateLimitError가 터져요.
해결 방법은 두 가지였어요.
import time
def call_claude_with_retry(prompt, max_retries=3):
for attempt in range(max_retries):
try:
response = claude_client.messages.create(...)
return response
except anthropic.RateLimitError:
wait_time = 2 ** attempt # 1초, 2초, 4초
print(f"Rate limit hit. Waiting {wait_time}s...")
time.sleep(wait_time)
raise Exception("Max retries exceeded")
지수 백오프(exponential backoff) 패턴으로 재시도 로직을 넣었어요. 이거 추가하고 나서 rate limit 에러가 거의 사라졌어요.
문제 2. 파이프라인이 중간에 멈추는데 에러 메시지가 없었어요
어느 날 아침에 보니까 글이 올라오지 않았어요. Railway 로그 봤더니 파이프라인이 실행되다가 그냥 조용히 멈춰 있는 거예요. 에러도 없이요.
원인을 찾다 보니 Notion API 응답이 늦어지면서 타임아웃이 걸린 건데, 에러가 제대로 잡히지 않고 그냥 프로세스가 멈춰버린 거였어요.
두 가지로 해결했어요.
하나는 각 단계에 타임아웃을 명시적으로 설정했어요.
import requests
response = requests.get(url, timeout=30) # 30초 안에 응답 없으면 예외 발생
다른 하나는 파이프라인 전체를 try-except로 감싸고, 실패 시 슬랙 메시지를 보내도록 했어요. (슬랙 웹훅 설정은 5분이면 돼요.)
try:
run_pipeline()
except Exception as e:
send_slack_alert(f"❌ Pipeline failed: {str(e)}")
이제 실패하면 폰으로 알림이 와요. 어디서 터졌는지 바로 확인할 수 있어요.
문제 3. Railway 무료 플랜에서 sleepmode가 걸렸어요
Railway 무료 플랜은 일정 시간 요청이 없으면 서버가 잠들어요. 그러면 worker 프로세스도 내려가고, 스케줄러도 멈춰요.
이건 솔직히 무료 플랜의 한계예요. 해결책은 두 가지예요.
방법 1: 유료 플랜 전환 ($5/월 Hobby 플랜, sleepmode 없음)
방법 2: 외부에서 주기적으로 ping 보내기 (UptimeRobot 같은 서비스로 30분마다 HTTP 요청 보내서 깨우기)
저는 방법 2로 버티고 있어요. HTTP 엔드포인트를 하나 만들어두고, UptimeRobot 무료 플랜으로 30분마다 ping 보내요.
from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
class PingHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
def log_message(self, format, *args):
pass # 로그 조용히
def start_ping_server():
server = HTTPServer(('0.0.0.0', 8080), PingHandler)
server.serve_forever()
# 메인 스케줄러와 별도 스레드로 실행
threading.Thread(target=start_ping_server, daemon=True).start()
이 코드 추가하고 Railway 환경변수에 PORT=8080 설정하면 돼요. UptimeRobot에서 Railway 앱 URL로 30분 주기 모니터링 등록하면 sleepmode 방지 완성이에요.
문제 4. 로그가 너무 많아서 정작 중요한 게 안 보였어요
LangGraph 파이프라인이 실행되면 내부적으로 엄청난 양의 로그를 뱉어요. 어느 단계에서 뭐가 실패했는지 찾기가 너무 힘들었어요.
해결 방법은 간단했어요. logging 모듈 쓰면서 레벨 분리하기예요.
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
# 노이즈 줄이기
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("anthropic").setLevel(logging.WARNING)
# 내 파이프라인 로그만 INFO 레벨로
logger.info("🚀 Pipeline started")
logger.error("❌ Notion API failed: %s", str(e))
이렇게 하니까 Railway 로그 창에서 내가 찍은 것만 깔끔하게 보여요.
지금 실제로 어떻게 돌아가고 있냐면
매일 아침 6시에 파이프라인이 실행돼요. 오늘의 주제를 노션 DB에서 가져와서, Claude가 리서치하고 아웃라인 잡고 초안을 써요. 초안이 노션에 저장되면 제가 퇴근 후에 검토하고 발행 버튼 누르는 구조예요.
완전 자동 발행은 아직 안 해요. AI가 쓴 글을 검토 없이 올리는 건 아직 불안하거든요. 그래도 "매일 초안 만들기"가 자동화된 것만으로도 시간이 엄청 아껴졌어요.
솔직한 수치로 말하면, 예전엔 포스팅 하나에 2~3시간 걸렸는데, 지금은 초안 검토 + 수정에 30~40분이면 올릴 수 있어요.
솔직한 한계
이 방식, 완벽하지 않아요.
AI가 쓴 글은 아직 내 글이 아니에요. 초안이 나와도 읽어보면 어딘가 건조하거나, 제가 직접 겪은 경험이 빠진 느낌이에요. 그래서 검토하면서 개인 경험을 덧붙이는 단계는 생략할 수가 없어요.
노션 API 불안정 이슈가 가끔 있어요. Notion API가 간헐적으로 느려지거나 타임아웃이 걸려요. 이럴 때는 파이프라인이 실패하고 슬랙 알림이 와요. 대부분 재실행하면 해결되는데, 그게 귀찮긴 해요.
비용이 쌓여요. Claude API는 토큰 단위 과금이에요. 매일 하나씩 돌리면 한 달에 API 비용이 조금씩 나와요. 지금은 몇 천 원 수준인데, 파이프라인이 복잡해질수록 올라갈 수 있어요.
마무리: 자동화의 진짜 가치
사람들이 자동화를 이야기할 때 "일 안 해도 되는 시스템"을 기대하는 경우가 많아요.
근데 실제로 써보니까 달랐어요. 자동화의 진짜 가치는 "반복 작업에서 해방되어 내가 진짜 가치 있는 일에 집중할 수 있게 되는 것"이에요.
초안 만드는 건 AI가 하고, 나는 내 경험을 녹이는 데 집중한다. 이 구조가 지금 저한테 가장 잘 맞아요.
3편에서는 이 파이프라인에 자동 SEO 최적화 + 자동 발행까지 붙이는 걸 다뤄볼 예정이에요.
다음 포스팅 예고: LangGraph 자동화 3편 — 자동 SEO 메타 작성 + API로 직접 발행하기
'개발 실무' 카테고리의 다른 글
| [Claude Code 실전 정복 #2] VS Code 연동 + 실전 워크플로우 — 7년차 개발자의 프롬프트 패턴 공개 (2026) (0) | 2026.05.08 |
|---|---|
| [Claude Code 실전 정복 #1] 설치부터 첫 실행까지 — 7년차 개발자가 겪은 진짜 과정 (2026) (0) | 2026.05.07 |
| LangGraph로 콘텐츠 자동화 파이프라인 만들기 — 리서치부터 초안 발행까지 실전 코드 공개 (0) | 2026.05.03 |
| 구글 애드센스 가입부터 티스토리 연동까지 — 2026년 최신 완벽 정리 (승인 팁 포함) (0) | 2026.05.01 |
| 닷홈 무료호스팅으로 그누보드5 서버 연결하고 테마까지 세팅하는 법 (2026 ver) (1) | 2026.04.30 |