Ghost 뉴스레터 자동화: 이별도 자동이 되나요? (Sunset Policy 구현기 with n8n)
"구독자 수"는 허상입니다. 중요한 건 "반응하는 구독자"입니다.
메일을 5번, 10번을 보내도 열어보지 않는 유저는 냉정하게 말해 우리 도메인의 평판(Sender Reputation)을 깎아먹는 '비용'입니다.
그래서 마케팅에는 Sunset Policy(일몰 정책)라는 개념이 있습니다. 해가 지듯, 반응 없는 유저를 자연스럽게 정리하는 정책이죠. 하지만 Ghost CMS는 글 쓰기엔 최고지만, 이런 CRM 자동화 기능은 부족합니다.
그래서 직접 만들었습니다. Ghost Admin API + Mailgun + n8n을 엮어서요.
이 글은 그로스(Growth)를 위한 자동화 구축 과정과, 그 과정에서 겪은 처절한 삽질의 기록입니다.
---
1. 설계: 의식의 흐름 (Scenario)
처음엔 단순하게 생각했습니다.
"안 읽는 사람 지우면 되는 거 아냐?"
하지만 기계적으로 지우면, 나중에 그 사람이 "왜 나 구독 취소됐어?"라고 돌아올 때 곤란해집니다. 기회를 줘야 합니다. 그래서 아래와 같은 시나리오를 짰습니다.
- 발굴: 메일을 5통 이상 보냈는데, 오픈 횟수가 0인 '유령 구독자'를 찾는다.
- 낙인: 이들에게 inactive-warning이라는 라벨을 붙인다.
- 경고: "48시간 뒤에 구독 취소됩니다"라는 마지막 메일을 보낸다.
- 대기: 48시간을 기다려준다.
- 심판:
- 메일을 열었다? -> 생존 (라벨 제거)
- 안 열었다? -> 삭제 (DB에서 Delete)
이 모든 과정을 n8n이라는 워크플로우 툴로 자동화했습니다.
---
2. 구현 과정 (The Code & The Struggles)
Step 1. 유령 구독자 찾아내기 (Ghost API의 함정)
n8n의 HTTP Request 노드로 Ghost Admin API를 호출했습니다.
처음엔 필터를 이렇게 걸었습니다.
Plaintext
filter: email_count:>=5 + open_rate:0
🚨 Error: BadRequestError: Request not understood error.
삽질 포인트:
Ghost 대시보드에는 Open rate가 표시되지만, 실제 DB 필드명은 open_rate가 아니었습니다. API 문서를 뒤져보니 오픈율은 계산된 값이고, 필터링을 위해서는 "오픈한 이메일 개수"를 써야 했습니다.
✅ Solution:
Plaintext
filter: email_count:>=5 + email_opened_count:0 + label:-inactive-warning
(Tip: label:-inactive-warning을 추가해서 이미 경고 라벨이 붙은 사람은 중복으로 가져오지 않게 처리했습니다.)
---
Step 2. 경고 라벨 붙이기 (JSON 인코딩의 늪)
대상자를 찾았으니 inactive-warning 라벨을 붙여야 합니다. Ghost API는 PUT 메서드를 사용하는데, 여기서 데이터를 보내는 포맷이 아주 까다롭습니다.
처음엔 n8n의 기본 매핑 기능을 썼더니, 라벨 데이터가 [object Object]라는 텍스트로 변환되어 에러가 났습니다.
🚨 Error: JSON parameter needs to be valid JSON
삽질 포인트:
기존 라벨(Array)을 유지하면서 새 라벨을 추가해야 하는데, n8n이 이걸 단순 문자열로 처리해버린 겁니다. 자바스크립트 객체(Expression)로 인식시켜야 했습니다.
✅ Solution:
JavaScript
{{
{
"members": [
{
"labels": ($json.labels || [])
.map(l \=> ({ name: l.name })) // 기존 라벨의 잡다한 정보 다 버리고 '이름'만 남김
.concat([{ "name": "inactive-warning" }]) // 새 라벨 추가
}
]
}
}}
(Tip: 기존 라벨 정보에 created_at 같은 메타데이터가 포함된 채로 다시 보내면 500 에러가 뜹니다. 깔끔하게 name만 추출해서 보내는 것이 핵심입니다.)
---
Step 3. 이별 통보 메일 보내기 (with Mailgun)
이제 "너 지워진다?"라는 메일을 보냅니다.
여기서 우리는 "프로세스 컨설팅 회사"답게, 이 자동화 로직을 투명하게 공개하는 메일 본문을 작성했습니다.
"이 메일은 제가 쓴 게 아닙니다. n8n 봇이 보내고 있죠." 로 시작하여, 실제 IF-THEN 로직을 보여주는 교육적인 콘텐츠로 구성했습니다.
삽질 포인트 (Mailgun 404):
메일이 안 가고 404 Not Found가 떴습니다. 알고 보니 Mailgun에 등록한 도메인은 mg.retn.kr인데, n8n 설정에는 retn.kr이라고 적었더군요. 서브 도메인까지 정확히 일치시켜야 합니다.
삽질 포인트 (재앙의 반복 발송):
테스트한다고 To Email에 제 개인 이메일을 적어놨었습니다.
그랬더니 대상자가 22명이면, 제 메일함에 22통의 이별 통보가 꽂히더군요. (n8n은 리스트의 아이템 개수만큼 반복 실행합니다.)
실전에서는 반드시 {{ $json.email }}로 동적 바인딩해야 합니다.
---
Step 4. 운명의 48시간 (Wait & Execute)
n8n의 Wait 노드를 사용해 정확히 48시간을 대기시킵니다.
그리고 48시간 뒤, 봇이 깨어나면 가장 중요한 일을 해야 합니다.
Ghost에게 다시 물어보기
48시간 전의 데이터는 낡았습니다. 그 사이에 유저가 메일을 열었을 수도 있으니까요. 반드시 GET /members/{id}로 최신 상태를 다시 조회(Re-fetch)해야 합니다.
✅ Logic:
JavaScript
IF ( email_opened_count > 0 ) {
// 살려준다 (라벨만 제거)
PUT /members/{id} (labels: inactive-warning 제거)
} ELSE {
// 처형한다
DELETE /members/{id}
}
---
3. 결과 및 베스트 프랙티스
이제 이 워크플로우는 매주 월요일 자동으로 돌아갑니다.
- 데이터 위생: 반응 없는 DB가 자동으로 청소됩니다.
- 비용 절감: Mailgun 발송 비용과 Ghost 멤버 티어 비용이 줄어듭니다.
- 구독자 경험: 무의미한 스팸 대신, 깔끔한 이별(혹은 재결합) 경험을 제공합니다.
💡 Self-Host 유저를 위한 3줄 요약
- 필터는 정확하게: open_rate 말고 email_opened_count를 쓰세요.
- 데이터는 가볍게: API로 데이터를 업데이트할 땐, name 같이 필수 필드만 남기고 나머지는 쳐내세요(Sanitize).
- 테스트는 신중하게: Loop가 도는 노드에서 To Email을 하드코딩하면 메일 폭탄을 맞습니다.
P.S.
이 글을 보고 계신 분들 중, 혹시 최근 제 뉴스레터를 안 열어보신 분이 계신가요?
조만간 n8n 봇이 보낸 "이별 예고장"을 받게 되실지도 모릅니다. 😉
(Powered by Ghost, Mailgun, and n8n)