Hosting My Own Newsletter

Matthias Endler

내 뉴스레터 직접 호스팅하기

원문은 Matthias Endler님이 에 게재했습니다. 이 블로그 구독하기

이 블로그에서 몇 년간 뉴스레터를 운영했지만, 오랫동안 메일을 한 통도 보내지 않았다. 마침내 다시 뉴스레터를 일으켜 세운 이야기와 그 과정에서 배운 것들을 공유하려 한다.

먼저 간단히 짚고 넘어가자면, 이 표현 때문에 혼란이 좀 있었다. “직접 호스팅한다”는 말은 뉴스레터 플랫폼을 쓰지 않는다는 뜻이다. 구독을 처리하는 백엔드와 발송에 쓰는 CLI는 내가 직접 만들었고, 뉴스레터 본문 자체는 git 저장소에 있는 마크다운 파일일 뿐이다. 발송 백엔드로는 여전히 Plunk을 쓴다(그래서 SES, 반송 처리, 수신 거부 리스트, 구독 해지 페이지는 내가 신경 쓸 필요가 없다). Plunk 오픈소스라 직접 호스팅할 수도 있지만, 전송 안정성과 관련된 예외 케이스가 워낙 많아서 그냥 돈을 내고 다른 사람이 운영해주게 하는 편이 마음 편하다. 🙃

Tinyletter 시절

예전 Tinyletter 랜딩 페이지, 이제는 처량한 404가 되었다.
예전 Tinyletter 랜딩 페이지, 이제는 처량한 404다.
출처: Wayback Machine

수년간 내 구성은 웹사이트에 있는 작은 폼 하나가 작가 중심의 소규모 뉴스레터 서비스인 Tinyletter로 연결되는 형태였다. 좋았던 점은 단순함이었다. 이메일 도달률이나 반송률, 수신 거부 리스트, SPF, DKIM, DMARC 같은 건 전혀 신경 쓸 필요가 없었다. 글을 쓰고 보내기만 하면 사람들이 받아보는 식이었다.

Tinyletter 작성 페이지, 인터페이스가 얼마나 단순했는지 보여준다.
Tinyletter 작성 페이지, 인터페이스가 얼마나 단순했는지 보여준다.

그냥 잘 됐다. 그러다 Tinyletter가 서비스를 종료했다.

잠깐 역사를 짚자면, Tinyletter는 2010년 Philip Kaplan이 만들었는데, 2010년 10월 31일 일요일 단 하루 만에 코딩했다고 전해진다.

1년 뒤 Mailchimp에 인수됐고, 조용히 퍼널이나 세그먼트, A/B 테스트 같은 걸 신경 쓰지 않고 개인 뉴스레터를 쓰고 싶은 작가들의 사실상 본거지가 되었다.

그러다 2023년 말, Mailchimp(현 Intuit 산하)가 서비스 종료를 발표했다. 공식 입장은 “비즈니스 우선순위가 변화했다”와 “마케터를 지원하고 중소기업의 성장을 돕는 도구 구축에 집중하고 있다”는 것이었다. 작가들은 애초에 그들의 핵심 고객이 아니었다.

2023년 말 Mailchimp의 서비스 종료 공지.
2023년 말 Mailchimp의 서비스 종료 공지.
출처: EmailOctopus

Tinyletter가 2024년 2월 29일 서비스를 종료하기 직전, 구독자 목록을 마지막으로 백업해 뒀지만 그걸로 뭘 할지는 계획이 없었다.

부정

이 시점에서 나는 타사 서비스를 쓰는 생각 자체가 싫어졌다. 똑같은 일이 또 반복될 수 있으니까.

그래도 모든 옵션을 살펴봤지만, 전부 마음에 들지 않았다:

  • 너무 비싸다! 대부분 서비스가 구독자 수 기준으로 과금하고, 사람들에게 편지를 쓰는 게 아니라 비즈니스 퍼널을 돌린다고 가정한다.
  • 너무 마케팅 중심이다! 템플릿, 드래그 앤 드롭 빌더, A/B 테스트, 참여도 점수, 추적 픽셀. 용어 자체가 완전히 맞지 않는다. 나는 캠페인을 돌리고 싶은 게 아니라 이메일을 보내고 싶을 뿐이다!
  • 해커 친화적이지 않다. 마크다운도, CLI도, 실제로 쓰고 싶은 API도 없다. 모든 게 마케팅 팀을 위해 만든 웹 대시보드에서 이루어진다.
  • 오픈소스가 아니다. 다음 Tinyletter가 문을 닫더라도 마이그레이션 없이 계속 이어가고 싶다.
  • 기본적으로 추적한다! 오픈 추적, 클릭 추적, 모든 푸터에 픽셀이 박혀 있다. 누가 뭘 열어봤는지 알고 싶지 않다. 나는 글을 쓰고, 당신은 읽거나(혹은 안 읽거나), 그게 끝이길 원한다.

Fly.io로 이전하기

사람들이 뉴스레터 언제 돌아오냐고 계속 물어서 fly.io 위에 급조한 무언가를 올려뒀다. 작은 Rust API와, 구독자가 담긴 CSV 파일 하나, 그리고 웹사이트를 통해 구독하는 방법이었다. 발송은 나중에 처리하더라도 일단 구독이라도 받을 수 있게 하자는 생각이었다.

그리고 나서 리스트는 그냥 방치됐다.

알고 보니 오랫동안 방치된 리스트 자체가 문제였다. 오랜만에 소식을 보내면 메일 제공업체가 의심하고 스팸으로 분류될 수 있다. 갑자기 내 뉴스레터가 나를 공격하는 셈이 되는 것이다.

발송 서비스 찾기

이게 단연 가장 어려운 부분이었다. Resend, Postmark, SendGrid, Mailgun, Amazon SES 등 수많은 서비스를 알아봤다. 모두 소규모 뉴스레터에는 꽤 비싸거나, API가 형편없거나, GDPR을 준수하지 않거나, 너무 복잡했다.

포기하려던 차에 Plunk을 찾았다. 오픈소스이고, 가격은 리스트 규모에 따라 달라지며, API도 거슬리지 않는다. 내가 신경 쓰고 싶지 않은 도달률 관련 작업(SES 연동, 반송 처리, 수신 거부 리스트, 호스팅되는 구독 해지 페이지)을 대신 해준다. 지금은 유료 고객으로 쓰고 있다. 제휴 관계는 없고, 그냥 진심으로 만족하는 사용자일 뿐이다.

심지어 작은 기여도 하나 보냈는데 10분 만에 머지해줬다. 덕분에 정말 커뮤니티의 일원이 된 것 같은 기분이 들었다.

첫 번째 진짜 뉴스레터는 오랫동안 소식을 듣지 못했던 천여 명 이상의 구독자에게 발송됐다. 반송이 쏟아질까 걱정했는데 잘 됐다. 반송률은 1% 안팎이었고, 구독 해지는 극소수였으며, 도달률 문제도 없었다. 와!

특별한 꼼수는 쓰지 않았다. 나눠 보내기도, 천천히 워밍업하기도, 기발한 제목을 쓰는 것도 없었다. 한 번에 전부 보내고, 명백히 죽은 주소는 Plunk(정확히는 그 아래의 SES)이 반송 처리를 통해 자동으로 정리하도록 맡겼다. 한 가지 한 일이라면 첫 호 서두에 짧고 솔직한 재인사를 넣은 것 정도다. “예전에 제 블로그 글 하나를 읽고 구독해주셨죠, 오랫동안 조용해서 죄송합니다”라는 식이었는데, 구독 해지를 낮게 유지하는 데는 그게 가장 큰 역할을 했다고 생각한다.

비용으로 보면 전체 리스트에 한 번 발송하는 데 대략 $1가 든다. 불규칙하게 보내는 뉴스레터에는 사실상 아무것도 아닌 금액이다.

Plunk 대시보드, 캠페인 개요와 도달률 리포트를 보여준다. 보다시피 나는 누가 이메일을 열었는지 추적하지 않는다.
Plunk 대시보드, 캠페인 개요와 도달률 리포트를 보여준다. 보다시피 나는 누가 이메일을 열었는지 추적하지 않는다.
출처: Plunk

이제야 집에 온 것 같다!

폴더 안에 평범한 마크다운 파일로 뉴스레터를 쓰고, 버전 관리를 하며, 나머지는 작은 CLI로 처리할 수 있다는 걸 깨달았다. 그게 바로 내가 편한 자리다. 나와 뜨거운 코코아 한 잔, 에디터, 터미널, 그리고 git만 있으면 된다. 더 이상 글쓰기와 나 사이에 웹 대시보드가 끼어 있지 않다.

전체는 하나의 저장소에 들어 있다:

newsletter/
├── issues/         # one .md per edition (1.md, 2.md, ...)
├── send/           # the CLI I run locally
└── subscribe/      # tiny HTTP service behind the website signup form

CLI 이름은 send다. 할 수 있는 일은 다음과 같다:

$ send help

Usage: send <COMMAND>

Commands:
  new      Create a new issue file and open $EDITOR
  list     List local issues
  lint     Check links in an issue (or all issues)
  test     Send a test email to myself  
  publish  Publish the issue to all subscribed contacts
  status   Show contact-list and deliverability report
  prune    Delete unsubscribed contacts

send publish 2를 실행하면 실제로 발송하기 전에 미리보기와 수신자 수, 그리고 y/N 확인 프롬프트가 뜬다. 제목은 corrode v0.N.0 # <topic> 형식으로 자동 생성된다. 시맨틱 버저닝 스타일로, 메이저 버전은 영원히 0에 머무르는데, 절대 1.0에 도달하지 못하는 프로젝트에 대한 작은 농담이다.

send status는 캠페인별 도달률을 보여주는데, 반송률 셀은 SES 임계값에 따라 색상으로 구분되고, 일별 반송과 구독 해지도 함께 나와서 문제를 일찍 감지할 수 있다.

send lint는 발행 전에 호 안의 모든 링크를 lychee로 검사한다. 나는 lychee 메인테이너라서 여기서 직접 써보는 건 당연한 선택이었고, 링크 검사가 전혀 없던 예전 Tinyletter 웹 에디터에 비하면 삶의 질이 꽤 좋아졌다.

웹사이트의 구독 폼은 내 서버에서 돌아가는 작은 subscribe 서비스로 POST 요청을 보낸다. 이메일 유효성을 검사하고, 허니팟 필드가 채워진 요청은 버린 뒤, subscribe-requested 이벤트로 Plunk에 POST한다. Plunk은 연락처를 unsubscribed 상태로 생성하고 Action 워크플로우를 통해 트랜잭셔널 확인 이메일을 발송한다. 수신자가 링크를 클릭해야만 Plunk이 상태를 subscribed1로 바꾼다. 내 쪽으로 오는 웹훅도, 콜백도, 페이지에 자바스크립트도 없다. 나는 그냥 git에 푸시하면 서버가 변경을 감지해 서버 크레이트를 빌드하고 실행하고, 새 버전이 바로 라이브가 된다. 실행 중인 서비스는 CPU나 메모리를 거의 전혀 쓰지 않는다.

DNS, 잠깐만

Plunk이 내 도메인을 대신해 메일을 보내려면 DNS에 세 가지가 필요하다. SPF 레코드(SES가 해당 도메인에서 발송할 수 있음을 명시), DKIM 키(SES가 발송 메일에 서명할 수 있도록), 그리고 리턴 패스 MX 레코드(반송 메일이 Plunk이 읽을 수 있는 곳으로 돌아오도록)다. 세 개 모두 서브도메인 아래에 위치한다. 걱정할 필요는 없다. Plunk이 설정 방법을 정확히 알려주고 DNS 제공업체 대시보드에 그대로 복사해 붙여 넣으면 된다.

한 가지 꼭 기억할 점은, Plunk의 선택 사항인 인바운드 MX를 도메인 apex에 추가하지 말라는 것이다. 그러면 현재 인박스를 처리하는 곳(내 경우 mailbox.org)에서 메일을 가로채게 되고, 답장이 엉뚱한 곳으로 가게 된다.

사소한 해프닝

답장이 제대로 오려면 From: 주소가 실제 메일박스여야 한다는 걸 깜빡했다. 첫 호는 [email protected]로 발송됐는데, 그 주소는 메일박스로 존재하지 않았다. 한 친절한 독자(안녕 Kevin!)가 인사를 하려고 답장을 보냈는데 메시지가 반송됐고, 그 반송 알림을 다시 내게 전달해줘서 알게 됐다. mailbox.org에 별칭을 만들었고, 그 뒤로는 답장이 정상적으로 내 인박스에 도착하고 있다.

두 개가 아니라, 하나의 리스트

겸사겸사 예전 endler.dev 뉴스레터와 corrode.dev 뉴스레터도 하나의 리스트로 합쳤다. 둘 다 항상 내가 썼고, 두 개를 병렬로 운영하는 건 별로 의미가 없었다. 키보드 앞에 앉은 사람은 같은데, 독자층은 대부분 겹치고, 관리는 두 배로 드니까.

병합 자체는 별일 없이 끝났다. Tinyletter에서 내보낸 CSV(원래 endler.dev 리스트)와 fly.io 서비스에서 가져온 CSV(corrode.dev를 시작하면서 모으기 시작한 리스트)가 있었는데, 형식은 동일했다. 둘 다 Plunk에 넣었고 중복 제거도 문제가 되지 않았다. 첫 호에서 ‘모든 글을 위한 하나의 뉴스레터’라는 점을 명확히 밝혀, 이제 무엇에 구독된 건지 아무도 헷갈리지 않게 했다. 2

앞으로는 뉴스레터가 하나뿐이다. 이 중 마음에 들지 않는 부분이 있다면 언제든 구독을 해지하면 되고, 다시는 내 메일을 받지 않게 될 것이다. 서운해하지 않는다.

전하고 싶은 말

직접 해볼까 고민하고 있다면, 하라고 말하고 싶다. 셀프 호스팅은 예전보다 훨씬 쉬워졌다. 이제 거의 모든 부분에 훌륭한 오픈소스 서비스가 있다. 일반적으로, 작은 것들을 직접 만들어보는 것은 그것을 제대로 이해하고 중요한 부분을 계속 내 것으로 유지하는 가장 좋은 방법 중 하나다. 그 얘기만으로도 글 하나가 나오니, 써주길 원하면 알려 달라.

조금 엉성한 저장소를 한번 보고 싶다면 메일을 보내 달라. 링크를 보내주겠다. 사실 별로 볼 건 없지만 작동 방식이 궁금하다면 기꺼이 공유하겠다. 아니면 내가 조금 정리해서 제대로 오픈소스로 공개할 때까지 기다려도 된다. 그러려면 또 몇 년은 걸릴 것 같다.

그리고 가장 좋은 점은, 아래 폼을 작성해 뉴스레터를 구독함으로써 내 설정을 직접 테스트해볼 수 있다는 것이다!

  1. GDPR을 준수하려면 확인 클릭이 필요하며, Plunk이 그 부분을 처리해준다.

  2. 백업에 관해서는, Plunk이 구독자 리스트의 원본을 보유하고 있고 나는 언제든 CSV로 내보낼 수 있다. API도 있는 걸로 아는데 아직 써보진 않았다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글