#dev 수정: 2026-11-09

텔레그램으로 블로그에 글 올리기 2: Worker와 GitHub 연결

Cloudflare Worker가 텔레그램 원고를 검증해 GitHub 저장소에 커밋하는 구성을 살펴봅니다.

텔레그램 메시지를 블로그 글로 저장하려면 메시지를 그대로 커밋하기 전에 인증, 원고 검증, 중복 확인이 필요합니다. Cloudflare Worker를 중간 처리자로 두면 이 단계를 한곳에서 수행할 수 있습니다.

시리즈 목차

  1. 봇과 웹훅 준비
  2. Worker와 GitHub 연결
  3. Worker에 웹훅 등록
  4. 원고 전송과 발행 확인

전체 코드는 어디에 있나

이 글은 DevBJ 저장소의 전체 Worker 소스를 기준으로 설명합니다. 저장소 접근 권한이 있어야 코드를 볼 수 있습니다. 아래 예시는 일부 동작을 보여 주는 코드 조각이며, 이것만 복사해서는 발행 기능이 동작하지 않습니다. 전체 기능을 사용하려면 저장소의 cloudflare/worker.js, package.json, package-lock.json과 테스트 파일이 필요합니다.

이 Worker에는 블로그 발행 외에 별도 사이트의 링크 제보 기능도 들어 있습니다. 다른 저장소에서 재사용할 때는 저장소 이름, 브랜치, 게시글 경로, 카테고리 매핑과 링크 제보 기능을 자신의 환경에 맞게 검토해야 합니다. 기존 운영 Worker를 연습용 코드로 덮어쓰지 말고 별도 Worker와 테스트 저장소에서 먼저 확인하세요.

Worker가 하는 일

이 구성의 Worker는 텔레그램 웹훅 요청을 받은 뒤 다음 순서로 처리합니다.

  1. 웹훅 비밀값과 허용된 채팅 ID를 확인합니다.
  2. 메시지 텍스트 또는 .md 첨부파일을 읽습니다.
  3. YAML front matter로 시작하는 원고의 필수 항목과 slug를 검사합니다.
  4. 카테고리에 맞는 경로를 정하고 기존 파일과 slug를 확인합니다.
  5. GitHub 저장소의 main에 파일을 커밋하고 텔레그램으로 결과를 알립니다.

일반 텍스트나 front matter가 없는 파일은 게시글이 아니라 별도 메모 폴더에 저장합니다. 공개 저장소라면 메모도 외부에서 볼 수 있으니 민감한 내용은 보내지 않아야 합니다.

요청 인증

Cloudflare 대시보드의 Worker 설정에서 봇 토큰, GitHub 토큰, 웹훅 비밀값을 Secret으로 등록합니다. Worker 코드는 fetch 핸들러의 env에서 값을 읽습니다. 실제 구현에서 웹훅 비밀값을 검사하는 부분은 다음과 같습니다.

const secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token");
if (!env.TELEGRAM_WEBHOOK_SECRET) {
  return reply(503, { ok: false, error: "웹훅 설정 오류" });
}
if (!tokenMatches(secret, env.TELEGRAM_WEBHOOK_SECRET)) {
  return reply(403, { ok: false, error: "웹훅 인증 실패" });
}

reply와 tokenMatches는 전체 Worker에 정의된 함수입니다. 이 검사 후에도 메시지의 채팅 ID가 CHAT_ID와 일치하는지 확인합니다. 그룹 ID를 허용하면 그룹 구성원의 메시지가 처리될 수 있으므로 개인 채팅을 사용하는 편이 안전합니다.

광고

GitHub에 저장하기

Worker는 GitHub API로 현재 main의 파일 목록을 확인한 뒤 새 파일과 커밋을 만듭니다. 같은 경로의 다른 내용을 덮어쓰지 않고, 게시글 slug가 다른 글에서 사용 중인지도 검사합니다. 저장 중 브랜치가 바뀌면 최신 트리에서 다시 확인합니다. 다음은 중복 검사를 마친 뒤 Git 객체를 생성하는 부분입니다. github, encodeBase64, base, commit, parent는 전체 소스에 정의되어 있습니다.

const blob = await github(env, `${base}/blobs`, {
  method: "POST",
  body: JSON.stringify({ content: encodeBase64(content), encoding: "base64" }),
});
const newTree = await github(env, `${base}/trees`, {
  method: "POST",
  body: JSON.stringify({
    base_tree: commit.tree.sha,
    tree: [{ path, mode: "100644", type: "blob", sha: blob.sha }],
  }),
});
const newCommit = await github(env, `${base}/commits`, {
  method: "POST",
  body: JSON.stringify({
    message: `docs: add ${path} via Telegram`,
    tree: newTree.sha,
    parents: [parent],
  }),
});
await github(env, `${base}/refs/heads/${BLOG_BRANCH}`, {
  method: "PATCH",
  body: JSON.stringify({ sha: newCommit.sha, force: false }),
});

blob은 파일 내용, tree는 파일 목록, commit은 변경 이력을 나타냅니다. 마지막 요청은 새 커밋으로 브랜치를 이동합니다. 실제 코드에는 위 조각 외에도 기존 파일과 충돌을 검사하고 동시 변경 시 재시도하는 처리가 있습니다.

GitHub 토큰에는 게시글 저장소의 Contents 쓰기 권한이 필요합니다. Worker에서 저장을 마쳐도 사이트 빌드와 배포의 성공까지 보장되지는 않습니다. 자세한 원고 형식과 확인 순서는 텔레그램 원고 발행에서 살펴봅니다.

Worker 번들 만들기

이 프로젝트의 Worker는 YAML 파서를 패키지로 불러옵니다. Cloudflare 대시보드에 원본 파일만 붙여 넣으면 의존성이 빠지므로 저장소 루트의 PowerShell에서 번들을 생성합니다.

Set-Location cloudflare
npm.cmd ci
npm.cmd test
npm.cmd run build

생성된 cloudflare/dist/worker.js 전체를 Cloudflare Worker 코드 편집기에 붙여 넣어 배포합니다. 이 파일에는 YAML 라이브러리가 함께 포함되어 원본보다 길어집니다. 설정할 Secret과 웹훅 등록은 다음 글에서 이어집니다.

이 구현은 main에 직접 커밋합니다. 따라서 브랜치 보호 규칙이 직접 쓰기를 허용하지 않으면 저장이 실패할 수 있습니다. 테스트 저장소에서 권한과 배포 흐름을 먼저 확인하세요.

광고