n8n AI Agent로 블로그 태그 자동 분류하기: Ghost 연동 심화편
AI가 블로그 본문을 읽고 기존 태그에서 선택하고, 필요하면 새 태그도 제안합니다. Ghost API가 새 태그를 자동 생성하므로 프롬프트 수정 없이 태그가 확장됩니다.
1. 태그 분류의 고통
이전 글에서 n8n → Ghost 포스팅 자동화를 구축했습니다. 그런데 한 가지 불편함이 남았습니다:
매번 글을 올릴 때마다...
1. 본문을 다시 훑어봄
2. "이 글은 Docker 태그? 인프라 태그? 둘 다?"
3. Ghost Admin에서 수동으로 태그 선택
4. 태그 잊어버리면 나중에 수정하러 다시 들어감
태그가 5개일 때는 괜찮았습니다. 11개로 늘어나니 매번 고민됩니다. 그리고 새로운 주제의 글을 쓸 때마다 "태그를 추가해야 하나?" 고민도 생깁니다.
해결책: AI가 본문을 읽고 기존 태그에서 선택하고, 필요하면 새 태그도 제안하게 합니다.
2. 설계: Closed vs Open
2.1 Closed Set (기존 방식)
AI: "이 목록에서만 골라"
→ ["ghost-tag", "docker", "infra", ...]
→ 새 주제 나오면 수동으로 태그 추가
문제: Kubernetes 글을 쓰면? React 글을 쓰면? 매번 태그 추가하고 프롬프트 수정해야 합니다.
2.2 Open Set (개선 방식)
AI: "기존 태그 우선, 없으면 새로 제안해"
→ { existing: ["infra"], new: ["kubernetes"] }
→ Ghost가 새 태그 자동 생성
장점:
- 새 주제에 유연하게 대응
- 프롬프트 수정 없이 태그 확장
- Ghost API가 존재하지 않는 slug는 자동 생성
3. 아키텍처
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Webhook │───▶│ Code │───▶│ AI Agent │───▶│ Code │───▶│ HTTP Request│
│ (트리거) │ │ (본문 추출) │ │ (태그 분류) │ │ (요청 생성) │ │ (Ghost) │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
OpenAI GPT-4o-mini
│
┌───────┴───────┐
│ existing: [...] │
│ new: [...] │
└───────────────┘
4. Ghost API의 태그 자동 생성
핵심 발견: Ghost Admin API는 존재하지 않는 태그 slug를 넣으면 자동으로 생성합니다.
// 포스트 생성 요청
{
"posts": [{
"title": "Kubernetes 입문",
"tags": [
{ "slug": "infra" }, // 기존 태그
{ "slug": "kubernetes" } // 없는 태그 → 자동 생성!
]
}]
}
이 덕분에 워크플로우가 단순해집니다:
- AI가 태그 slug 목록 반환
- 그대로 Ghost API에 전달
- Ghost가 알아서 새 태그 생성
5. AI Agent 프롬프트
5.1 System Message
당신은 블로그 글 분류 전문가입니다. 주어진 블로그 본문을 읽고 가장 적절한 태그 2-4개를 선택합니다. 기존 태그를 우선 사용하되, 필요하면 새 태그를 제안할 수 있습니다. 반드시 JSON 형식으로만 응답하세요.
5.2 User Prompt
다음 블로그 글의 본문을 읽고 가장 적절한 태그를 2-4개 선택해주세요.
## 기존 태그 목록 (우선 사용)
- ghost-tag: Ghost 블로그 관련
- infra: 서버, 네트워크, DevOps
- automation: n8n, 스크립트, 워크플로우
- javascript: JS/TS 프론트엔드
- chrome-extension: 크롬 확장 개발
- typescript: TypeScript 관련
- docker: Docker/컨테이너
- security: 보안 관련
- ai: AI/LLM 관련
- n8n: n8n 워크플로우
- claude-code: Claude Code 관련
## 규칙
1. 기존 태그 목록에서 적합한 것을 먼저 선택하세요.
2. 기존 태그로 분류하기 어려운 핵심 주제가 있다면, 새 태그를 제안해도 됩니다.
3. 새 태그는 slug 형식(영문 소문자, 하이픈)으로 작성하세요. 예: kubernetes, react-native
4. 너무 구체적인 태그는 피하세요. 예: react-usestate-hook (X) → react (O)
## 본문
{{ $json.body }}
## 응답 형식 (JSON만, 설명 없이)
{
"existing": ["ghost-tag", "docker"],
"new": ["kubernetes"]
}
5.3 프롬프트 설계 포인트
| 규칙 | 이유 |
|---|---|
| 기존 태그 우선 | 태그 일관성 유지 |
| slug 형식 강제 | Ghost URL과 호환 |
| 구체적 태그 금지 | 태그 폭발 방지 |
| 2-4개 제한 | 과도한 태그 방지 |
6. Code 노드: 태그 파싱 + Ghost Body 생성
// 이전 노드에서 데이터 가져오기
const extractedData = $('Extract Content').first().json;
const aiResponse = $input.first().json.output;
// AI 응답에서 태그 파싱
let existing = [];
let newTags = [];
try {
// JSON 객체 추출 (설명이 섞여 있을 수 있음)
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
existing = parsed.existing || [];
newTags = parsed.new || [];
}
} catch (e) {
console.log('태그 파싱 실패:', e.message);
// 폴백: 배열 형식 시도 (이전 버전 호환)
try {
const arrayMatch = aiResponse.match(/\[[\s\S]*?\]/);
if (arrayMatch) {
existing = JSON.parse(arrayMatch[0]);
}
} catch (e2) {
existing = ['ghost-tag']; // 최종 폴백
}
}
// 모든 태그를 slug 형식으로 통합
const allTagSlugs = [...existing, ...newTags];
const tags = allTagSlugs.map(slug => ({
slug: slug.toLowerCase().replace(/\s+/g, '-')
}));
// Ghost API 요청 본문 생성
const ghostBody = {
posts: [{
title: extractedData.title,
mobiledoc: JSON.stringify({
version: "0.3.1",
markups: [],
atoms: [],
cards: [["markdown", { markdown: extractedData.body }]],
sections: [[10, 0]]
}),
slug: extractedData.slug,
custom_excerpt: extractedData.excerpt,
meta_description: extractedData.metaDescription,
tags: tags, // slug로 전달 → Ghost가 없으면 자동 생성
status: "draft"
}]
};
return {
ghostBody,
existingTags: existing,
newTags: newTags,
allTags: allTagSlugs,
title: extractedData.title
};
6.1 핵심 변경점
기존 (Closed):
// ID로 매핑 필요
const TAG_IDS = { 'ghost-tag': '69761ec...' };
const tags = slugs.map(s => ({ id: TAG_IDS[s] }));
개선 (Open):
// slug만 전달, Ghost가 자동 생성
const tags = slugs.map(s => ({ slug: s }));
7. 테스트
7.1 기존 태그만 사용되는 케이스
curl -X POST https://n8n.example.dev/webhook/ghost-post \
-H "Content-Type: application/json" \
-d '{
"content": "# Docker Compose로 Ghost 설치하기\n\n## 1. 소개\n\nDocker Compose로 Ghost를 설치합니다...\n\n---\n\n## Ghost SEO 설정\n\n| 항목 | 값 |\n|------|-----|\n| Post URL | `test-docker` |"
}'
AI 응답:
{
"existing": ["ghost-tag", "docker", "infra"],
"new": []
}
7.2 새 태그가 제안되는 케이스
curl -X POST https://n8n.example.dev/webhook/ghost-post \
-H "Content-Type: application/json" \
-d '{
"content": "# Kubernetes에서 Ghost 배포하기\n\n## 1. 소개\n\nHelm Chart로 Ghost를 Kubernetes 클러스터에 배포합니다...\n\n---\n\n## Ghost SEO 설정\n\n| 항목 | 값 |\n|------|-----|\n| Post URL | `k8s-ghost` |"
}'
AI 응답:
{
"existing": ["ghost-tag", "infra"],
"new": ["kubernetes"]
}
Ghost Admin에서 확인하면 kubernetes 태그가 자동 생성되어 있습니다.
8. 태그 관리 전략
8.1 새 태그 모니터링
주기적으로 Ghost에서 태그 목록을 확인합니다:
curl -s "https://blog.example.dev/ghost/api/admin/tags/?limit=all" \
-H "Authorization: Ghost $TOKEN" | jq '.tags[] | {name, slug, count: .count.posts}'
8.2 태그 정리 기준
| 상황 | 조치 |
|---|---|
| 글 1개뿐인 태그 | 상위 태그로 병합 고려 |
| 비슷한 태그 중복 | 하나로 통합 (예: k8s → kubernetes) |
| 너무 구체적 | 삭제 또는 상위 태그로 대체 |
8.3 프롬프트 업데이트 시점
새 태그가 3개 이상 글에 사용되면 프롬프트의 기존 태그 목록에 추가합니다. 이렇게 하면 AI가 더 일관되게 해당 태그를 선택합니다.
9. 비용 분석
9.1 OpenAI API 비용
gpt-4o-mini 기준:
| 항목 | 비용 |
|---|---|
| Input | $0.15 / 1M tokens |
| Output | $0.60 / 1M tokens |
블로그 글 하나당:
- 프롬프트 + 본문: ~2,500 토큰
- AI 응답: ~50 토큰
- 글 당 비용: ~$0.0004 (약 0.5원)
월 30개 글 = 약 15원
9.2 대안: 로컬 LLM
로컬 LLM을 사용하면 API 비용 0원입니다. n8n의 Ollama Chat Model 노드를 AI Agent에 연결하면 됩니다.
10. 트러블슈팅
10.1 AI 응답 파싱 실패
원인: LLM이 JSON 외에 설명을 덧붙임
해결: 정규식으로 JSON 부분만 추출
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
10.2 태그가 생성되지 않음
원인: slug에 대문자나 공백 포함
해결: 소문자 변환 + 공백을 하이픈으로
slug.toLowerCase().replace(/\s+/g, '-')
10.3 너무 구체적인 태그 생성
원인: 프롬프트 규칙이 약함
해결: 규칙 4번 강화 + few-shot 예시 추가
"예: react-usestate-hook (X) → react (O)
docker-compose-yaml (X) → docker (O)"
10.4 이전 노드 데이터 접근 불가
원인: AI Agent 이후 노드에서 컨텍스트 분리
해결: 노드 이름으로 명시적 참조
$('Extract Content').first().json
11. 확장 아이디어
11.1 태그 통계 대시보드
n8n에서 주기적으로 Ghost 태그 통계를 수집해서 Notion이나 Slack으로 전송:
매주 월요일:
- 새로 생성된 태그 목록
- 태그별 글 수
- 사용되지 않는 태그 알림
11.2 태그 제안 검토 워크플로우
새 태그가 제안되면 Slack으로 승인 요청:
AI가 새 태그 'kubernetes' 제안
→ Slack 알림: "승인하시겠습니까?"
→ 승인 시 Ghost에 포스트 생성
→ 거부 시 기존 태그만 사용
11.3 다국어 태그
// 프롬프트에 추가
"한국어 글은 'korean' 태그 추가
영어 글은 'english' 태그 추가"
12. 핵심 요약
| 항목 | Closed Set | Open Set |
|---|---|---|
| 태그 범위 | 지정된 목록만 | 기존 + 새 태그 제안 |
| 새 주제 대응 | 수동 추가 필요 | AI가 자동 제안 |
| Ghost 연동 | 태그 ID 매핑 | slug만 전달 (자동 생성) |
| 유지보수 | 프롬프트 수정 필요 | 주기적 정리만 |
13. FAQ
Q: 왜 gpt-4o-mini인가요?
A: 태그 분류는 간단한 작업이라 저렴한 모델로 충분합니다.
Q: AI가 이상한 태그를 만들면요?
A: Draft로 저장되므로 발행 전 확인할 수 있습니다. 주기적으로 태그 목록을 정리하세요.
Q: 기존 글의 태그도 AI로 재분류할 수 있나요?
A: 네. Ghost API로 모든 글을 조회하고 AI로 재분류한 후 업데이트하는 워크플로우를 만들 수 있습니다.
Q: 태그 이름(name)은 어떻게 되나요?
A: Ghost가 slug에서 자동 생성합니다. kubernetes → Kubernetes. 나중에 Ghost Admin에서 한글 이름으로 수정할 수 있습니다.