테마 전환

GitHub Actions CI 파이프라인 실전: 처음부터 자동 빌드와 테스트 구축하기

Easton editorial illustration: solo-founder business system console

휴대전화가 울렸습니다. 동료가 메시지를 보냈습니다. “프로덕션 환경에 문제가 생겼어요. 어제 병합한 코드에 문제가 있습니다.”

머리가 멍해졌습니다. 분명 로컬에서 테스트했는데 왜 그랬을까요? 나중에 로그를 확인해 보니 로컬 Node 버전은 20이었고 테스트 환경은 18이었습니다. 특정 API의 동작이 달라 버그가 발생한 것입니다. 그 순간 이런 생각밖에 들지 않았습니다. 병합 전에 테스트를 자동으로 한 번 돌려 주는 CI 파이프라인만 있었다면 애초에 일어나지 않았을 일입니다.

테스트를 수동으로 실행하면 개발자 열 명 중 아홉 명은 잊어버립니다. 잊지 않는 나머지 한 명은 아마 이미 크게 데인 적이 있을 것입니다. GitHub Actions는 바로 이 문제를 해결합니다. 코드를 push하면 빌드, 테스트, 배포를 자동으로 실행합니다. 신경 쓰지 않아도 시스템이 대신 처리합니다.

이 글에서는 처음부터 완전한 CI 파이프라인을 구축합니다. 바로 복사해 사용할 수 있는 워크플로 템플릿, Matrix 전략을 활용한 다중 버전 병렬 테스트(빌드 시간을 절반 이상 줄일 수 있습니다), 그리고 직접 겪은 문제와 실전 경험까지 다룹니다. 준비되셨나요? 시작하겠습니다.

1장: GitHub Actions 빠르게 시작하기

GitHub Actions란 무엇인가요?

간단히 말해 GitHub Actions는 GitHub에 내장된 자동화 플랫폼입니다. 로컬에서 코드를 push하면 클라우드에서 테스트, 빌드, 배포를 완전히 자동으로 실행해 줍니다.

예전에는 CI/CD를 구현하려면 Jenkins 서버를 직접 구축하고 설정, 유지 관리, 업그레이드를 모두 챙겨야 했습니다. GitHub Actions의 강점은 서버를 관리하거나 소프트웨어를 설치할 필요 없이 저장소에 YAML 파일 하나만 넣으면 된다는 점입니다. 게다가 매월 2,000분의 무료 사용량이 제공되고 공개 저장소는 제한이 없어 개인 프로젝트와 소규모 팀에는 충분합니다.

Jenkins나 Travis CI와 비교하면 GitHub Actions의 장점은 분명합니다. GitHub 저장소와 긴밀하게 통합되어 PR에서 빌드 상태를 바로 볼 수 있고, 설정이 간단해 Groovy 문법을 배울 필요가 없으며, 공식 Marketplace에는 바로 사용할 수 있는 Action이 수만 개나 있습니다. 단점도 있습니다. GitHub에 종속되기 때문에 GitLab으로 이전하려면 설정을 다시 작성해야 하고, 복잡한 엔터프라이즈급 파이프라인에서는 Jenkins가 더 유연할 수 있습니다. 하지만 대부분의 프로젝트에는 GitHub Actions로 충분합니다.

핵심 개념 빠르게 훑어보기

GitHub Actions를 처음 배우면 몇 가지 개념이 헷갈리기 쉽습니다. 쉬운 말로 설명해 보겠습니다.

Workflow(워크플로): 전체 자동화 프로세스를 정의하는 YAML 파일입니다. 예를 들어 “main 브랜치에 push할 때마다 테스트 실행”이 하나의 워크플로입니다. 이 파일은 .github/workflows/ 디렉터리에 둡니다.

Job(작업): 워크플로 안에 있는 단계의 묶음입니다. 여러 Job은 병렬로 실행할 수도 있고 의존 관계를 설정할 수도 있습니다. 예를 들어 먼저 “테스트” Job을 실행하고 그다음 “배포” Job을 실행할 수 있습니다.

Step(단계): Job 안에서 순서대로 하나씩 실행하는 구체적인 작업입니다. 한 줄짜리 명령(npm test)을 실행할 수도 있고 다른 사람이 만든 Action(actions/checkout@v4)을 호출할 수도 있습니다.

Runner(러너): Job을 실행하는 가상 머신입니다. GitHub는 ubuntu-latest(Linux), windows-latest(Windows), macos-latest(macOS) 세 가지를 제공합니다. 자체 서버를 사용할 수도 있지만 대부분은 공식 Runner면 충분합니다.

비유하자면 Workflow는 대본 한 권이고, Job은 대본 속 여러 장면이며, Step은 각 장면의 구체적인 동작이고, Runner는 그 장면을 연기하는 배우입니다.

첫 번째 CI 워크플로 만들기

너무 오래 고민하지 말고 먼저 실행해 봅시다. 프로젝트 루트 디렉터리에 .github/workflows/ci.yml을 만들고 아래 코드를 붙여 넣으세요.

name: CI Pipeline  # 워크플로 이름이며 GitHub Actions 페이지에 표시됩니다

on:
  push:
    branches: [main]  # main 브랜치에 push할 때 트리거됩니다
  pull_request:
    branches: [main]  # main 브랜치를 대상으로 하는 PR에서 트리거됩니다

permissions:
  contents: read  # 최소 권한 원칙에 따라 읽기 권한만 부여합니다

jobs:
  build:
    runs-on: ubuntu-latest  # 최신 Ubuntu 환경을 사용합니다
    timeout-minutes: 15     # 멈춘 채 유지되지 않도록 제한 시간을 설정합니다

    steps:
      - name: Checkout code
        uses: actions/checkout@v4  # 코드를 가져옵니다

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20   # Node.js 20을 사용합니다
          cache: 'npm'       # npm 캐시를 활성화합니다

      - name: Install dependencies
        run: npm ci          # 의존성을 설치합니다. ci가 install보다 빠르고 안정적입니다

      - name: Run tests
        run: npm test        # 테스트를 실행합니다

      - name: Build
        run: npm run build   # 빌드합니다

이 코드는 무엇을 할까요?

첫 번째 on 부분은 트리거 조건을 정의합니다. main 브랜치에 push하거나 main 브랜치를 대상으로 PR을 만들면 실행됩니다. 두 번째 permissions 부분은 최소 권한 원칙에 따라 contents: read만 부여하여 워크플로가 의도치 않게 저장소를 수정하지 못하게 합니다. 세 번째 부분이 핵심입니다. build라는 Job 하나가 Ubuntu에서 실행되며 코드 가져오기, Node 설치, 의존성 설치, 테스트 실행, 빌드를 차례로 수행합니다.

이 코드를 커밋해 GitHub에 push한 다음 저장소의 Actions 페이지를 여세요. 초록색 작은 원이 돌아가는 모습이 보일 것입니다. Runner가 워크플로를 실행하고 있다는 뜻입니다. 몇 분 기다린 뒤 모두 초록색 체크로 바뀌었다면 첫 번째 CI 파이프라인이 성공한 것입니다.

빨간색 X가 표시되면 어떻게 해야 할까요? 클릭해서 로그를 확인하면 각 단계의 출력이 명확하게 나옵니다. 90%는 의존성 설치 실패나 테스트 자체의 문제이며 CI 설정과는 큰 관련이 없습니다.

2장: CI 파이프라인 핵심 설정

1장의 워크플로는 실행되지만 실제로 편리하게 사용하려면 조금 더 손봐야 합니다. 이번 장에서는 트리거, 권한 관리, 환경 변수, 의존성 캐시라는 네 가지 핵심 설정을 살펴봅니다. 이는 CI 파이프라인의 뼈대이며 제대로 설정하면 워크플로를 더 안전하고 효율적으로 만들 수 있습니다.

트리거: 언제 실행할 것인가

트리거는 워크플로를 언제 시작할지 결정합니다. 가장 많이 쓰는 두 가지는 pushpull_request입니다.

on:
  push:
    branches: [main, dev]    # main 또는 dev 브랜치에 push할 때 트리거됩니다
    paths:
      - 'src/**'             # src 디렉터리 아래 파일이 변경된 경우에만 트리거됩니다
      - 'package.json'       # package.json 변경(의존성 업데이트)도 트리거합니다
  pull_request:
    branches: [main]         # main을 대상으로 하는 PR에서 트리거됩니다

paths 필터는 특히 유용합니다. 예를 들어 프로젝트에 docs/ 문서 디렉터리가 있다면 문서 수정은 CI를 트리거할 필요가 없습니다. paths를 설정하면 코드가 변경될 때만 빌드가 실행되어 리소스와 시간을 모두 절약할 수 있습니다.

push와 PR 외에도 다음과 같은 트리거 방식이 있습니다.

schedule: cron 표현식을 사용하는 예약 작업입니다. 예를 들어 매일 새벽에 빌드를 실행할 수 있습니다.

on:
  schedule:
    - cron: '0 0 * * *'  # 매일 UTC 0시(베이징 시간 오전 8시)

저는 한 프로젝트에서 이 기능으로 의존성을 정기 점검합니다. 매일 npm outdated를 실행해 오래된 패키지를 찾아 자동으로 이메일 알림을 보냅니다.

workflow_dispatch: 수동 트리거입니다. 코드를 push하지 않고 빌드를 한 번 실행하고 싶을 때(예: 특정 설정 테스트) 유용합니다. Actions 페이지에 “Run workflow” 버튼이 표시되며 이 버튼을 누르면 수동으로 실행할 수 있습니다.

on:
  workflow_dispatch:  # 별도 설정 없이 수동으로 트리거합니다

권한 관리: 보안이 우선입니다

GitHub Actions는 기본적으로 워크플로에 GITHUB_TOKEN을 제공합니다. 이 token은 저장소를 읽고 쓸 수 있으며 PR을 만들거나 코드를 push할 수도 있습니다. 편리해 보이지만 위험도 있습니다. 워크플로가 악용되면 공격자가 저장소의 쓰기 권한을 얻을 수 있기 때문입니다.

2021년에 한 오픈 소스 프로젝트의 CI 워크플로가 악용된 보안 사고가 있었습니다. 공격자는 위조된 PR을 통해 악성 코드를 제출했습니다. 뼈아픈 교훈이었습니다. 그래서 현재 GitHub는 최소 권한을 명시적으로 선언하라는 원칙을 권장합니다.

permissions:
  contents: read         # 저장소 콘텐츠는 읽을 수 있지만 쓸 수 없습니다
  pull-requests: write   # PR을 만들어야 하는 경우에만 별도로 선언합니다

테스트와 빌드만 실행하는 일반적인 CI 파이프라인에는 contents: read면 충분합니다. 워크플로에서 Release를 게시하거나 PR에 댓글을 다는 작업이 필요하다면 필요한 권한만 추가하세요.

유용한 팁이 하나 있습니다. 저장소 설정에서 기본 권한을 “Read repository contents permission”으로 바꾸세요. 그러면 모든 워크플로에 기본적으로 읽기 권한만 주어지고 쓰기가 필요한 경우에만 별도로 선언하게 됩니다. 방어막을 하나 더 두면 위험을 한 단계 줄일 수 있습니다.

환경 변수: 계층별 관리

환경 변수는 workflow, job, step의 세 가지 수준으로 나뉩니다. 수준이 낮을수록 적용 범위는 작지만 상위 수준의 값을 덮어쓸 수 있습니다.

env:
  NODE_ENV: production     # workflow 수준이며 모든 job에서 사용할 수 있습니다
  CI: true                 # 많은 도구가 이 변수를 감지해 동작을 조정합니다

jobs:
  build:
    env:
      BUILD_TARGET: web    # job 수준이며 build job 안에서만 유효합니다

    steps:
      - name: Run custom script
        env:
          MY_VAR: hello    # step 수준이며 이 단계에서만 유효합니다
        run: echo $MY_VAR

왜 계층을 나눌까요? 프로젝트에 builddeploy라는 여러 Job이 있다고 가정해 봅시다. NODE_ENV는 두 Job에서 모두 필요하므로 workflow 수준에 둡니다. 하지만 BUILD_TARGET은 build Job에서만 사용하므로 job 수준에 두는 편이 더 명확합니다. 특정 단계에서만 임시 변수(예: 특정 스크립트의 매개변수)가 필요하다면 step 수준에 둡니다.

민감한 정보는 어떻게 처리할까요? 절대 YAML에 직접 작성하지 마세요. GitHub는 Secrets 기능을 제공합니다. 저장소 설정에서 키(예: API_KEY)를 추가하고 워크플로에서 ${{ secrets.API_KEY }}로 참조합니다. Secrets는 로그에서 자동으로 숨겨져 유출되지 않습니다.

steps:
  - name: Deploy to server
    env:
      SSH_KEY: ${{ secrets.SSH_KEY }}  # Secrets에서 참조합니다
    run: |
      echo "$SSH_KEY" > private.key
      ssh -i private.key user@server 'deploy.sh'

의존성 캐시: 빌드 속도 높이기

프로젝트에 의존성이 많다면(수백 개의 npm 패키지 등) CI를 실행할 때마다 처음부터 설치하느라 시간이 오래 걸립니다. 제 프로젝트 하나는 의존성 설치에 3분, 테스트에 1분이 걸려 의존성 설치가 전체 시간의 75%를 차지했습니다.

GitHub Actions는 설치된 의존성을 저장해 두었다가 다음 실행에서 바로 사용할 수 있는 캐시 기능을 제공합니다. 가장 간단한 방법은 setup-node에 내장된 캐시를 사용하는 것입니다.

- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'  # npm 의존성을 자동으로 캐시합니다

cache: 'npm' 한 줄만 추가하면 첫 실행에서는 정상적으로 설치하면서 node_modules를 캐시에 저장합니다. 두 번째 실행에서는 package-lock.json이 바뀌지 않았다면 캐시에서 바로 가져와 설치 시간이 3분에서 10초로 줄어듭니다.

pnpm이나 yarn을 사용한다면 cache: 'pnpm' 또는 cache: 'yarn'으로 바꾸세요.

더 세밀하게 캐시하려면 actions/cache를 사용할 수 있습니다.

- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: ~/.npm         # npm 전역 캐시 디렉터리
    key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

key는 캐시의 고유 식별자입니다. 여기서는 package-lock.json의 해시값을 사용합니다. 잠금 파일이 바뀌면 캐시가 무효화되어 다시 설치됩니다. restore-keys는 대체 전략입니다. 정확히 일치하는 캐시가 없다면 같은 유형의 캐시를 찾아 일부라도 먼저 복원합니다.

캐시 적중률이 높으면 빌드 속도가 상당히 빨라집니다. 제 프로젝트 하나에서 측정한 결과 캐시가 없을 때는 4분, 캐시가 있을 때는 1.5분이 걸렸습니다. 하루에 빌드를 20번 실행한다면 절약한 시간만으로 블로그 글 하나를 쓸 수 있을 정도입니다.

3장: Matrix 전략으로 병렬 테스트하기

Matrix는 제가 GitHub Actions에서 가장 좋아하는 기능이자 이 글의 핵심입니다. Matrix 전략을 사용하면 하나의 Job을 병렬로 실행되는 여러 Job으로 바꾸고 여러 버전과 운영체제를 동시에 테스트할 수 있습니다. 한 번 push하면 몇 초 안에 열 개가 넘는 빌드 작업이 시작되어 모두 병렬로 실행되므로 전체 시간이 직렬 실행보다 절반 이상 줄어듭니다.

Matrix란 무엇인가요?

프로젝트가 Node 16, 18, 20 세 버전에서 정상 동작하는지 테스트해야 한다고 가정해 봅시다. 기존 방식이라면 Job 세 개를 작성하거나 한 Job에서 버전을 차례로 바꿔 가며 테스트해야 합니다. 전자는 설정이 중복되고 후자는 시간이 오래 걸립니다.

Matrix는 표와 같습니다. 가로축은 Node 버전, 세로축은 운영체제이며 각 칸이 독립적인 테스트 작업입니다. GitHub Actions는 모든 조합을 자동으로 생성해 병렬로 실행합니다.

strategy:
  matrix:
    node: [16, 18, 20]
    os: [ubuntu-latest, windows-latest]

이 설정은 6개의 Job을 생성합니다. Node 16 + Ubuntu, Node 16 + Windows, Node 18 + Ubuntu 등의 조합입니다. 전체 실행 시간은 모든 Job 시간의 합이 아니라 가장 느린 Job 하나의 시간에 따라 결정됩니다.

버전 Matrix: 여러 Node 버전 테스트

제 프로젝트 하나에서 이런 문제가 있었습니다. 개발 환경은 Node 20이었는데 어느 날 사용자가 Node 18에서는 실행되지 않는다고 알려 왔습니다. 조사해 보니 특정 API가 Node 18에서 다르게 동작했습니다. 다중 버전 테스트를 일찍 도입했다면 이 버그가 출시될 일도 없었을 것입니다.

Matrix를 사용한 다중 버전 테스트는 간단합니다.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false    # 한 버전이 실패해도 다른 버전은 계속 실행합니다
      matrix:
        node-version: [16, 18, 20, 22]  # 네 가지 버전을 테스트합니다

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}  # matrix의 버전을 동적으로 사용합니다
          cache: 'npm'
      - run: npm ci
      - run: npm test

핵심 내용을 설명하겠습니다.

  • matrix.node-version은 테스트할 버전 목록을 정의합니다.
  • ${{ matrix.node-version }}은 steps에서 이 값을 참조하며 각 Job은 서로 다른 값을 받습니다.
  • fail-fast: false는 한 버전이 실패해도 다른 버전의 실행을 중단하지 않는다는 뜻입니다. 기본값은 true이므로 하나가 실패하면 모두 중지됩니다. 호환성 테스트에서는 모든 버전의 결과를 볼 수 있도록 이 옵션을 끄는 것이 좋습니다.

include와 exclude: 테스트할 필요가 없는 조합이 있거나 추가 설정이 필요한 경우 이 두 키워드를 사용할 수 있습니다.

strategy:
  matrix:
    node-version: [16, 18, 20]
    os: [ubuntu-latest, windows-latest]
    exclude:
      - node-version: 16      # Node 16 + Windows 조합은 테스트하지 않습니다
        os: windows-latest
    include:
      - node-version: 20      # Node 20을 macOS에서 한 번 더 실행합니다
        os: macos-latest

exclude는 필요 없는 조합을 제거하고 include는 추가 조합을 넣습니다. 테스트 범위를 유연하게 제어할 수 있습니다.

OS Matrix: 크로스 플랫폼 테스트

프로젝트가 여러 운영체제에서 실행될 수 있다면(예: 명령줄 도구) OS Matrix가 유용합니다.

jobs:
  test:
    runs-on: ${{ matrix.os }}  # 운영체제를 동적으로 지정합니다
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

여기서 주의할 점이 있습니다.

플랫폼 차이: Windows와 Linux는 파일 경로가 다르고(\/) 일부 명령줄 도구의 동작도 다릅니다. 크로스 플랫폼 테스트를 통해 이런 문제를 미리 발견할 수 있습니다.

비용 관리: GitHub Actions는 운영체제에 따라 사용량 계산 방식이 다릅니다. Linux는 매월 2,000분이 무료이고 Windows는 Linux의 2배, macOS는 10배의 사용량을 소모합니다. macOS Runner로 테스트하면 월간 사용량을 빠르게 소진할 수 있습니다.

비용 절감 전략은 다음과 같습니다.

  • 정말 macOS에서 사용해야 하는 프로젝트처럼 필요한 경우에만 macOS를 테스트합니다.
  • macOS 테스트는 별도 워크플로로 옮기고 workflow_dispatch로 수동 실행합니다.
  • 공개 저장소는 제한이 없으므로 프로젝트를 공개할 수 있다면 공개를 권장합니다.

성능 개선 팁

Matrix는 병렬 실행이 가능하지만 무제한은 아닙니다. GitHub는 리소스 고갈을 막기 위해 기본 병렬 실행 수를 제한합니다. 다음과 같이 직접 제어할 수 있습니다.

strategy:
  max-parallel: 4  # 동시에 최대 4개의 Job을 실행합니다
  matrix:
    node-version: [16, 18, 20, 22]

Matrix 조합이 많다면(예: Job 10개 이상) max-parallel을 설정해 한꺼번에 너무 많이 실행되는 것을 방지하고 무료 사용량 소모도 줄일 수 있습니다.

캐시 적중으로 속도 향상: Matrix의 각 Job에는 자체 캐시가 있습니다. setup-nodecache가 자동으로 처리하므로 추가 설정은 필요 없습니다. package-lock.jsonnode-version이 안정적으로 유지되면 캐시 적중률이 높습니다.

불필요한 단계 줄이기: Matrix의 모든 Job에서 실행되지만 사실은 반복할 필요가 없는 단계도 있습니다. 예를 들어 코드 검사(lint)는 보통 여러 버전에서 테스트할 필요가 없습니다.

jobs:
  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  # lint는 Node 20에서 한 번만 실행합니다

  test:
    needs: lint  # lint가 성공한 뒤 test를 실행합니다
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16, 18, 20]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

이렇게 하면 lint는 한 번만 실행하고 test는 세 버전에서 실행합니다. 효율이 더 높습니다.

실측 데이터: 제 프로젝트 하나에서는 Matrix를 사용하지 않고 3개 버전을 직렬로 테스트했을 때 12분이 걸렸습니다. Matrix로 병렬 실행하자 전체 시간은 가장 느린 버전을 기준으로 4분이었습니다. 8분을 아낀 셈입니다. 하루에 빌드를 10번 실행한다면 한 달 동안 절약한 시간으로 영화 한 편을 볼 수 있습니다.

4장: 실전 경험과 문제 해결

앞의 세 장에서는 CI 파이프라인 설정 방법을 살펴봤습니다. 이번 장에서는 보안, 성능, 문제 해결이라는 세 가지 관점에서 “빠른 참조 목록”을 정리했습니다. 문제가 생기면 바로 이 부분을 확인해 시간을 절약할 수 있습니다.

보안 실전 체크리스트

실천 항목설명예시
permissions 명시적 선언기본 권한에 의존하지 말고 필요한 권한을 명시합니다permissions: { contents: read }
민감한 정보는 Secrets에 저장API Key, SSH 키 등을 하드코딩하지 않습니다${{ secrets.API_KEY }}
트리거 브랜치 제한모든 브랜치가 아니라 필요한 브랜치에서만 CI를 실행합니다branches: [main]
SHA로 Action 참조변조를 막기 위해 버전 태그 대신 구체적인 commit SHA를 사용합니다actions/checkout@b4ffde65f46336ab88eb53be808477a39b6bc2b1
timeout 설정멈춘 작업이 사용량을 낭비하지 않게 합니다timeout-minutes: 15

마지막 항목은 많은 사람이 놓칩니다. 바로 Action 버전 참조입니다. 공식 Action에는 보통 업그레이드하기 편한 @v4 같은 태그를 사용합니다. 하지만 태그는 변경될 수 있습니다. 이론적으로 누군가 @v4가 악성 코드를 가리키게 만들 수도 있습니다. SHA(@b4ffde65f...)로 참조하면 업그레이드는 번거롭지만 더 안전합니다. 프로덕션 환경의 프로젝트라면 SHA 사용을 권장합니다.

성능 개선 체크리스트

효과설정 방법
의존성 캐시 활성화설치 시간 50% 이상 절약cache: 'npm'
install 대신 npm ci 사용더 빠르고 안정적인 설치run: npm ci
timeout-minutes 설정멈춘 작업의 사용량 낭비 방지timeout-minutes: 15
Matrix 병렬 실행전체 시간 60% 이상 단축strategy.matrix
concurrency로 중복 빌드 취소같은 브랜치에서는 최신 빌드만 실행concurrency.group: ${{ github.ref }}

concurrency는 매우 유용합니다. 같은 브랜치에 연속으로 5번 push하면 기본적으로 빌드 5개가 실행됩니다. concurrency를 설정하면 앞의 4개는 취소되고 마지막 빌드만 실행됩니다.

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # 실행 중인 이전 빌드를 취소합니다

자주 발생하는 문제 빠른 참조표

오류 메시지원인해결 방법
Permission denied권한 부족permissions 설정을 확인하고 필요한 권한을 추가합니다
Cache not found캐시 키 불일치cache key를 확인하고 package-lock.json이 바뀌지 않았는지 확인합니다
npm ERR! network네트워크 시간 초과제한 시간을 늘리거나 중국 내 미러를 사용합니다
Out of memoryNode 메모리 부족NODE_OPTIONS=--max_old_space_size=4096을 설정합니다
EACCES permission denied파일 권한 문제스크립트 시작 부분에 chmod +x script.sh를 추가합니다
Error: Cannot find module의존성 설치 미완료npm ci가 성공했는지 확인하고 오류 로그를 살펴봅니다

자주 발생하는 몇 가지 상황의 처리 방법은 다음과 같습니다.

네트워크 시간 초과: GitHub Runner가 npm registry에 연결할 때 느린 경우가 있습니다. .npmrc에 미러를 설정할 수 있습니다.

- name: Configure npm registry
  run: echo "registry=https://registry.npmmirror.com" > .npmrc

메모리 부족: 대규모 프로젝트를 빌드할 때 Node 메모리가 부족할 수 있습니다. 다음 환경 변수를 추가하세요.

env:
  NODE_OPTIONS: --max_old_space_size=4096  # Node에 4GB 메모리를 할당합니다

캐시가 적용되지 않음: 첫 실행에 캐시가 없는 것은 정상입니다. package-lock.json이 존재하는지 확인하세요(npm ci에는 잠금 파일이 필요합니다). 또한 setup-nodecache 매개변수가 사용하는 패키지 관리자(npm/pnpm/yarn)와 일치해야 합니다.

결론

내용이 많았지만 핵심은 몇 가지뿐입니다. YAML 파일 하나만으로 CI 파이프라인을 구축할 수 있고, 권한은 최소 권한 원칙에 따라 선언해야 합니다. 환경 변수는 세 계층으로 관리하며, 의존성 캐시는 설치 시간을 절반으로 줄일 수 있습니다. Matrix 전략을 사용하면 다중 버전 병렬 테스트도 간단해집니다.

1장의 워크플로 템플릿을 복사한 뒤 Node 버전과 프로젝트 명령만 바꾸면 프로젝트에 CI를 추가할 수 있습니다. 먼저 실행하고 차근차근 조정하세요. Node 버전을 두 개만 테스트하더라도 Matrix 전략을 사용해 보길 권합니다. 여러 개의 초록색 체크가 동시에 나타날 때 병렬 테스트의 효율을 실감할 수 있습니다. 꽤 만족스러운 순간입니다.

GitHub Actions를 사용하다 문제가 생기면 댓글로 남겨 주세요. 자주 발생하는 문제를 4장의 빠른 참조표에 추가해 더 많은 사람이 같은 시행착오를 피하도록 돕겠습니다.

GitHub Actions CI 파이프라인 구축하기

처음부터 완전한 CI 파이프라인을 구축해 빌드와 테스트를 자동화합니다.

⏱️ Estimated time: 30 min

  1. 1

    Step 1: 워크플로 디렉터리 만들기

    프로젝트 루트 디렉터리에 모든 워크플로 설정 파일을 보관할 `.github/workflows/` 디렉터리를 만듭니다.
  2. 2

    Step 2: 기본 CI 설정 작성하기

    `ci.yml` 파일을 만들고 트리거 조건(push/PR), 권한(최소 권한 원칙), Job 단계(checkout, setup-node, install, test, build)를 설정합니다.
  3. 3

    Step 3: 의존성 캐시 활성화하기

    `setup-node` 단계에 `cache: 'npm'` 매개변수를 추가해 npm 의존성을 자동으로 캐시하고 이후 빌드 속도를 높입니다.
  4. 4

    Step 4: Matrix 다중 버전 테스트 설정하기

    `strategy.matrix` 설정을 추가하고 테스트할 Node 버전 목록(예: [16, 18, 20])을 지정해 병렬 테스트를 구현합니다.
  5. 5

    Step 5: 커밋하고 빌드 결과 확인하기

    설정 파일을 커밋해 GitHub에 push한 뒤 Actions 페이지에서 빌드 상태와 로그를 확인합니다.

FAQ

GitHub Actions의 월간 무료 사용량은 얼마인가요?
비공개 저장소에는 매월 2,000분의 무료 사용량(Linux Runner 기준)이 제공되며 공개 저장소는 제한이 없습니다. Windows Runner는 Linux의 2배, macOS는 10배의 사용량을 소모합니다.
CI 워크플로는 어떤 브랜치에서 트리거해야 하나요?
main/master 브랜치와 main을 대상으로 하는 PR에서만 트리거하는 것이 좋습니다. 개발 브랜치에서는 CI를 건너뛰어 리소스를 절약할 수 있습니다. `paths` 필터로 문서 변경을 제외하세요.
npm install 대신 npm ci를 권장하는 이유는 무엇인가요?
npm ci는 더 빠르고 안정적입니다. package-lock.json에 따라 엄격하게 설치하고 잠금 파일을 수정하지 않으므로 CI 환경에 적합합니다. npm install은 의존성 버전을 업데이트해 빌드 결과가 불확실해질 수 있습니다.
Matrix 전략으로 빌드 시간을 얼마나 줄일 수 있나요?
실측 결과 Node 버전 3개를 테스트할 때 직렬 실행은 12분이 걸렸지만 Matrix 병렬 실행은 가장 느린 버전을 기준으로 4분만 걸려 시간을 60% 이상 줄였습니다.
캐시가 가끔 적용되지 않는 이유는 무엇인가요?
캐시는 `package-lock.json`의 해시값을 기준으로 합니다. 잠금 파일이 바뀌면 캐시는 무효화됩니다. 첫 실행에 캐시가 없는 것은 정상입니다. `cache` 매개변수가 패키지 관리자(npm/pnpm/yarn)와 일치하는지 확인하세요.
CI에서 민감한 정보(API Key, SSH 키)를 어떻게 사용하나요?
GitHub Secrets를 사용하세요. 저장소 설정에 키를 추가하고 워크플로에서 `${{ secrets.KEY_NAME }}`로 참조합니다. Secrets는 로그에서 자동으로 숨겨져 유출되지 않습니다.

4분 읽기 · 게시일: 2026년 4월 6일 · 수정일: 2026년 9월 4일

댓글

GitHub로 로그인하여 댓글을 남기세요

Easton BlogEaston Blog