디자이너가 Slack에서 버튼 한 번 누르면 feature 브랜치가 main에 머지되고 npm 배포까지 이어지는 플로우를 Cloudflare Worker와 GitHub Actions로 구현한 과정, 만났던 에러와 해결 방법을 정리했습니다.
우리 팀은 Shoplflow라는 React 컴포넌트 라이브러리(디자인 시스템)를 모노레포로 개발하고 있습니다. 기존에는 아래와 같은 플로우로 진행되고 있었죠.
디자인 가이드 나옴
새 브랜치 생성
컴포넌트 작업
PR 생성
Vercel preview URL 나올 때까지 대기
Preview URL을 Slack 채널에 공유
디자인 QA 완료
main merge
npm 자동 배포 후 version packages PR이 자동으로 열림
해당 브랜치까지 merge한 뒤에야 npm 배포 완료
단계가 많을 뿐 아니라, preview URL 대기 → Slack 공유 → QA 완료 후 개발자가 직접 main 머지하고, version packages PR까지 머지해야 배포가 끝나는 구조였습니다. 디자이너는 "QA 완료"라고만 하면 되는데, 실제로는 개발자가 여러 단계를 수동으로 진행해야 했죠.
이 프로세스를 줄이기 위해 "디자인 검수 완료" 한 번으로 배포까지 갈 수 있게 만들고 싶었습니다. Chromatic(또는 preview) URL을 Slack에 보낼 때 "디자인 검수 완료" 버튼을 함께 넣고, 디자이너가 그 버튼만 누르면 해당 feature 브랜치가 main에 머지되고, changesets 워크플로가 버전 업·version packages PR 처리·npm 배포까지 이어지게 하는 것이 목표였습니다. PR 생성·preview 대기·수동 머지 단계를 줄이는 게 핵심이었죠.
이 배경에서 "Slack 버튼 클릭 → Worker → GitHub repository_dispatch → main 머지 → 배포" 자동화를 설계하고 구현하게 됐습니다.
Chromatic으로 스토리북 빌드 후 Slack에 URL + "디자인 검수 완료" 버튼 전송
디자이너가 버튼 클릭 → 해당 feature 브랜치가 main에 머지 → changesets 기반 npm 배포 자동 실행
PR 없이 "디자인 검수 OK" 시점에 바로 배포 파이프라인을 돌리고 싶었음
[Slack 메시지 + 버튼]
↓ 클릭
[Cloudflare Worker] ← Slack Interactivity Request URL
↓ 서명 검증, payload 파싱
↓ GitHub API: repository_dispatch (event_type: design-approved)
[GitHub Actions: design-approved-deploy.yml]
↓ feature 브랜치 → main 머지 & push
[GitHub Actions: changesets] ← main 푸시 감지
↓ version + npm publish
[배포 완료]Worker: Slack에서 오는 POST 처리, 서명 검증, 브랜치명 추출, GitHub repository_dispatch 호출
design-approved-deploy: repository_dispatch 수신 시 해당 브랜치를 main에 머지하고 푸시
changesets: main 푸시에 반응해 버전 올리고 npm 배포
이 플로우에서 Slack 버튼 클릭을 받아서 GitHub API를 호출하는 역할을 Cloudflare Worker가 맡습니다. Worker와 배포 도구인 Wrangler가 뭔지 간단히 정리합니다.
Cloudflare Workers는 Cloudflare가 제공하는 서버리스 실행 환경입니다. 24시간 켜 두고 요청을 기다리는 "서버"를 따로 둘 필요 없이, HTTP 요청이 들어왔을 때만 지정한 코드가 실행됩니다. 요청이 오면 코드가 돌고, 응답을 보내면 실행이 끝나는 식이라 트래픽이 없을 때 비용이 거의 들지 않고, 스케일링도 Cloudflare가 처리해 줍니다.
이번에는 "Slack이 버튼 클릭 시 보내는 POST 요청"을 받을 엔드포인트가 필요했기 때문에, 서버를 직접 호스팅하기보다 Worker 하나만 배포해 두고, 그 Worker가 Slack 서명 검증 → payload 파싱 → GitHub API 호출 → Slack 응답까지 처리하도록 했습니다. 무료 플랜에서도 하루 10만 요청까지 사용 가능해서, 버튼 클릭 정도의 트래픽에는 충분했습니다.
Wrangler는 Cloudflare Workers를 위한 공식 CLI입니다. 로컬에서 Worker 코드를 개발·실행하고, Cloudflare에 배포하고, 시크릿(환경 변수)을 설정하는 작업을 모두 터미널에서 할 수 있게 해 줍니다.
wrangler dev — Worker를 로컬에서 띄워서 테스트
wrangler deploy — 작성한 Worker를 Cloudflare에 배포해, 지정한 URL(예: xxx.workers.dev)에서 동작하게 함
wrangler secret put <이름> — 비밀키(GH_PAT, SLACK_SIGNING_SECRET 등)를 Worker에 안전하게 등록
정리하면, Worker = "이 URL로 들어오는 요청을 처리하는 코드", Wrangler = 그 코드를 배포하고 설정하는 도구라고 보면 됩니다.
Slack App 생성 → Bot Token Scopes에 chat:write 추가
Interactivity & Shortcuts 켜고 Request URL을 이후 배포할 Worker URL로 설정
메시지는 Block Kit으로 전송: 스토리북 링크 + action_id: design_approved, value: 브랜치명 인 버튼
workers/slack-design-approved/index.js 에서:
Slack 요청 서명 검증 (Signing Secret, HMAC SHA-256)
payload 파싱 → actions[0].action_id === 'design_approved', action.value 로 브랜치 추출
GitHub API POST /repos/{owner}/{repo}/dispatches 호출 (event_type: design-approved, client_payload: { branch })
Slack에는 response_url 로 결과 메시지 전송 (원본 메시지/버튼은 유지, replace_original: false)
.github/workflows/design-approved-deploy.yml: on.repository_dispatch.types: [design-approved]
client_payload.branch 로 체크아웃·머지·푸시 후, Slack 웹훅으로 성공/실패 알림
pnpm send-design-review <Chromatic URL>: 현재 git 브랜치와 버튼이 포함된 메시지를 Slack에 전송
.env.local 의 SLACK_BOT_TOKEN, SLACK_CHANNEL_ID 사용
원인: Slack Interactivity는 3초 안에 200 + 본문을 기대하는데, 본문에 메시지 갱신 정보가 없거나 형식이 맞지 않으면 화면에 아무 변화가 없음. replace_original: true 없이 텍스트만 보내면 "메시지 갱신"이 되지 않음.
해결: 응답 본문에 replace_original: true, text: "..." 를 넣어 원본 메시지를 확인 문구로 교체. 더 안정적으로 하려면 response_url 로 같은 내용을 POST. 이후에는 "버튼 사라지지 않게" 하려고 replace_original: false 로 바꾸고, 결과만 새 메시지로 보내는 방식으로 정리함.
원인: Worker 안에서 예외 미처리 (예: GitHub API 실패, 시크릿 누락). Slack은 3초 안에 200을 못 받으면 500처럼 처리하고 재시도함.
해결: triggerGitHubDispatch 를 try/catch 로 감싸기. 실패해도 항상 200 을 돌려주고, 본문/response_url 에는 "배포 요청 실패" 같은 메시지 전달. 실제 에러는 console.error 로만 남겨서 Cloudflare 로그에서 확인.
원인: GitHub REST API는 User-Agent 헤더 필수. Worker에서 fetch 할 때 User-Agent를 안 보내서 403.
해결: GitHub API 요청 헤더에 'User-Agent': 'Shoplflow-Design-Approved-Worker/1.0' 추가.
원인: Classic PAT에 repo scope 없음. 또는 조직(Organization) 저장소인데 SSO Authorize 를 안 함.
해결: Classic PAT 생성 시 repo (필요 시 workflow) 체크. 조직이면 토큰 목록에서 해당 토큰 Configure SSO → 해당 org Authorize. Worker 시크릿 GH_PAT 에 앞뒤 공백 없이 다시 설정.
원인: Worker 시크릿의 GH_OWNER / GH_REPO 가 실제 저장소와 다름 (오타, 대소문자, 공백).
해결: GH_OWNER = shopl, GH_REPO = shoplflow 처럼 정확한 값만 넣기 (소문자, 공백 없음). Worker에 console.log('dispatch target repo:', env.GH_OWNER, env.GH_REPO) 넣어서 Cloudflare 로그로 실제 사용 값 확인.
원인: PAT가 만료됐거나 Revoke 됐는데 Worker에는 예전 토큰이 그대로 들어 있음. 또는 채팅/터미널에 노출된 토큰을 Revoke 한 뒤 새 토큰을 Worker에 안 넣은 경우.
해결: 새 Classic PAT 생성 (repo, 필요 시 SSO Authorize). wrangler secret put GH_PAT 로 새 토큰만 깔끔하게 다시 설정. 로컬에서 curl 로 같은 토큰·같은 URL로 repository_dispatch 호출해 204 나오는지 확인 후 Worker와 비교.
원인: repository_dispatch 는 기본 브랜치(main) 에 있는 워크플로만 실행함. design-approved-deploy.yml 이 feature 브랜치에만 있고 main에는 푸시된 적 없음.
해결: 해당 브랜치를 main에 머지한 뒤 main 을 push. 그러면 main에 워크플로가 생기고, 버튼 클릭 시 design-approved-deploy 가 실행됨. "해당 브랜치만 push" 가 아니라 main에 반영이 필수임을 문서/온보딩에 명시.
원인: Slack 응답에 replace_original: true 로 원본 메시지 전체를 결과 텍스트로 교체해서 버튼까지 사라짐.
해결: replace_original: false 로 변경. 결과는 새 메시지로만 전송 (response_url 에 같은 payload 로 POST). 원본 메시지와 버튼은 그대로 유지.
Slack Interactivity는 3초 내 200 + 적절한 본문/response_url 이 중요하고, GitHub API는 User-Agent 와 PAT 권한·SSO 를 꼭 맞춰줘야 하며, repository_dispatch 는 main에 있는 워크플로만 실행한다는 점을 알면 디버깅이 훨씬 수월했습니다. 이 플로우를 한 번 세팅해 두면, 디자이너는 Slack에서 버튼 한 번으로 배포까지 트리거할 수 있어서 협업이 편해졌습니다.