logo Fonnpo

문의하기
2026년 5월 31일 1분 읽기 Fonnpo

GitHub Actions 입문부터 실전까지

핵심 개념, 트리거 이벤트, 캐시 가속 — Nuxt 프로젝트 예시로 자동 배포 구현.

#DevOps#CI/CD#GitHub

GitHub Actions는 GitHub에 내장된 CI/CD 플랫폼으로, 별도의 Jenkins나 CircleCI 없이 저장소 안에서 직접 자동화 플로우를 정의할 수 있습니다. 이 글은 본 사이트(Nuxt 4 + pnpm 프로젝트)를 예시로, 개념부터 실전까지 코드 푸시 후 자동 빌드·배포를 구현하는 과정을 설명합니다.

핵심 개념

설정을 작성하기 전에 주요 용어를 정리해 두면 이후 내용을 이해하기 쉽습니다.

개념설명
Workflow자동화 플로우 전체. .github/workflows/ 아래에 YAML 파일로 저장됨
EventWorkflow를 실행하는 트리거. push·pull_request·schedule·수동 실행 등
JobWorkflow 안의 실행 단위. 각 Job은 독립된 VM에서 실행되며 기본은 병렬 실행
StepJob 안의 단일 작업. 순서대로 실행되며 쉘 스크립트 또는 Action을 지정할 수 있음
ActionGitHub Marketplace나 직접 작성한 재사용 가능 모듈. uses로 참조
RunnerJob을 실행하는 VM. GitHub 제공 hosted runner와 직접 구성하는 self-hosted runner가 있음
Secret암호화된 환경 변수. 저장소 Settings에서 설정하며 ${{ secrets.MY_KEY }}로 참조

Workflow 파일 구조

모든 Workflow 파일은 저장소 루트의 .github/workflows/ 아래에 위치합니다. 파일명은 자유이고, 확장자는 .yml 또는 .yaml 모두 가능합니다.

가장 단순한 예시:

.github/workflows/deploy.yml
        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

    

트리거 이벤트 상세

Yaml
        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로 서버를 운영합니다. 목표는 다음 두 가지입니다.

  1. CI (코드 검사): PR 또는 푸시 시마다 ESLint를 자동 실행
  2. CD (자동 배포): main 브랜치에 푸시하면 Runner에서 빌드 완료 후 결과물을 압축해 SCP로 서버에 업로드, SSH로 압축 해제 후 PM2 재시작

1단계: Secrets 준비

저장소의 Settings → Secrets and variables → Actions에서 다음 변수를 추가합니다.

Secret 이름내용
SSH_HOST서버 IP 또는 도메인, 예: 123.45.67.89
SSH_PORTSSH 포트, 기본값 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 (코드 검사)

.github/workflows/ci.yml
        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 빌드 환경이 필요 없고, 빌드 실패가 운영 서비스에 영향을 주지 않으며, 서버 부하도 낮습니다.

.github/workflows/deploy.yml
        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을 직렬화합니다.

.github/workflows/ci-cd.yml
        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에 머지되면 lintdeploy 순으로 실행되며, 둘 다 성공해야 완료입니다.

유용한 팁

의존성 캐시로 빌드 속도 향상

actions/setup-nodecache: 'pnpm'을 사용하면 pnpm store가 자동으로 캐시되어 pnpm install 시간이 크게 단축됩니다.

Yaml
        - uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: pnpm       # pnpm-lock.yaml 기반으로 캐시 키 자동 생성

    

환경 변수와 Secrets의 차이

Yaml
        env:
  NODE_ENV: production       # 평문 — 로그에 표시됨

steps:
  - run: echo ${{ secrets.TOKEN }}  # Secret — 로그에서 ***로 마스킹됨

    

비밀번호, 토큰, 개인 키는 반드시 Secrets를 사용하세요.

수동 배포 트리거

코드를 푸시하지 않고 재배포하고 싶을 때(서버 재시작 후 등) workflow_dispatch를 추가합니다.

Yaml
        on:
  workflow_dispatch:    # Actions 페이지에 "Run workflow" 버튼이 생김

  push:
    branches: [main]

    

Job 간 데이터 전달

Job은 격리되어 있습니다. outputs로 데이터를 공유합니다.

Yaml
        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 }} 배포 중"

    

특정 경로 변경 시에만 트리거

프론트엔드와 백엔드가 같은 저장소에 있을 때, 관련 파일이 변경될 때만 플로우를 실행할 수 있습니다.

Yaml
        on:
  push:
    paths:
      - 'app/**'
      - nuxt.config.ts
      - package.json
      - pnpm-lock.yaml
    branches: [main]

    

실행 결과 확인

푸시 후 GitHub 저장소의 Actions 탭에서 모든 실행 기록을 확인할 수 있습니다. 클릭하면 각 Step의 전체 로그를 펼쳐볼 수 있으며, 실패 시 빨간색으로 표시되어 원인을 빠르게 파악할 수 있습니다.

설정 후 전체 Workflow의 실행 모습:

Text
        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