웹훅

수신 코드 레시피

실제 운영에서 자주 쓰는 패턴을 복붙 가능한 형태로 정리했습니다. 서명 검증은 모두 포함되어 있습니다.

레시피 1 — 자사 카카오 알림톡 발송

상태 변경 시 그 아이디어 관련 사용자에게 카카오 알림톡으로 알림 (실무에서 가장 흔한 패턴).

Express + 카카오 알림톡
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import express from "express";

const app = express();

app.post(
  "/lenit-webhook",
  express.json(),
  async (req, res) => {
    const event = req.body;

    // 1) 상태 변경 / 공지 발행 이벤트만 처리
    const isAudienceEvent =
      event.event === "idea.status_changed" ||
      event.event === "announcement.published";
    if (!isAudienceEvent || !event.data.audience) {
      return res.status(200).send("skip");
    }

    // 2) audience 합치고 dedupe
    const a = event.data.audience;
    const recipients = [
      a.author,
      ...(a.commenters || []),
      ...(a.voters     || []),
      ...(a.reactors   || []),
    ].filter(Boolean);

    const seen = new Set();
    const unique = recipients.filter(u => {
      if (seen.has(u.externalId)) return false;
      seen.add(u.externalId);
      return true;
    });

    // 3) 자사 DB 에서 phone 조회 → 카카오 알림톡 발송
    for (const user of unique) {
      const profile = await db.users.findByExternalId(user.externalId);
      if (!profile?.phone) continue;

      await kakaoAlimtalk.send({
        to: profile.phone,
        templateCode: "IDEA_STATUS_CHANGED",
        variables: {
          name: profile.name,
          ideaTitle: event.data.title,
          newStatus: event.data.newStatus,
        },
      });
    }

    res.status(200).send("ok");
  }
);

app.listen(3000);

레시피 2 — traits 기반 채널 분기

plan: "pro" 인 사용자만 카카오 알림톡, 나머지는 이메일.

js
1
2
3
4
5
6
7
8
9
for (const user of unique) {
  const channel = user.traits?.plan === "pro" ? "kakao" : "email";

  if (channel === "kakao") {
    await sendKakaoAlimtalk(user.externalId, event.data);
  } else {
    await sendEmail(user.externalId, event.data);
  }
}

레시피 3 — 헤비유저에게만 푸시

js
1
2
3
4
5
6
7
8
9
10
const heavyUsers = unique.filter(
  u => u.traits?.userType === "heavy"
);

for (const user of heavyUsers) {
  await sendPush(user.externalId, {
    title: event.eventLabel,                     // "아이디어 상태 변경"
    body: `${event.data.title} → ${event.data.newStatus}`,
  });
}

레시피 4 — Discord로 알림 (Slack/JANDI는 자동 변환)

Discord는 자동 변환 미지원이라 직접 변환 코드를 끼워야 합니다.

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async function forwardToDiscord(event) {
  const data = event.data;
  const audienceCount =
    (data.audience?.author ? 1 : 0) +
    (data.audience?.commenters?.length || 0) +
    (data.audience?.voters?.length || 0) +
    (data.audience?.reactors?.length || 0);

  await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      embeds: [{
        title: event.eventLabel,
        description: `**${data.title}**\n${data.oldStatus} → ${data.newStatus}`,
        color: 0x7C6DED,
        footer: { text: `알림 대상: ${audienceCount}명` },
        timestamp: data.updatedAt,
      }],
    }),
  });
}

레시피 5 — n8n / Zapier 연동

n8n과 Zapier는 별도 코드 없이 GUI로 처리 가능합니다.

  1. n8n에서 Webhook 트리거 노드 생성 → URL 복사
  2. Lenit 설정 → 웹훅 에 그 URL 등록
  3. n8n 시각 빌더에서 Notion 페이지 생성, Slack 메시지 발송, Google Sheets 추가 등 자유롭게 연결
페이로드 구조 미리보기
n8n/Zapier 트리거 노드는 첫 요청을 받아 페이로드 스키마를 자동 인식합니다. Lenit 어드민에서 테스트 트리거(아이디어 상태 1회 변경)를 발생시켜 스키마를 등록한 뒤 후속 노드를 연결하세요.

레시피 6 — 재시도 큐 (운영 권장)

Lenit은 fire-and-forget이라 자사 서버가 잠깐 다운되면 그 이벤트는 영영 못 받습니다. 큐를 끼워서 재시도를 자체 운영하는 패턴.

BullMQ + Redis 기반 재시도 큐
js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Queue, Worker } from "bullmq";
const queue = new Queue("lenit-webhooks", { connection: redis });

app.post("/lenit-webhook", express.json(), async (req, res) => {
  // 일단 큐에 적재만 하고 즉시 200 반환 (Lenit 측 timeout 방지)
  await queue.add("process", {
    event: req.body,
    receivedAt: Date.now(),
  }, {
    attempts: 5,
    backoff: { type: "exponential", delay: 1000 },
  });

  res.status(200).send("queued");
});

// 별도 worker 프로세스
new Worker("lenit-webhooks", async (job) => {
  await processEvent(job.data.event);  // 실제 알림 발송
});

디버깅 팁

증상점검
알림이 안 옴설정 → 웹훅에서 URL·이벤트 체크 여부 확인. webhook.site에 임시 등록해서 페이로드 도착 확인
audience가 비어있음이벤트가 idea.status_changed/announcement.published인지, audience targets 체크 여부 확인. 비식별 방문자(핑거프린트)만 있는 경우는 비어있음
401 검증 실패express.json() 같은 자동 파싱이 raw body를 망가뜨리는지 확인. 반드시 express.raw() 사용
서명이 같은데도 실패비교 시 prefix sha256= 포함했는지 확인. trim/공백 점검

다음 단계

Audience 구조MCP 연동