GitHub Actions는 GitHub에 내장된 CI/CD 플랫폼으로, 별도의 Jenkins나 CircleCI 없이 저장소 안에서 직접 자동화 플로우를 정의할 수 있습니다. 이 글은 본 사이트(Nuxt 4 + pnpm 프로젝트)를 예시로, 개념부터 실전까지 코드 푸시 후 자동 빌드·배포를 구현하는 과정을 설명합니다.
핵심 개념
설정을 작성하기 전에 주요 용어를 정리해 두면 이후 내용을 이해하기 쉽습니다.
| 개념 | 설명 |
|---|---|
| Workflow | 자동화 플로우 전체. .github/workflows/ 아래에 YAML 파일로 저장됨 |
| Event | Workflow를 실행하는 트리거. push·pull_request·schedule·수동 실행 등 |
| Job | Workflow 안의 실행 단위. 각 Job은 독립된 VM에서 실행되며 기본은 병렬 실행 |
| Step | Job 안의 단일 작업. 순서대로 실행되며 쉘 스크립트 또는 Action을 지정할 수 있음 |
| Action | GitHub Marketplace나 직접 작성한 재사용 가능 모듈. uses로 참조 |
| Runner | Job을 실행하는 VM. GitHub 제공 hosted runner와 직접 구성하는 self-hosted runner가 있음 |
| Secret | 암호화된 환경 변수. 저장소 Settings에서 설정하며 ${{ secrets.MY_KEY }}로 참조 |
Workflow 파일 구조
모든 Workflow 파일은 저장소 루트의 .github/workflows/ 아래에 위치합니다. 파일명은 자유이고, 확장자는 .yml 또는 .yaml 모두 가능합니다.
가장 단순한 예시:
name: Deploy # Actions 페이지에 표시되는 이름
on: # 트리거 조건
push:
branches:
- main
jobs: # 하나 이상의 Job
build: # Job ID — 자유롭게 명명
runs-on: ubuntu-latest # 사용할 Runner
steps: # 이 Job의 실행 단계
- name: 코드 체크아웃
uses: actions/checkout@v4
- name: Node 버전 출력
run: node -v
트리거 이벤트 상세
on:
# 지정 브랜치에 푸시할 때
push:
branches:
- main
- 'release/**' # glob 패턴 지원
paths:
- 'app/**' # 이 경로가 변경될 때만 실행(선택 사항)
# PR 활동 시
pull_request:
branches:
- main
types: [opened, synchronize, reopened]
# 정기 실행(cron 구문, UTC 기준)
schedule:
- cron: '0 2 * * *' # 매일 UTC 02:00
# 수동 실행(Actions 페이지에 버튼이 생김)
workflow_dispatch:
inputs:
environment:
description: '배포 환경'
required: true
default: 'production'
type: choice
options:
- production
- staging
실전: 본 사이트 CI/CD 구성
본 사이트는 Nuxt 4 프로젝트로, pnpm으로 의존성을 관리하고 PM2로 서버를 운영합니다. 목표는 다음 두 가지입니다.
- CI (코드 검사): PR 또는 푸시 시마다 ESLint를 자동 실행
- CD (자동 배포):
main브랜치에 푸시하면 Runner에서 빌드 완료 후 결과물을 압축해 SCP로 서버에 업로드, SSH로 압축 해제 후 PM2 재시작
1단계: Secrets 준비
저장소의 Settings → Secrets and variables → Actions에서 다음 변수를 추가합니다.
| Secret 이름 | 내용 |
|---|---|
SSH_HOST | 서버 IP 또는 도메인, 예: 123.45.67.89 |
SSH_PORT | SSH 포트, 기본값 22 |
SSH_USER | 로그인 사용자명, 예: root 또는 deploy |
SSH_KEY | 서버 개인 키 내용(cat ~/.ssh/id_ed25519 출력값) |
SSH_PATH | 서버 배포 디렉토리, 예: /www/wwwroot/fonnpo-web |
개인 키 설정:
~/.ssh/id_ed25519의 전체 내용(-----BEGIN...및-----END...줄 포함)을SSH_KEY에 붙여넣습니다. 대응하는 공개 키(~/.ssh/id_ed25519.pub)는 서버의~/.ssh/authorized_keys에 추가하세요.
2단계: CI Workflow (코드 검사)
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: ESLint 검사
runs-on: ubuntu-latest
steps:
- name: 코드 체크아웃
uses: actions/checkout@v4
- name: pnpm 설치
uses: pnpm/action-setup@v4
with:
version: 10
- name: Node.js 설치
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm # pnpm store 캐시로 설치 속도 향상
- name: 의존성 설치
run: pnpm install --frozen-lockfile
- name: ESLint 실행
run: pnpm eslint .
--frozen-lockfile은 CI 환경이 항상 pnpm-lock.yaml에 명시된 버전을 설치하도록 보장합니다.
3단계: CD Workflow (자동 배포)
본 사이트는 Runner에서 빌드를 완료한 후 결과물을 서버로 전송하는 방식을 채택합니다. 서버에서 git pull + build를 실행하는 방식과 비교했을 때, 서버에 Node.js 빌드 환경이 필요 없고, 빌드 실패가 운영 서비스에 영향을 주지 않으며, 서버 부하도 낮습니다.
name: Deploy to Self-hosted Server
on:
push:
branches: [main]
workflow_dispatch: # Actions 페이지에서 수동 실행 가능
jobs:
deploy:
runs-on: ubuntu-latest
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # Actions 런타임을 Node 24로 강제
NODE_OPTIONS: --max-old-space-size=4096 # 대규모 빌드 OOM 방지
steps:
- name: 코드 체크아웃
uses: actions/checkout@v4
- name: pnpm 설치
uses: pnpm/action-setup@v4
- name: Node.js 설치
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: 의존성 설치
run: pnpm install --frozen-lockfile
- name: 빌드
run: pnpm build
- name: 결과물 압축
run: |
tar -czf release.tgz .output ecosystem.config.cjs package.json pnpm-lock.yaml
- name: 서버에 업로드 (SCP)
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
source: release.tgz
target: ${{ secrets.SSH_PATH }}
- name: 서버에서 압축 해제 및 재시작
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
set -e
cd ${{ secrets.SSH_PATH }}
tar -xzf release.tgz
rm -f release.tgz
corepack enable
pnpm install --prod --frozen-lockfile
pm2 startOrReload ecosystem.config.cjs --update-env
pm2 save
주요 포인트:
env블록:FORCE_JAVASCRIPT_ACTIONS_TO_NODE24로 Actions 내부 런타임을 Node 24에 맞춤.NODE_OPTIONS로 힙 메모리 한도를 늘려 Nuxt 대규모 빌드 OOM 방지tar압축:.output(Nuxt 빌드 결과물),ecosystem.config.cjs,package.json,pnpm-lock.yaml만 포함. 소스 코드는 업로드하지 않음appleboy/scp-action: SCP 프로토콜로 아카이브를 서버의SSH_PATH디렉토리에 전송pnpm install --prod: 서버에서는 프로덕션 의존성만 설치. 빌드 도구 불필요, 용량도 절감pm2 startOrReload:pm2 reload보다 안정적. 프로세스가 없으면 새로 시작하고, 있으면 다운타임 없이 재로드.pm2 save로 프로세스 목록을 저장해 서버 재시작 후에도 자동 복구
4단계: 하나의 파일로 합치기 (CI → CD)
CI가 통과한 경우에만 배포하려면 needs로 Job을 직렬화합니다.
name: CI / CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: ESLint 검사
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm eslint .
deploy:
name: 프로덕션 서버 배포
runs-on: ubuntu-latest
needs: lint # ← lint 성공 후에만 실행
if: github.ref == 'refs/heads/main' # ← PR은 배포 안 함. main 푸시만 배포
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
NODE_OPTIONS: --max-old-space-size=4096
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: 결과물 압축
run: tar -czf release.tgz .output ecosystem.config.cjs package.json pnpm-lock.yaml
- name: 서버에 업로드 (SCP)
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
source: release.tgz
target: ${{ secrets.SSH_PATH }}
- name: 서버에서 압축 해제 및 재시작
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
set -e
cd ${{ secrets.SSH_PATH }}
tar -xzf release.tgz && rm -f release.tgz
corepack enable
pnpm install --prod --frozen-lockfile
pm2 startOrReload ecosystem.config.cjs --update-env
pm2 save
PR에서는 lint만 실행됩니다. main에 머지되면 lint → deploy 순으로 실행되며, 둘 다 성공해야 완료입니다.
유용한 팁
의존성 캐시로 빌드 속도 향상
actions/setup-node의 cache: 'pnpm'을 사용하면 pnpm store가 자동으로 캐시되어 pnpm install 시간이 크게 단축됩니다.
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm # pnpm-lock.yaml 기반으로 캐시 키 자동 생성
환경 변수와 Secrets의 차이
env:
NODE_ENV: production # 평문 — 로그에 표시됨
steps:
- run: echo ${{ secrets.TOKEN }} # Secret — 로그에서 ***로 마스킹됨
비밀번호, 토큰, 개인 키는 반드시 Secrets를 사용하세요.
수동 배포 트리거
코드를 푸시하지 않고 재배포하고 싶을 때(서버 재시작 후 등) workflow_dispatch를 추가합니다.
on:
workflow_dispatch: # Actions 페이지에 "Run workflow" 버튼이 생김
push:
branches: [main]
Job 간 데이터 전달
Job은 격리되어 있습니다. outputs로 데이터를 공유합니다.
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- id: get-version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "버전 ${{ needs.build.outputs.version }} 배포 중"
특정 경로 변경 시에만 트리거
프론트엔드와 백엔드가 같은 저장소에 있을 때, 관련 파일이 변경될 때만 플로우를 실행할 수 있습니다.
on:
push:
paths:
- 'app/**'
- nuxt.config.ts
- package.json
- pnpm-lock.yaml
branches: [main]
실행 결과 확인
푸시 후 GitHub 저장소의 Actions 탭에서 모든 실행 기록을 확인할 수 있습니다. 클릭하면 각 Step의 전체 로그를 펼쳐볼 수 있으며, 실패 시 빨간색으로 표시되어 원인을 빠르게 파악할 수 있습니다.
설정 후 전체 Workflow의 실행 모습:
Actions
└── Deploy to Self-hosted Server
├── 🟢 lint (20s)
│ ├── 코드 체크아웃
│ ├── pnpm / Node.js 설치
│ ├── pnpm install
│ └── ESLint 검사
└── 🟢 deploy (3분)
├── 코드 체크아웃
├── pnpm / Node.js 설치
├── pnpm install
├── pnpm build
├── tar 압축
├── SCP 서버 업로드
└── SSH 압축 해제 및 PM2 재시작
├── tar -xzf release.tgz
├── pnpm install --prod
├── pm2 startOrReload
└── pm2 save
한 번 설정해두면 이후 git push만으로 완전 자동화됩니다. 서버에 직접 SSH 접속해 배포하는 번거로움이 사라집니다.
Fonnpo