LogoSEO Jing
  • All Posts
  • SEO Jing
  • okayJing
  • KD Team
  • CLab CoreTeam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

vinextCloudflare WorkersGitHub ActionsCI/CDDevOps

vinext + GitHub Actions로 Cloudflare Workers 배포하기

2026년 3월 16일·12분 읽기

vinext란?

vinext는 Cloudflare가 만든 Vite 기반 Next.js 대체 프레임워크다. Next.js의 API 표면(App Router, Pages Router, next/* 모듈)을 Vite 위에서 재구현했고, 빌드 속도 4.4배, 번들 크기 57% 감소를 자랑한다. 무엇보다 vinext deploy 한 줄이면 Cloudflare Workers에 배포할 수 있다는 점이 매력적이다.

하지만 실제로 GitHub Actions CI/CD를 구성하면서 생각보다 많은 이슈를 만났다. 이 글은 그 삽질 과정과 최종 해결책을 정리한 기록이다.

사전 준비

1. Cloudflare API Token 발급

Cloudflare 대시보드에서 API 토큰을 발급받아야 한다.
  1. Cloudflare 대시보드 접속
  2. 좌측 메뉴 → My Profile → API Tokens
  3. "Create Token" → "Edit Cloudflare Workers" 템플릿 선택
  4. 토큰 생성 후 복사

2. Cloudflare Account ID 확인

Cloudflare 대시보드 → Workers & Pages → 우측 사이드바에서 Account ID를 복사한다.

3. GitHub Secrets 등록

GitHub 레포지토리 → Settings → Secrets and variables → Actions에서 두 개의 secret을 등록한다.

  • CLOUDFLARE_API_TOKEN: 위에서 발급받은 API 토큰
  • CLOUDFLARE_ACCOUNT_ID: 위에서 확인한 Account ID

GitHub Actions 워크플로우

최종적으로 완성된 .github/workflows/ci.yml의 deploy job이다. lint → test → build를 거친 후, main 브랜치 push일 때만 배포가 실행된다.

yaml
deploy:
  name: Deploy
  runs-on: ubuntu-latest
  needs: [build]
  if: github.event_name == 'push' && github.ref == 'refs/heads/main'
  steps:
    - uses: actions/checkout@v4

    - uses: pnpm/action-setup@v4

    - uses: actions/setup-node@v4
      with:
        node-version: 22
        cache: "pnpm"

    - run: pnpm install --frozen-lockfile
    - run: pnpm build

    - name: Deploy to Cloudflare Workers
      run pnpm filter web exec wrangler deploy config dist/server/wrangler.json

핵심 포인트는 빌드와 배포를 분리하는 것이다. vinext build로 빌드하면 dist/server/wrangler.json이 생성되고, 이 설정 파일을 wrangler deploy --config에 넘겨서 배포한다.

트러블슈팅 이슈 노트

여기서부터가 본론이다. 이 워크플로우에 도달하기까지 겪은 에러들을 시간순으로 정리했다.

이슈 1: .dev.vars는 로컬 전용이다

처음에 Cloudflare API 토큰을 apps/web/.dev.vars에 넣었다.

CLOUDFLARE_API_TOKEN=my-api-token-here

그런데 배포 시 인식을 못했다. .dev.vars는 wrangler의 로컬 개발용 환경변수 파일이다. 배포할 때는 셸 환경변수나 CI의 secrets로 CLOUDFLARE_API_TOKEN을 주입해야 한다.

그리고 .dev.vars에는 API 토큰이 들어있으므로 반드시 .gitignore에 추가해야 한다.

gitignore
# cloudflare
.wrangler/
.dev.vars

이슈 2: cloudflare/wrangler-action의 workspace 프로토콜 에러

처음에는 공식 cloudflare/wrangler-action@v3을 사용했다.
yaml
- name: Deploy to Cloudflare Workers
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    workingDirectory: apps/web
    command: deploy --config dist/server/wrangler.json
그런데 이런 에러가 발생했다.
npm error code EUNSUPPORTEDPROTOCOL
npm error Unsupported URL Type "workspace:": workspace:*

wrangler-action이 내부적으로 npm i wrangler를 실행하는데, pnpm의 workspace:* 프로토콜을 npm이 이해하지 못해서 발생한 문제다.

해결: action을 쓰지 않고, 이미 pnpm install로 설치된 wrangler를 직접 실행한다.

yaml
- name: Deploy to Cloudflare Workers
  run: pnpm --filter web exec wrangler deploy --config dist/server/wrangler.json
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

이슈 3: vite, @cloudflare/vite-plugin, wrangler 모듈 미설치

vinext deploy를 로컬에서 실행하면 vite.config.ts, worker/index.ts, wrangler.jsonc를 자동 생성한다. 이 파일들을 커밋해서 CI에서 빌드하면 이런 에러가 발생했다.

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'vite'
Could not resolve '@cloudflare/vite-plugin' in vite.config.ts
Command "wrangler" not found

vinext가 내부적으로 vite를 번들하고 있어서 로컬에서는 동작하지만, 자동 생성된 vite.config.ts가 vite와 @cloudflare/vite-plugin을 직접 import하기 때문에 이 패키지들이 devDependencies에 명시적으로 있어야 한다.

해결: 세 패키지를 모두 devDependency로 추가한다.

bash
pnpm --filter web add -D vite @cloudflare/vite-plugin wrangler

이슈 4: import.meta.dirname이 Workers에서 undefined

이게 가장 까다로운 이슈였다. 배포는 되는데 Worker가 시작할 때 크래시가 났다.

Uncaught TypeError: The "paths[0]" argument must be of type string. Received undefined
  at null.<anonymous> (node-internal:validators:116:15) in validateString
  at null.<anonymous> (node-internal:internal_path:942:13) in resolve
  at null.<anonymous> (index.js:26607:24)

원인은 shared/config/index.ts에서 import.meta.dirname을 사용한 것이다.

ts
// 변경 전 - Workers에서 import.meta.dirname은 undefined
export const CONTENT_DIR = path.resolve(
  import.meta.dirname,
  "../../../content",
);

import.meta.dirname은 Node.js 21+에서 지원하는 기능인데, Cloudflare Workers 런타임에서는 undefined를 반환한다. path.resolve에 undefined가 들어가면서 TypeError가 발생한 것이다.

해결: nullish coalescing으로 fallback을 추가한다.

ts
// 변경 후
export const CONTENT_DIR = path.resolve(
  import.meta.dirname ?? "",
  "../../../content",
);

이슈 5: wrangler.jsonc prettier 포맷 에러

vinext deploy가 자동 생성한 wrangler.jsonc가 prettier 포맷과 맞지 않아서 CI의 format:check에서 실패했다.

[warn] apps/web/wrangler.jsonc
[warn] Code style issues found in the above file.
해결: 커밋 전에 prettier --write를 실행한다.
bash
pnpm exec prettier --write apps/web/wrangler.jsonc

vinext deploy가 생성하는 파일들

vinext deploy를 처음 실행하면 아래 파일들이 자동 생성된다. 어떤 것을 커밋하고 어떤 것을 무시해야 하는지 정리했다.

파일커밋 여부설명
vite.config.tsOVite + Cloudflare 플러그인 설정
worker/index.tsOWorker 엔트리포인트
wrangler.jsoncOWrangler 설정 (prettier 포맷 후 커밋)

마무리

정리하면, vinext + GitHub Actions 배포의 핵심은 이렇다.
  1. vinext deploy 대신 빌드와 배포를 분리한다 (vinext build → wrangler deploy)
  2. cloudflare/wrangler-action은 pnpm workspace와 충돌하니 직접 wrangler를 실행한다
  3. vite, @cloudflare/vite-plugin, wrangler를 devDependencies에 명시한다
  4. Workers 런타임의 Node.js 호환성 차이를 주의한다 (import.meta.dirname 등)
  5. .dev.vars와 .wrangler/는 반드시 gitignore한다

vinext는 아직 초기 단계라 CI/CD 관련 공식 문서가 부족한 편이다. 이 글이 같은 삽질을 하는 누군가에게 도움이 되길 바란다.

포스트 목록

/SEOJing
파일 11개, 폴더 1개
Cloudflare Workers에서 fs 모듈이 안 되는 이유와 해결법모바일 웹에서 가로 모드를 강제하는 5가지 방법 — iOS Safari에서도 동작하는 코드 뷰어 만들기블로그 글을 PPT로 만들기 — DOM 클로닝 기반 프레젠테이션 모드100vh가 100%가 아닌 이유 — 모바일 뷰포트 단위 완전 정리Context로 퀴즈 컴포넌트를 만들다 막혀서 React.Children을 공부하게 된 이야기localStorage 읽기에서 하이드레이션 에러가 터지는 이유 useSyncExternalStore로 해결useEffect cleanup과 의존성 배열 — 실전 버그 사례로 이해하는 생애주기vinext + GitHub Actions로 Cloudflare Workers 배포하기vinext 오픈소스 기여기: 한국어 slug가 RSC에서 이슈를 일으킨 이유RSC 환경에서 WebAssembly가 차단되는 이유 — Shiki에서 rehype-prism-plus로vinext는 왜 빠를까? — SSR, Vite, Edge, 그리고 Web Vitals까지
:
-
-
-
-
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
.wrangler/
X
Wrangler 캐시 디렉토리
.dev.varsX로컬 환경변수 (API 토큰 포함)
dist/X빌드 산출물 (이미 gitignore)