이 글은 누구를 위한 것인가
- PR마다 자동으로 미리보기 URL을 생성하고 싶은 팀
- GitHub Actions 캐시로 빌드 시간을 줄이고 싶은 개발자
- 타입체크, 테스트, 배포를 자동화해서 수동 배포를 없애고 싶은 팀
들어가며
"배포하는 거 잊었다"는 더 이상 변명이 안 된다. GitHub Actions로 PR 생성 시 자동 미리보기, main 머지 시 자동 프로덕션 배포, 실패 시 Slack 알림까지 완전히 자동화할 수 있다.
이 글은 bluefoxdev.kr의 GitHub Actions CI/CD 가이드 를 참고하여 작성했습니다.
1. CI/CD 파이프라인 설계
[워크플로우 트리거]
PR 생성/업데이트:
1. 타입체크 (tsc --noEmit)
2. 린트 (ESLint)
3. 단위 테스트 (Vitest)
4. 빌드 확인
5. 미리보기 배포 (Vercel Preview)
6. PR 댓글에 Preview URL 게시
main 머지:
1. 위 모든 체크
2. 프로덕션 배포
3. Slack 배포 알림
[캐시 전략]
npm/pnpm: package-lock.json 해시로 캐시
Next.js 빌드: .next/cache 캐시
효과: 첫 실행 5분 → 이후 1-2분
[보안]
VERCEL_TOKEN: GitHub Secrets
환경변수: Environment별 분리
OIDC: AWS 자격증명 대신 사용 (비밀키 없음)
2. GitHub Actions 워크플로우
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # 이전 실행 취소
jobs:
# 타입체크, 린트, 테스트 병렬 실행
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm' # pnpm 캐시 자동 관리
- name: 의존성 설치
run: pnpm install --frozen-lockfile
- name: 타입 체크
run: pnpm type-check
- name: 린트
run: pnpm lint
- name: 테스트
run: pnpm test --coverage
- name: 커버리지 업로드
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
# 빌드 (캐시 활용)
build:
runs-on: ubuntu-latest
needs: quality
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: 의존성 설치
run: pnpm install --frozen-lockfile
# Next.js 빌드 캐시
- name: Next.js 빌드 캐시 복원
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
- name: 빌드
run: pnpm build
env:
NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
# .github/workflows/preview.yml
name: PR 미리보기 배포
on:
pull_request:
branches: [main]
jobs:
deploy-preview:
runs-on: ubuntu-latest
permissions:
pull-requests: write # PR 댓글 작성 권한
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Vercel 미리보기 배포
id: deploy
run: |
npm i -g vercel@latest
DEPLOY_URL=$(vercel --token ${{ secrets.VERCEL_TOKEN }} \
--env NEXT_PUBLIC_API_URL=${{ vars.PREVIEW_API_URL }} \
2>&1 | grep "https://" | tail -1)
echo "url=$DEPLOY_URL" >> $GITHUB_OUTPUT
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- name: PR 댓글에 미리보기 URL 게시
uses: actions/github-script@v7
with:
script: |
const previewUrl = '${{ steps.deploy.outputs.url }}';
const comment = `## 🚀 미리보기 배포 완료
| 항목 | 값 |
|------|-----|
| 🔗 URL | [미리보기 열기](${previewUrl}) |
| 📦 커밋 | ${{ github.sha }} |
| ⏰ 배포 시각 | ${new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' })} |
`;
// 기존 댓글 업데이트 또는 새 댓글 작성
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existingComment = comments.find(c => c.body?.includes('미리보기 배포 완료'));
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body: comment,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment,
});
}
# .github/workflows/deploy-prod.yml
name: 프로덕션 배포
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # 승인 필요 환경
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: 프로덕션 배포
run: |
npm i -g vercel@latest
vercel --prod --token ${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- name: Slack 배포 알림
if: always()
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "${{ job.status == 'success' && '✅' || '❌' }} 프로덕션 배포 ${{ job.status }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*프로덕션 배포 ${{ job.status }}*\n커밋: <${{ github.event.head_commit.url }}|${{ github.event.head_commit.message }}>\n작성자: ${{ github.event.head_commit.author.name }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
마무리
프론트엔드 CI/CD의 핵심은 캐시와 병렬 실행이다. actions/cache로 Next.js 빌드 캐시를 보존하고, 타입체크/린트/테스트를 병렬로 실행하면 5분 파이프라인을 2분으로 단축할 수 있다. concurrency로 동일 브랜치의 이전 실행을 취소하면 불필요한 실행을 줄인다. PR 미리보기 URL은 디자이너와 QA팀이 개발자 없이도 확인할 수 있어 협업 효율이 크게 오른다.