나는 200줄의 코드로 나만의 Mailchimp 대안을 만들었다

발행: (2025년 12월 31일 오후 10:03 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

(번역할 텍스트가 제공되지 않았습니다. 번역을 원하시는 본문을 알려주시면 한국어로 번역해 드리겠습니다.)

아키텍처 개요

시스템은 세 부분으로 구성됩니다:

  1. Config file – 이메일 시퀀스를 정의합니다.
  2. Cron job – 다음 이메일을 받을 준비가 된 구독자를 찾습니다.
  3. Queue worker – 이메일을 전송합니다.

Config file

{
  "workflowSteps": [
    {
      "step": 1,
      "hoursAfterSignup": 24,
      "template": {
        "subject": "Welcome! 🎉",
        "heading": "Hi {{name}}!",
        "body": "Thanks for signing up..."
      }
    },
    { "step": 2, "hoursAfterSignup": 96, ... },
    { "step": 3, "hoursAfterSignup": 264, ... }
  ]
}

Cron job – 준비된 구독자 찾기

app.job('*/15 * * * *', async (req, res) => {
  const conn = await Datastore.open();

  for (const stepConfig of workflowSteps) {
    const cutoffTime = new Date(Date.now() - stepConfig.hoursAfterSignup * 60 * 60 * 1000);

    // Stream subscribers (constant memory usage)
    await conn.getMany('subscribers', {
      subscribed: true,
      createdAt: { $lte: cutoffTime }
    }).forEach(async (subscriber) => {
      if (!subscriber.emailsSent?.includes(stepConfig.step)) {
        // Atomically mark as sent
        const updated = await conn.updateOne(
          'subscribers',
          { _id: subscriber._id, emailsSent: { $nin: [stepConfig.step] } },
          { $push: { emailsSent: stepConfig.step } }
        );

        if (updated) {
          await conn.enqueue('send-email', { subscriberId: subscriber._id, step: stepConfig.step });
        }
      }
    });
  }
});

Queue worker – 이메일 전송

app.worker('send-email', async (req, res) => {
  const { subscriberId, step } = req.body.payload;
  const template = await getTemplate(step);
  await sendEmail(subscriber.email, template.subject, generateHTML(template));
  res.end();
});

메모리 효율적인 처리

Instead of loading all subscribers into memory:

// ❌ Memory issues with 50k+ subscribers
const subscribers = await conn.getMany('subscribers').toArray();

we stream them:

// ✅ Constant memory usage
await conn.getMany('subscribers').forEach(async (sub) => {
  await processSubscriber(sub);
});

This scales to 100 k+ subscribers without breaking a sweat.

중복 이메일 방지

원자적 업데이트는 특정 구독자/단계에 대해 하나의 크론 인스턴스만 전송을 큐에 넣을 수 있도록 보장합니다:

const updated = await conn.updateOne(
  'subscribers',
  { _id: subscriber._id, emailsSent: { $nin: [step] } },
  { $push: { emailsSent: step } }
);

if (updated) {
  await conn.enqueue('send-email', { subscriberId, step });
}

전송에 실패하면 워커가 플래그를 롤백하여 다음 크론 실행 시 재시도합니다:

catch (error) {
  await conn.updateOne(
    'subscribers',
    { _id: subscriberId },
    { $pull: { emailsSent: step } }
  );
  // Next cron run will retry
}

배포 및 구성

npm install -g codehooks
coho create my-drip --template drip-email-workflow
coho deploy

이메일 제공자를 설정하세요 (예: SendGrid):

coho set-env EMAIL_PROVIDER "sendgrid"
coho set-env SENDGRID_API_KEY "SG.your-key"
coho set-env FROM_EMAIL "hello@yourdomain.com"

이제 드립 캠페인이 실행 중입니다.

Codehooks가 제공하는 것

  • Serverless functions
  • NoSQL database (MongoDB‑compatible)
  • Cron scheduler
  • Queue workers
  • Environment secrets

하나의 플랫폼에 모두 포함되어 있어 AWS Lambda, SQS, CloudWatch, EventBridge 등을 따로 연결할 필요가 없습니다.

비용 비교

구독자 수월 비용 (Codehooks + SendGrid)연간 비용Mailchimp (Standard)
5 000$19 + $19.95 = $38.95$467$1 020
50 000$19 + $89.95 = $108.95$1 307$3 600‑4 800

리스트 규모가 커질수록 절감액이 증가합니다.

템플릿 기능

  • 완전한 작동 시스템
  • 구독자 관리용 REST API
  • 반응형 HTML 이메일 템플릿
  • 드라이‑런 모드가 있는 이메일 감사 로깅
  • 통합: SendGrid, Mailgun, Postmark
  • 예시 구성 (온보딩, 코스, 육성)

사용하면 되는 경우

  • 하위 분 단위 전달 SLA가 필요합니다 (전용 트랜잭션 서비스 사용)
  • 고급 세분화 UI
  • 코드 없는 워크플로우 빌더
  • 비기술적인 팀원이 캠페인을 관리하는 경우

이 솔루션은 제어와 소유권을 원하는 개발자를 위한 것입니다.

빠른 시작 다시

coho create my-campaign --template drip-email-workflow
cd my-campaign
coho deploy

제가 직접 만든 SaaS용 템플릿이며, 다른 사람들에게도 유용할 것이라 생각했습니다. 행복한 사용자일 뿐, 별다른 관계는 없습니다.

질문이 있나요? 아래에 남겨 주세요!

Back to Blog

관련 글

더 보기 »