coredot.today
GitHub Actions 실전 가이드: 나만의 자동화 파이프라인 만들기
블로그로 돌아가기
GitHub ActionsCI/CD자동화DevOps워크플로우GitHub

GitHub Actions 실전 가이드: 나만의 자동화 파이프라인 만들기

Push만 하면 테스트·빌드·배포가 알아서 돌아간다. GitHub Actions의 개념부터 YAML 문법, 실전 파이프라인 구축까지. 코드 저장소에 자동화를 내장하는 방법을 처음부터 끝까지 풀어본다.

코어닷투데이2026-04-0150

들어가며: "Push하면 알아서 테스트하고 배포된다" — 개발자의 꿈

개발자에게는 오래된 꿈이 있다.

코드를 작성하고, git push를 누르면 — 테스트가 자동으로 돌아가고, 빌드가 만들어지고, 스테이징 서버에 배포되고, Slack에 결과가 날아온다. 수동으로 서버에 접속해서 git pull을 치고, npm run build를 치고, PM2를 재시작하는 시대는 이미 끝났다.

이것을 CI/CD(Continuous Integration / Continuous Delivery)라고 부른다.

  • CI(지속적 통합): 코드가 합쳐질 때마다 자동으로 테스트하고 빌드한다
  • CD(지속적 배포): 빌드된 결과물을 자동으로 서버에 배포한다

CI/CD는 새로운 개념이 아니다. Jenkins, Travis CI, CircleCI 등 많은 도구가 있었지만, 대부분 별도의 서비스 가입과 서버 운영이 필요했다.

2019년 11월, GitHub이 이 판을 뒤집었다. GitHub Actions.github/workflows/ 폴더에 YAML 파일 하나만 넣으면 되는 CI/CD 플랫폼.

2026년 현재, GitHub Actions는 오픈소스 프로젝트의 사실상 표준 CI/CD 도구가 됐다. 이 글에서는 개념부터 YAML 문법, 트리거 종류, 실전 배포 파이프라인까지 — 처음 접하는 사람도 자신만의 자동화 파이프라인을 만들 수 있도록 풀어본다.


1. GitHub Actions의 핵심 개념 5가지

GitHub Actions 워크플로우를 요리 레시피 단계로 표현한 일러스트

GitHub Actions를 이해하려면 5가지 개념을 알아야 한다. 이 5개가 전부다.

GitHub Actions 핵심 구성 요소
Workflow (워크플로우) 자동화의 최상위 단위. YAML 파일 하나 = 워크플로우 하나
Job (작업) 워크플로우 안의 실행 단위. 기본적으로 병렬 실행
Step (단계) Job 안의 개별 명령. 순차 실행
Action (액션) 재사용 가능한 단위 기능. Marketplace에서 가져다 쓸 수 있음
Runner (러너) 워크플로우가 실행되는 서버. GitHub-hosted 또는 Self-hosted

Workflow는 자동화 전체를 정의하는 YAML 파일이다. .github/workflows/에 저장하며, ci.yml, deploy.yml 등 여러 개를 만들 수 있다.

Job은 워크플로우 안의 독립적 실행 단위다. 기본적으로 병렬 실행되며, needs로 순서를 지정한다. 각 Job은 별도의 Runner에서 실행되므로 Job 간 파일 시스템은 공유되지 않는다.

Step은 Job 안에서 순차 실행되는 개별 명령이다. run으로 쉘 명령을, uses로 외부 Action을 실행한다. 같은 Job의 Step들은 파일 시스템을 공유한다.

Action은 재사용 가능한 코드 묶음이다. uses: actions/checkout@v4처럼 소유자/이름@버전으로 참조한다.

Runner는 워크플로우가 실행되는 서버다. GitHub-hosted(Ubuntu, Windows, macOS)를 쓰거나, Self-hosted Runner를 등록할 수 있다.

💡
비유하면: Workflow는 레시피 전체, Job은 "반죽 만들기" "소스 만들기" 같은 큰 단계, Step은 "밀가루 넣기" "물 섞기" 같은 개별 동작, Action은 "믹서기" 같은 도구, Runner는 "주방" 그 자체다.

2. YAML 문법 기초: 워크플로우의 뼈대

GitHub Actions 워크플로우는 YAML 형식으로 작성한다. 들여쓰기로 구조를 표현하는 데이터 직렬화 언어다.

가장 간단한 워크플로우

hljs language-yaml
name: Hello World

on: push

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Say hello
        run: echo "Hello, GitHub Actions!"

이 7줄짜리 파일만으로도 동작하는 워크플로우가 완성된다.

name 워크플로우의 이름. GitHub UI의 Actions 탭에 표시된다.
on 트리거. 어떤 이벤트가 발생했을 때 실행할지 정의한다. push, pull_request, schedule 등.
jobs / runs-on 실행할 작업들과 Runner OS를 정의. ubuntu-latest, windows-latest, macos-latest.
steps 순차 실행할 단계들. run은 쉘 명령, uses는 외부 Action.

실전 구조 예시

hljs language-yaml
name: CI Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4       # 코드 체크아웃 (거의 모든 워크플로우에 필수)
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test
        env:
          NODE_ENV: test                 # 환경 변수 설정

  deploy:
    needs: build                         # build Job이 성공한 후에 실행
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: echo "Deploying..."
구분usesrun
역할외부 Action을 가져와 실행쉘 명령을 직접 실행
형식uses: owner/repo@versionrun: npm ci && npm test
입력값with: 키워드로 전달쉘 변수나 env:로 전달
적합한 경우복잡한 로직을 재사용할 때간단한 명령을 실행할 때
💡
들여쓰기 주의: YAML에서 들여쓰기는 스페이스 2칸을 사용한다. 탭(tab)을 쓰면 에러가 난다. 에디터 설정에서 탭을 스페이스 2칸으로 변환하는 것을 권장한다.

3. 트리거 종류: 언제 워크플로우를 실행할 것인가

on 키워드 아래에 정의하는 트리거는 워크플로우의 시작 조건이다. 실전에서 자주 쓰는 4가지를 알아보자.

push — 코드가 push될 때

hljs language-yaml
on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'                   # src 폴더가 변경됐을 때만
      - '!src/**/*.test.ts'        # 테스트 파일 변경은 제외

paths 필터로 특정 파일이 변경됐을 때만 실행하게 할 수 있다.

pull_request — PR이 열리거나 업데이트될 때

hljs language-yaml
on:
  pull_request:
    branches: [main]

PR 코드 리뷰 전에 자동으로 테스트를 돌리는 데 사용한다.

schedule — 크론(cron) 스케줄

hljs language-yaml
on:
  schedule:
    - cron: '0 0 * * *'    # 매일 자정(UTC) 실행
크론 표현식 해석법
┌───── 분 (0~59)
│ ┌───── 시 (0~23)
│ │ ┌───── 일 (1~31)
│ │ │ ┌───── 월 (1~12)
│ │ │ │ ┌───── 요일 (0~6, 0=일요일)
│ │ │ │ │
* * * * *

0 0 * * * → 매일 자정(UTC)
0 */6 * * * → 6시간마다
30 9 * * 1-5 → 평일 오전 9:30(UTC)
0 0 1 * * → 매월 1일 자정

주의: UTC 기준이므로 한국 시간(KST = UTC+9)으로 환산해야 한다.

workflow_dispatch — 수동 실행

hljs language-yaml
on:
  workflow_dispatch:
    inputs:
      environment:
        type: choice
        options: [staging, production]

GitHub UI에서 수동으로 실행하며 입력값을 받을 수 있다. 긴급 배포에 유용하다.


4. 첫 번째 워크플로우: PR마다 Lint + Test 자동 실행

이론은 충분하다. 가장 기본적이면서도 가장 중요한 워크플로우 — PR마다 lint와 테스트를 자동으로 돌리는 CI 파이프라인을 만든다.

hljs language-yaml
name: CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm test

  typecheck:
    name: Type Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npx tsc --noEmit
PR 또는 Push 이벤트
Lint (ESLint)
Test (Jest)
Type Check (tsc)
3개 모두 통과 → PR 머지 가능

needs가 없으므로 3개 Job이 병렬 실행되어 전체 시간을 단축한다. cache: 'npm'으로 의존성 설치를 빠르게 하고, npm cipackage-lock.json 기반의 재현 가능한 빌드를 보장한다.

Branch Protection Rule과 함께 사용하면 더 강력하다. Settings → Branches에서 "Require status checks to pass"를 활성화하면, 하나라도 실패한 PR은 머지할 수 없다.


5. Secrets와 환경 변수: 민감한 정보 다루기

CI/CD 파이프라인에서는 API 키, 배포 토큰 같은 민감한 정보를 다뤄야 한다. GitHub Actions는 이를 위해 Secrets 기능을 제공한다.

GitHub 리포지토리 → Settings → Secrets and variables → ActionsNew repository secret에서 등록한다. 한 번 저장하면 값을 다시 볼 수 없고, 로그에도 ***로 마스킹된다.

hljs language-yaml
steps:
  - name: Configure AWS credentials
    uses: aws-actions/configure-aws-credentials@v4
    with:
      aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
      aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      aws-region: ap-northeast-2

Secrets의 3가지 범위

GitHub Secrets 범위
Repository Secret 가장 일반적 해당 리포지토리의 워크플로우에서만 접근 가능
Environment Secret 환경별 분리 staging, production 등 환경별로 다른 값. 승인 절차 추가 가능
Organization Secret 조직 전체 공유 조직 내 여러 리포지토리에서 공유. 접근 가능 리포 제한 가능

Environment 기능으로 staging/production을 분리할 수 있다. environment: production을 설정하면 required reviewers(필수 승인자)를 지정할 수 있어, 프로덕션 배포 전 승인을 요구할 수 있다.

hljs language-yaml
jobs:
  deploy-staging:
    environment: staging
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh
        env:
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}

  deploy-production:
    needs: deploy-staging
    environment: production
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

6. Matrix Strategy: 여러 버전에서 동시에 테스트하기

매트릭스 테스트를 병렬 요리 스테이션으로 표현한 일러스트

"Node.js 18에서는 되는데 20에서는 안 된다" — 이런 문제를 방지하려면 여러 환경에서 동시에 테스트해야 한다. Matrix Strategy가 이것을 자동화한다.

hljs language-yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

이 설정 하나로 3개의 Job이 자동 생성되어 병렬 실행된다. OS와 버전을 조합하면 다차원 Matrix도 가능하다 — os: [ubuntu-latest, windows-latest] + node-version: [18, 20, 22]면 6개 Job이 생성된다. exclude로 특정 조합을 제외할 수도 있다.

💡
fail-fast 옵션: 기본적으로 Matrix의 한 Job이 실패하면 나머지도 취소된다. strategy: fail-fast: false로 설정하면 실패한 조합과 관계없이 모든 조합이 끝까지 실행된다. 어떤 버전에서 실패하는지 전부 확인하고 싶을 때 유용하다.

7. 캐싱: 빌드 속도를 극적으로 높이는 방법

캐싱을 미리 준비된 요리 재료로 비유한 일러스트

CI가 느리면 아무도 안 쓴다. 캐싱은 CI 속도를 개선하는 가장 효과적인 방법이다.

가장 간단한 캐시: setup-node 내장

hljs language-yaml
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'     # 또는 'yarn', 'pnpm'

이 한 줄로 ~/.npm 캐시가 자동 저장/복원된다.

세밀한 캐싱: actions/cache

더 세밀한 제어가 필요하면 actions/cache를 직접 사용한다.

hljs language-yaml
- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

hashFiles('package-lock.json')package-lock.json이 바뀔 때만 새 캐시가 만들어진다. 의존성이 변하지 않았으면 캐시를 재활용한다.

캐싱 효과

캐시 없음 (매번 npm ci)
45초
setup-node cache: npm
24초
actions/cache + node_modules
6초

Docker 이미지를 빌드하는 워크플로우에서는 Docker 레이어 캐시도 중요하다. docker/build-push-actioncache-from: type=gha, cache-to: type=gha,mode=max 옵션으로 GitHub Actions 캐시 스토리지를 Docker BuildKit 캐시로 활용하면 이미지 빌드 시간이 50~80% 단축된다.


8. 실전 예제 1: Docker Build → ECR Push → ECS Deploy

컨테이너 배포 파이프라인 — Docker 이미지를 빌드하고, AWS ECR에 푸시한 뒤, ECS 서비스를 업데이트한다.

main Push
테스트
Docker 빌드 + ECR Push
ECS 롤링 업데이트
hljs language-yaml
name: Deploy to ECS

on:
  push:
    branches: [main]

env:
  AWS_REGION: ap-northeast-2
  ECR_REPOSITORY: myapp
  ECS_SERVICE: myapp-service
  ECS_CLUSTER: production

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci && npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ env.AWS_REGION }}
      - name: Login to ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
      - name: Build and push image
        id: build-image
        run: |
          IMAGE=${{ steps.login-ecr.outputs.registry }}/$ECR_REPOSITORY:${{ github.sha }}
          docker build -t $IMAGE .
          docker push $IMAGE
          echo "image=$IMAGE" >> $GITHUB_OUTPUT
      - name: Update task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: myapp
          image: ${{ steps.build-image.outputs.image }}
      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true

github.sha(커밋 해시)를 이미지 태그로 사용하면 모든 배포를 추적할 수 있다. wait-for-service-stability: true로 배포 실패 시 워크플로우도 실패로 표시된다.

💡
보안 팁: AWS 자격증명은 Secrets보다 OIDC(OpenID Connect)가 더 안전하다. role-to-assume 옵션으로 IAM Role을 직접 assume하면 장기 자격증명 없이 배포할 수 있다.

9. 실전 예제 2: 정적 사이트 → S3 + CloudFront 무효화

정적 사이트를 S3에 업로드하고 CloudFront 캐시를 무효화하는 파이프라인이다.

main Push (src 변경 시)
빌드 → S3 동기화 → CloudFront 무효화
hljs language-yaml
name: Deploy Static Site

on:
  push:
    branches: [main]
    paths: ['src/**', 'public/**', 'package.json']

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: 'npm' }
      - run: npm ci && npm run build
      - name: Configure AWS
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-2
      - name: Sync hashed assets (long cache)
        run: |
          aws s3 sync out/ s3://${{ vars.S3_BUCKET }} --delete \
            --cache-control "public, max-age=31536000, immutable" \
            --exclude "*.html" --exclude "*.json"
      - name: Sync HTML/JSON (no cache)
        run: |
          aws s3 sync out/ s3://${{ vars.S3_BUCKET }} \
            --cache-control "public, max-age=0, must-revalidate" \
            --exclude "*" --include "*.html" --include "*.json"
      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ vars.CLOUDFRONT_DIST_ID }} --paths "/*"

S3 동기화를 두 번 하는 이유:

파일 종류Cache-Control이유
JS, CSS, 이미지 (해시 포함)max-age=31536000, immutable파일명에 해시가 있어 내용 변경 시 파일명도 바뀜. 영구 캐싱 가능
HTML, JSONmax-age=0, must-revalidate항상 같은 URL이므로 최신 버전을 받아야 함

10. GitHub Marketplace: 커뮤니티 액션 활용하기

대부분의 일반적인 작업은 이미 Marketplace에 Action으로 만들어져 있다.

카테고리Action용도
기본actions/checkout@v4코드 체크아웃
언어actions/setup-node@v4Node.js 설정 + 캐시
캐시actions/cache@v4의존성 캐싱
Dockerdocker/build-push-action@v5이미지 빌드 + 푸시
AWSaws-actions/configure-aws-credentials@v4AWS 자격증명 (OIDC)
보안github/codeql-action/analyze@v3코드 취약점 분석

버전 관리와 Dependabot

Action을 참조할 때는 @v4 같은 메이저 버전 태그를 사용하라. @main은 호환성이 깨질 수 있어 위험하다. .github/dependabot.ymlpackage-ecosystem: "github-actions"를 설정하면 새 버전이 나올 때 자동으로 업데이트 PR이 생성된다.


11. Self-hosted Runner: 언제, 왜 사용하는가

GitHub-hosted Runner는 편리하지만 한계가 있다.

항목GitHub-hostedSelf-hosted
관리GitHub이 관리직접 관리
비용공개 리포: 무료 / 비공개: 월 2,000분서버 비용만
스펙2 vCPU, 7GB RAM원하는 만큼
네트워크공용 IP (내부망 접근 불가)VPC 내부, 사내 DB 접근 가능
GPU불가가능 (ML 학습, CUDA 빌드)
깨끗한 환경매번 새 VM직접 정리 필요

Self-hosted가 필요한 상황

내부망 접근 사내 DB, 내부 API에 접근해야 하는 배포. VPC 안에 Runner를 두는 것이 깔끔하다.
높은 사양 ML 학습, 대규모 빌드, GPU 필요. 2 vCPU/7GB로는 부족하다.
비용 / 특수 환경 워크플로우가 많아 비용이 높을 때, 또는 특정 OS·하드웨어(ARM, FPGA)가 필요할 때.

설정은 간단하다. Settings → Actions → Runners에서 설치 스크립트가 제공되고, 워크플로우에서는 runs-on: [self-hosted, linux, gpu]처럼 라벨로 지정한다.

💡
보안 주의: 공개 리포지토리에서는 Self-hosted Runner를 사용하지 마라. 외부 기여자가 악의적인 코드를 PR로 올리면 당신의 서버에서 실행된다. Self-hosted Runner는 비공개 리포지토리에서만 사용하라.

12. 실전 팁과 보안 모범 사례

자주 쓰는 패턴

조건부 실행 (if)

hljs language-yaml
steps:
  - name: Deploy to production
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: ./deploy.sh

  - name: Notify on failure
    if: failure()
    run: curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} ...

동시 실행 제한 (concurrency)

hljs language-yaml
concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

같은 브랜치에서 여러 번 push하면 이전 워크플로우를 취소하고 최신 것만 실행한다.

타임아웃

hljs language-yaml
jobs:
  test:
    timeout-minutes: 15

기본 타임아웃은 6시간이다. 반드시 적절한 값을 설정하라.

Reusable Workflow

여러 리포지토리에서 같은 CI를 쓴다면 on: workflow_call로 재사용 가능한 워크플로우를 만들고, 다른 리포에서 uses: my-org/shared-workflows/.github/workflows/reusable-ci.yml@main으로 호출할 수 있다.

보안 핵심 원칙

최소 권한 permissions: contents: read만 필요한데 write-all을 주지 마라.
입력값 신뢰 금지 PR 제목, 커밋 메시지를 쉘에 직접 넣지 마라. 인젝션 공격에 취약하다. 환경 변수로 전달하라.
Action 버전 고정 서드파티 Action은 최소한 메이저 버전 태그. @main은 절대 금지.
OIDC 사용 클라우드 자격증명은 장기 키 대신 OIDC 토큰. Secret 유출 위험을 원천 차단.

비용 관리

Free 플랜은 월 2,000분, Team은 3,000분, Enterprise는 50,000분이 무료다. Windows는 2배, macOS는 10배 비용이므로 ubuntu-latest가 가장 저렴하다. 공개 리포지토리는 완전 무료. paths 필터, concurrency, 캐싱, timeout-minutes를 적극 활용해서 낭비를 줄여라.


마치며: 자동화는 습관이다

GitHub Actions는 단순한 CI/CD 도구가 아니다. 코드 저장소에 자동화를 내장하는 플랫폼이다.

문제
수동 배포의 고통
서버 접속 → git pull → 빌드 → 재시작 → 확인. 매번 30분. 실수 한 번이면 서비스 다운. 금요일 오후 5시에 배포하고 싶은 사람은 아무도 없다.
해결
GitHub Actions로 자동화
YAML 파일 하나에 전체 파이프라인을 정의한다. Push하면 테스트 → 빌드 → 배포가 자동으로 돌아간다. 사람이 개입할 필요 없다.
결과
개발에만 집중
배포는 git push 한 번이면 끝. 테스트가 실패하면 PR이 머지되지 않는다. 팀 전체의 코드 품질이 자동으로 유지된다.

시작은 작게 하라. PR에 lint 하나 돌리는 것부터 시작해서, 테스트를 추가하고, 배포를 자동화하고, 점진적으로 확장하면 된다. .github/workflows/ci.yml 파일 하나가 팀 전체의 개발 문화를 바꿀 수 있다.

코드는 혼자 배포되지 않는다. 하지만 자동화를 설정해 놓으면, Push 한 번으로 세상에 나갈 수 있다.


GitHub Actions 고급 주제(Composite Action, OIDC 상세 설정, Terraform 연동, 모노레포 전략 등)는 후속 글에서 다룰 예정입니다.