# 내 뉴스레터 폼의 가입을 MailerLite로 보내기

> 내 가입 폼을 MailerLite에 연결합니다. 릴레이가 새 독자를 더블 옵트인을 위해 미확인으로 추가하고, 구독자에게 두 번 확인을 요청하지 않습니다.

2026-10-25 게시 · https://ikiform.com/ko/guides/newsletter-signup/mailerlite

내 폼을 Ikiform으로 POST하고, MailerLite API를 호출하는 릴레이로 웹훅을 보내세요. 릴레이는 먼저 주소를 조회합니다. 새 독자는 `unconfirmed`로 만들어지며, API 설정이 켜져 있으면 MailerLite가 더블 옵트인 이메일을 보냅니다. 기존 구독자는 새 그룹에만 추가됩니다. 이 조회 덕분에 확인된 구독자가 다시 확인을 요청받지 않습니다.

## MailerLite에서 다른 점

MailerLite의 구독자 생성 엔드포인트는 upsert입니다. `/api/subscribers`로 `POST` 한 번이면 새 구독자를 만들거나 기존 구독자를 업데이트하고, `201`이나 `200`으로 응답합니다. 비파괴적이라서 빼먹은 그룹과 필드는 그대로 남습니다. 문제는 `status`입니다. 계정 설정 **Double opt-in for API and integrations**(API 및 연동용 더블 옵트인)가 켜져 있으면 MailerLite는 새 API 구독자에게 확인 이메일을 보냅니다. 기존의 활성 또는 수신 거부 구독자는 호출이 `status: "unconfirmed"`를 보낼 때만 확인 이메일을 받습니다.

MailerLite 도움말에 따르면 기존의 활성 또는 수신 거부 구독자는 호출에 `"status": "unconfirmed"`가 없으면 확인 이메일을 받지 않습니다. 그래서 항상 `unconfirmed`를 보내는 릴레이는 충성 독자의 반복 가입을 모두 새 확인 요청으로 바꿉니다. 해결책은 무엇을 보낼지 정하기 전에 주소를 조회하는 것입니다.

다이어그램: 릴레이가 MailerLite에 무엇을 보낼지 정하는 방식

1. 릴레이가 Ikiform의 웹훅을 검증합니다.
2. MailerLite에 이메일로 구독자를 요청합니다.
3. 없음: 상태 unconfirmed와 위치의 그룹으로 만들고, MailerLite가 확인 이메일을 보냅니다.
4. 있고 active나 unconfirmed: 상태 없이 그룹만 추가하므로 확인 상태는 바뀌지 않습니다.
5. 있고 unsubscribed: 아무것도 하지 않으므로 가입이 수신 거부를 몰래 되돌리지 않습니다.

가입마다 API 호출 두 번이며, 두 번째 호출은 첫 번째가 찾은 결과에 따라 달라집니다.

## MailerLite 설정하기

1. MailerLite에서 구독 설정을 열고 **Double opt-in for API and integrations**를 켜세요. 확인 이메일은 거기서 편집합니다. MailerLite는 API로 추가된 모든 구독자에게 같은 이메일을 보냅니다.
2. "Website footer"와 "Blog posts"처럼 위치마다 그룹을 만들고 각 그룹의 ID를 기록하세요.
3. **Integrations**에서 API 토큰을 만들고 릴레이의 환경 변수용으로 보관하세요.

> MailerLite 요금 FAQ에 따르면 가장 낮은 플랜에서는 API 접근이 제한됩니다. 의존하기 전에 내 플랜에서 토큰으로 구독자를 만들 수 있는지 테스트하세요.

## 릴레이

이것은 Express 서버입니다. 서명은 Ikiform이 보낸 정확한 바이트를 대상으로 하고 JSON을 먼저 파싱하면 바이트가 바뀌므로, 원시 본문을 텍스트로 읽습니다.

`server.ts`:

```ts
import express from "express";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

const API = "https://connect.mailerlite.com/api";
const GROUPS: Record<string, string> = {
  footer: process.env.MAILERLITE_GROUP_FOOTER!,
  post: process.env.MAILERLITE_GROUP_POST!,
};

function mailerlite(path: string, init: RequestInit = {}) {
  return fetch(`${API}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.MAILERLITE_API_KEY}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
  });
}

async function subscribe(email: string, name: string | undefined, source: string) {
  const found = await mailerlite(`/subscribers/${encodeURIComponent(email)}`);
  if (found.status !== 200 && found.status !== 404) return found;

  const groups = [GROUPS[source] ?? process.env.MAILERLITE_GROUP_FOOTER!];
  const fields = name ? { name } : {};

  if (found.status === 404) {
    return mailerlite("/subscribers", {
      method: "POST",
      body: JSON.stringify({ email, fields, groups, status: "unconfirmed" }),
    });
  }

  const existing = await found.json();
  if (existing.data.status === "unsubscribed") return new Response(null, { status: 208 });
  return mailerlite("/subscribers", { method: "POST", body: JSON.stringify({ email, fields, groups }) });
}

const app = express();

app.post("/hooks/mailerlite", express.text({ type: "application/json", limit: "512kb" }), async (req, res) => {
  let event;
  try {
    event = await verifyWebhook({ payload: req.body, headers: req.headers, secret: process.env.IKIFORM_WEBHOOK_SECRET! });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return res.status(400).send(error.reason);
    return res.status(500).end();
  }
  if (event.type !== "submission.created") return res.status(204).end();

  const { email, first_name, consent, source } = event.data.fields;
  if (consent !== true || typeof email !== "string") return res.status(200).send("Skipped");

  const answer = await subscribe(
    email,
    typeof first_name === "string" ? first_name : undefined,
    typeof source === "string" ? source : "footer",
  );
  if (answer.status === 208) return res.status(200).send("Unsubscribed earlier, left alone");
  if (answer.status === 429) {
    return res.status(503).set("retry-after", answer.headers.get("retry-after") ?? "60").send("MailerLite rate limit");
  }
  if (!answer.ok) return res.status(502).send(`MailerLite answered ${answer.status}: ${await answer.text()}`);
  res.status(204).end();
});

app.listen(Number(process.env.PORT ?? 3000));
```

- MailerLite는 토큰을 `Authorization: Bearer`로 받습니다. 기본 URL은 `https://connect.mailerlite.com/api`입니다.
- `GET /api/subscribers/{email}`은 이메일이나 ID를 받고, 구독자가 없으면 `404`로 응답합니다.
- `groups`는 이미 존재하는 그룹 ID를 받습니다. 틀린 ID는 `422`로 거부되며, 릴레이는 이를 MailerLite의 메시지와 함께 `502`로 전달합니다.
- `fields.name`은 MailerLite의 기본 이름 필드입니다. 커스텀 필드는 보내기 전에 MailerLite에 있어야 합니다.
- `208`은 수신 거부한 독자를 위한 내부 표시입니다. Ikiform에는 `200`과 전달 로그의 메모로 보입니다.

443 포트의 공개 HTTPS URL을 주는 곳이라면 어디든 배포하고, Ikiform의 **Webhooks**에 그 URL을 추가하고, `whsec_` 시크릿을 `IKIFORM_WEBHOOK_SECRET`에 넣으세요. 재시도와 페이로드는 [웹훅 문서](https://ikiform.com/ko/docs/webhooks.md)에서 다루고, [자체 서버](https://ikiform.com/blog/form-submissions-to-your-own-server.md)에는 데이터베이스를 쓰는 같은 Express 구성이 있습니다.

## 동의 기록이 있는 곳

MailerLite의 생성 호출은 `subscribed_at`, `ip_address`, `opted_in_at`, `optin_ip`를 받습니다. IP 필드는 빼세요. Ikiform의 웹훅에는 방문자의 IP 주소나 그 해시가 절대 포함되지 않으므로 릴레이가 보낼 실제 값이 없고, 지어낸 값은 없는 것보다 나쁩니다.

가입 기록은 Ikiform에 남습니다. 제출에 타임스탬프, origin, referrer, 국가와 함께 `consent: true`가 보관됩니다. 확인 기록은 독자가 링크를 클릭한 시점을 기록하는 MailerLite에 있습니다. 둘을 합치면 누군가 언제 체크박스를 선택했고 언제 주소가 자기 것임을 증명했는지 보여 줄 수 있습니다. 필요할 때 **Submissions**에서 Ikiform 쪽을 CSV로 내보내세요.

## 레이트 리밋

MailerLite의 전역 API 한도는 분당 120개 요청입니다. 릴레이는 가입마다 두 번 호출하므로 분당 약 60건이 들어갑니다. 그 이상이면 MailerLite가 `429`로, 릴레이가 `Retry-After`와 함께 `503`으로 응답하고, Ikiform이 나중에 같은 전달로 다시 시도합니다.

| MailerLite 응답 | 릴레이 응답 | Ikiform |
| --- | --- | --- |
| `201` 또는 `200` | `204` | 전달 성공 |
| `429` | `Retry-After`와 함께 `503` | 기다린 뒤 재시도 |
| `401` | `502` | 재시도. 토큰을 고친 뒤 실패한 전달에서 **Retry now**를 쓰세요 |
| `422` | 오류와 함께 `502` | 재시도. 대개 그룹 ID나 필드 이름이 틀린 경우입니다 |

## 주의할 점

- **`status: "active"`를 보내지 마세요.** 그 사람을 구독 상태로 표시하며, 그래도 확인 이메일이 나가는지는 MailerLite 문서에 나와 있지 않습니다. 보내는 상태로 테스트하세요.
- **재구독은 선택입니다.** MailerLite에는 수신 거부한 사람을 위한 `resubscribe` 플래그가 있습니다. 폼이 그렇다고 명시하고 독자가 다시 동의한다는 것을 알 때만 보내세요.
- **토글이 중요합니다.** MailerLite는 **Double opt-in for API and integrations**가 켜져 있을 때만 API 가입에 확인 이메일을 보냅니다.
- **확인 이메일은 하나.** API 더블 옵트인 이메일은 모든 API 가입이 공유합니다. 위치마다 문구가 달라야 한다면 대신 별도의 MailerLite 폼을 쓰세요.
- **Ikiform의 중복.** Ikiform은 받아들인 모든 POST를 보관하고, MailerLite는 이메일당 구독자 하나를 보관합니다. 구독자 수는 Ikiform 대시보드가 아니라 MailerLite에서 세세요.

## 작동 확인하기

1. Ikiform의 엔드포인트에서 테스트를 보내세요. 전달에 `204`가 표시됩니다.
2. 내 새 주소로 구독하세요. MailerLite에 푸터 그룹의 미확인 구독자로 표시되고 확인 이메일이 도착합니다.
3. 확인한 뒤 블로그 글에서 다시 구독하세요. 구독자는 활성으로 유지되고, 두 번째 그룹에 들어가며, 새 확인 이메일은 받지 않습니다.
4. MailerLite에서 그 주소를 수신 거부하고 한 번 더 구독하세요. 전달 로그에 수신 거부 메모가 표시되고, MailerLite에서는 여전히 수신 거부 상태입니다.

## 질문

### MailerLite 임베드 대신 내 HTML 폼을 쓸 수 있나요?

네. 폼을 Ikiform으로 POST하고, 릴레이가 MailerLite API로 각 가입을 알맞은 그룹에 추가하게 하세요. 폼은 내 마크업으로 남고 뉴스레터는 여전히 MailerLite가 보냅니다.

### MailerLite가 API 가입에 확인 이메일을 보내지 않는 이유는 무엇인가요?

MailerLite 구독 설정에서 Double opt-in for API and integrations가 꺼져 있거나, 릴레이가 구독자를 active로 만들었습니다. 설정을 켜 두고, status unconfirmed를 보내고, 내 주소로 테스트하세요.

### 두 번 가입한 구독자는 확인 이메일을 두 통 받나요?

이 릴레이에서는 아닙니다. 먼저 주소를 조회하고 새 구독자일 때만 unconfirmed를 보냅니다. 기존 구독자는 새 그룹에 추가될 뿐 다른 것은 바뀌지 않습니다.

### 푸터와 블로그 가입을 서로 다른 MailerLite 그룹에 넣으려면 어떻게 하나요?

위치마다 폼의 hidden source 입력에 다른 값을 주고, 릴레이에서 각 값을 MailerLite 그룹 ID에 매핑하세요. 두 번째 위치에서 가입한 기존 구독자는 그 그룹에도 들어가고 첫 그룹도 유지합니다.

### Ikiform이 구독자에게 무언가를 보내나요?

아니요. Ikiform은 폼을 제출한 사람에게 절대 이메일을 보내지 않습니다. 확인 이메일, 모든 뉴스레터, 수신 거부 링크는 MailerLite가 보냅니다.

## 가이드 더 보기

- [뉴스레터 가입 폼](https://ikiform.com/ko/guides/newsletter-signup.md): 직접 작성한 마크업으로 뉴스레터 가입 폼을 만들고, 동의를 기록하고, 봇 가입을 막고, 각 구독자를 Kit, MailerLite, beehiiv 등 어떤 도구로든 넘깁니다.
- [더블 옵트인](https://ikiform.com/ko/guides/newsletter-signup/double-opt-in.md): Ikiform은 확인 이메일을 보내지 않으므로 더블 옵트인은 이메일 도구에서 실행됩니다. 릴레이가 각 가입을 미확인으로 추가하고 도구가 링크를 보냅니다.
- [Kit (구 ConvertKit)](https://ikiform.com/ko/guides/newsletter-signup/kit.md): 구독자를 만들고, 인센티브 이메일을 위해 Kit 폼에 추가하고, 가입 위치를 태그하는 릴레이로 내 뉴스레터 폼을 Kit에 연결합니다.
- [beehiiv](https://ikiform.com/ko/guides/newsletter-signup/beehiiv.md): 내 가입 폼을 beehiiv에 연결합니다. 릴레이가 더블 옵트인을 켜고, 위치를 UTM source로, 페이지를 referrer로 해서 각 구독을 만듭니다.
- [정적 사이트](https://ikiform.com/ko/guides/newsletter-signup/static-site.md): Hugo, Jekyll, Eleventy, Astro 정적 사이트에 뉴스레터 가입 폼을 추가하고, 이메일 도구의 API 키는 페이지에서 빼고, 확인은 도구에서 합니다.
- [Next.js 뉴스레터 폼](https://ikiform.com/ko/guides/newsletter-signup/nextjs.md): Next.js 푸터 뉴스레터 폼입니다. 동의 확인과 Turnstile이 있는 서버 액션이 Ikiform으로 POST하고, 라우트 핸들러가 독자를 Kit에 추가합니다.
