내 뉴스레터를 직접 호스팅하기
이 블로그에서 수년간 뉴스레터를 운영해 왔지만, 오랫동안 메일을 한 통도 보내지 않았습니다. 이 글은 뉴스레터를 어떻게 다시 살려냈는지, 그리고 그 과정에서 무엇을 배웠는지에 대한 이야기입니다.
미리 간단히 짚고 넘어가겠습니다. 이 부분 때문에 혼란이 있었거든요. 제가 말하는 “직접 호스팅한다”는 뉴스레터 플랫폼을 쓰지 않는다는 뜻입니다. 구독을 처리하는 백엔드와 발송에 쓰는 CLI는 제가 직접 만들었고, 뉴스레터 본문 자체는 git 저장소에 있는 마크다운 파일일 뿐입니다. 발송 백엔드로는 여전히 Plunk을 사용합니다(그래서 SES, 반송 처리, 수신 거부 리스트, 구독 취소 페이지 같은 건 제가 신경 쓸 필요가 없습니다). Plunk은 자체가 오픈소스라 직접 호스팅할 수도 있지만, 발송 도달률 쪽에는 신경 쓸 예외 케이스가 너무 많아서 차라리 비용을 내고 다른 사람이 운영해주길 바랍니다. 🙃
Tinyletter 시절

출처: Wayback Machine
오랫동안 제 설정은 웹사이트에 있는 작은 폼 하나가 Tinyletter로 연결되는 형태였습니다. Tinyletter는 작가에 초점을 맞춘 작은 뉴스레터 서비스였죠. 제가 좋아했던 건 단순함이었습니다. 이메일 도달률이나 반송률, 수신 거부 리스트, SPF, DKIM, DMARC 같은 건 전혀 신경 쓸 필요가 없었습니다. 글을 쓰고 보내기만 하면 사람들이 받아봤습니다.

그냥 잘 됐습니다. 그러다 Tinyletter가 서비스를 종료했습니다.
잠깐 역사를 짚어보자면, Tinyletter는 2010년 Philip Kaplan이 만들었고, 2010년 10월 31일 일요일 하루 만에 코딩했다고 알려져 있습니다.
1년 뒤 Mailchimp에 인수되었고, 퍼널이나 세그먼트, A/B 테스트 같은 걸 고민하지 않고 개인 뉴스레터를 쓰고 싶은 작가들의 사실상 본거지가 되었습니다.
그리고 2023년 말, Mailchimp(현 Intuit 산하)는 서비스 종료를 발표했습니다. 공식 입장은 “비즈니스 우선순위가 변화했고” “마케터를 지원하고 중소기업의 성장을 돕는 도구를 만드는 데 집중하고 있다”는 것이었습니다. 작가는 애초에 그들의 핵심 고객이 아니었습니다.

출처: 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
이제야 집에 온 것 같다!
폴더 안에 일반 마크다운 파일로 호를 작성하고 버전 관리를 하며, 나머지는 작은 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 formCLI 이름은 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 contactssend 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이 상태를 subscribed로 바꿉니다1. 제 쪽으로 오는 웹훅도, 콜백도, 페이지에 자바스크립트도 없습니다. 저는 그냥 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
앞으로는 뉴스레터가 하나뿐입니다. 이 중 마음에 들지 않는 부분이 있다면 언제든 구독을 취소하시면 다시는 메일을 받지 않으실 겁니다. 서운해하지 않겠습니다.
드리고 싶은 말
직접 해볼까 고민 중이셨다면, 해보시길 권합니다. 셀프 호스팅은 예전보다 확실히 쉬워졌습니다. 이제 거의 모든 부분에 훌륭한 오픈소스 서비스가 있습니다. 일반적으로 작은 것들을 직접 만들어 보는 것은 제대로 이해하고 중요한 부분을 계속 소유하는 가장 좋은 방법 중 하나입니다. 그 자체로 글 하나가 될 주제이니, 써보길 원하시면 알려 주세요.
(다소 허술한) 저장소를 한번 보고 싶으시다면 메일을 보내 주세요. 링크를 보내드리겠습니다. 사실 별로 흥미로운 건 아니지만, 어떻게 동작하는지 궁금하시다면 기꺼이 공유하겠습니다. 아니면 제가 조금 정리해서 제대로 오픈소스로 공개할 때까지 기다리셔도 됩니다. 그러려면 또 몇 년 걸릴 것 같네요.
그리고 가장 좋은 점은 아래 폼을 작성해 뉴스레터를 구독하시면 제 설정을 바로 테스트해 보실 수 있다는 겁니다!
글을 무작위로 읽기