GitHub Actions Matrix 빌드: 여러 버전 병렬 테스트 실전

지난주 프로젝트를 출시했는데, 오후가 채 지나기도 전에 Node 16 환경에서 페이지가 하얗게 나온다는 사용자 제보를 받았습니다. 순간 머리가 멍해졌습니다.
꼬박 두 시간 동안 원인을 추적했습니다. 로그를 샅샅이 살펴보고 코드를 세 차례 비교한 끝에, 특정 API가 Node 16에서 JSON.stringify()를 처리하는 방식이 Node 20과 다르다는 사실을 발견했습니다. 이전 버전에서는 순환 참조를 만나면 바로 오류를 던졌지만, 새 버전에서는 조용히 처리했습니다. CI 파이프라인이 Node 20만 테스트했기 때문에 이 호환성 문제를 전혀 걸러내지 못했습니다.
사후 회고를 하면서 이런 생각이 들었습니다. 처음부터 여러 버전 병렬 테스트를 구성했다면 출시 전에 문제를 발견할 수 있었을 텐데 말입니다. 그 일을 계기로 GitHub Actions의 Matrix 빌드를 본격적으로 살펴보기 시작했습니다. 이름만 들으면 복잡해 보이지만, 쉽게 말해 하나의 작업을 여러 개로 자동 분할해 서로 다른 버전과 플랫폼에서 동시에 실행하는 기능입니다.
이 글에서는 Matrix의 기초부터 고급 활용까지 차근차근 다룹니다. 기본 문법, exclude/include를 이용한 조합 필터링, fail-fast 전략 선택법, max-parallel 리소스 제어까지 살펴봅니다. 마지막에는 그대로 복사해 쓸 수 있는 완성형 Node.js 여러 버전 테스트 템플릿도 제공합니다. 약 10분이면 모두 읽을 수 있고, 읽은 뒤 바로 CI를 여러 버전 병렬 실행 방식으로 바꿀 수 있습니다.
Matrix 기초 — 5분 만에 시작하기
Matrix는 한 문장으로 설명할 수 있습니다. 하나의 job을 작성하면 GitHub Actions가 여러 병렬 작업으로 자동 확장해 줍니다.
예를 들어 Node.js 버전 세 개 [18, 20, 22]를 정의하면 Matrix는 Node 18, Node 20, Node 22 환경에서 각각 실행되는 독립적인 테스트 작업 세 개를 만듭니다. 세 작업은 동시에 시작해 서로 간섭하지 않고 실행됩니다.
가장 간단한 구성은 다음과 같습니다.
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 && npm test
핵심은 strategy.matrix 부분입니다. node-version은 직접 정의한 변수명이고, 뒤의 배열 [18, 20, 22]는 이 변수에 들어갈 수 있는 값입니다. GitHub Actions는 배열을 순회하면서 매번 값을 하나씩 matrix.node-version에 할당하고, 그에 대응하는 job 인스턴스를 만듭니다.
${{ matrix.node-version }} 문법은 현재 값을 참조합니다. 처음 실행할 때는 18, 두 번째는 20, 세 번째는 22입니다.
처음 사용할 때 궁금했던 점이 하나 있었습니다. 이 세 작업은 직렬로 실행될까요, 병렬로 실행될까요? 정답은 기본적으로 병렬 실행입니다. 코드를 한 번 push하면 GitHub가 Runner 세 개를 동시에 시작해 세 작업을 함께 실행합니다. 실제로 세 버전을 직렬로 테스트할 때는 15분이 걸렸지만 Matrix로 바꾼 뒤에는 5분이면 끝났습니다. 세 작업이 동시에 실행되므로 가장 느린 작업의 실행 시간만큼으로 전체 시간이 줄어든 것입니다.
다만 주의할 점이 있습니다. 병렬 실행은 Runner 사용 시간을 더 많이 소모합니다. 작업이 세 개라면 사용 시간도 세 배입니다. 무료 한도(월 2,000분)를 사용한다면 큰 Matrix가 한도를 빠르게 소진할 수 있습니다. 이 부분은 뒤에서 max-parallel을 다룰 때 자세히 설명하겠습니다.
exclude/include 필터링 — 테스트 Matrix 세밀하게 제어하기
여러 차원을 조합하기 시작하면 Matrix의 조합 수는 기하급수적으로 늘어납니다.
예를 들어 Node 버전 세 개 [16, 18, 20]와 운영체제 세 개 [ubuntu, windows, macos]를 조합하면 3 x 3 = 9개 작업이 됩니다. 여기에 테스트 스위트 [unit, integration, e2e]까지 추가하면 27개입니다. 오픈 소스 프로젝트라면 감당할 만할 수도 있지만, 소규모 팀에게 Runner 사용 시간은 곧 비용입니다.
게다가 일부 조합은 애초에 의미가 없습니다. Node 16은 이미 EOL(End of Life)이므로 Windows와 macOS에서 테스트하는 것은 시간 낭비일 수 있습니다. 이럴 때 exclude로 해당 조합을 제거하면 됩니다.
strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]
exclude:
- node-version: 16
os: windows-latest
- node-version: 16
os: macos-latest
exclude에는 제외할 조합을 나열하면 됩니다. 위 구성은 Node 16 + Windows와 Node 16 + macOS 조합을 제거합니다. 원래 9개였던 작업이 7개로 줄어 Runner 사용 시간을 곧바로 22% 절약할 수 있습니다.
include는 반대로 특정 조합이나 추가 변수를 더합니다. 예를 들어 Node 23 실험 버전 테스트를 추가하되 Ubuntu에서만 실행하고 싶다고 해보겠습니다.
strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest]
include:
- node-version: 23
os: ubuntu-latest
experimental: true
여기에는 한 가지 세부 사항이 있습니다. include는 조합을 추가할 뿐 아니라 특정 조합에 추가 변수를 설정할 수도 있습니다. 위의 experimental: true는 Node 23 작업에만 존재합니다. 이후 단계에서 이 변수를 확인해, 예를 들면 실험 버전이 실패하더라도 전체 workflow를 중단하지 않도록 만들 수 있습니다.
- name: Run tests
run: npm test
continue-on-error: ${{ matrix.experimental == true }}
제가 겪었던 함정 중 하나는 exclude와 include의 우선순위였습니다. GitHub Actions는 먼저 include로 조합을 추가한 다음 exclude로 제거합니다. 따라서 include한 조합을 exclude에서도 제외하면 해당 조합은 생성되지 않습니다. 순서를 반대로 이해하면 결과가 예상과 달라질 수 있습니다.
두 기능의 용도를 정리하면 다음과 같습니다.
exclude: 불필요한 조합을 제거해 비용과 시간을 절약합니다include: 특수한 조합을 추가하고, 차별화된 처리를 위한 추가 변수도 설정할 수 있습니다
fail-fast와 max-parallel — 병렬 실행 전략 최적화
Matrix에는 놓치기 쉬운 기본 동작이 하나 있습니다. 바로 fail-fast: true입니다.
무슨 뜻일까요? Matrix 안의 작업 하나라도 실패하면 GitHub Actions가 아직 실행 중인 다른 작업을 즉시 취소한다는 뜻입니다. 예를 들어 10개 작업을 병렬로 실행하는데 세 번째 작업이 1분 만에 실패하면 나머지 7개 작업은 곧바로 중단됩니다.
이 동작이 좋을까요? 상황에 따라 다릅니다.
PR 검사에서는 fail-fast가 유용합니다. 누군가 코드를 제출했는데 Node 18 테스트가 실패했다면 다른 버전이 끝날 때까지 기다릴 필요가 없습니다. 작성자에게 바로 피드백해 빠르게 수정하도록 할 수 있습니다. 시간과 리소스를 모두 아낄 수 있습니다.
하지만 Nightly 테스트나 정기 회귀 테스트에서는 fail-fast가 적합하지 않을 수 있습니다. 이때 필요한 것은 어떤 버전에 문제가 있고 어떤 버전은 정상인지 보여 주는 전체 테스트 보고서입니다. Node 18이 실패했다고 다른 작업까지 멈추면 Node 20에도 같은 문제가 있는지 알 수 없습니다. 이런 경우에는 fail-fast: false로 설정해야 합니다.
strategy:
fail-fast: false
matrix:
node-version: [16, 18, 20]
max-parallel은 동시에 실행할 작업 수를 제어합니다. 기본적으로 제한이 없어 GitHub는 가능한 한 모든 작업을 동시에 시작합니다. 하지만 조합이 30개에 이르는 큰 Matrix라면 Runner 리소스를 한꺼번에 모두 사용하고 싶지 않을 수 있습니다.
strategy:
fail-fast: true
max-parallel: 6
matrix:
node-version: [16, 18, 20, 22]
test-suite: [unit, integration, e2e]
위 구성은 동시에 실행되는 작업을 최대 6개로 제한합니다. 30개 조합을 6개씩 나눠 실행하는 셈입니다. Runner 리소스를 제어하고 한도를 한꺼번에 소진하지 않는다는 장점이 있지만, 전체 실행 시간이 길어진다는 단점도 있습니다.
상황에 맞는 값을 선택할 수 있도록 간단한 결정표로 정리했습니다.
| 상황 | fail-fast | max-parallel | 이유 |
|---|---|---|---|
| PR 검사 | true | 제한 없음 | 빠르게 피드백하고 하나가 실패하면 멈춰 시간을 절약 |
| Nightly 테스트 | false | 4-6 | 전체 문제 보고서를 수집하고 모든 bug를 파악 |
| 큰 Matrix(>20개 조합) | true | 4 | 리소스 사용량을 제한하고 한도 소진 방지 |
| 실험 버전 테스트 | false | 제한 없음 | 실험 버전 실패가 전체 판단에 영향을 주지 않음 |
솔직히 대부분의 상황에서는 기본값인 fail-fast: true로 충분합니다. 전체 진단이 필요할 때만 false로 바꾸면 됩니다. max-parallel은 작은 Matrix(10개 이하)에서는 영향이 크지 않으며, 큰 Matrix일 때 신중히 고려하면 됩니다.
한 가지 더 알아둘 점이 있습니다. max-parallel은 GitHub Actions가 동시에 시작하는 작업 수만 제한하며 Runner 수를 제한하는 것은 아닙니다. self-hosted runner(자체 호스팅 Runner)를 사용한다면 값을 너무 작게 설정할 경우 오히려 대기열이 생겨 전체 시간이 길어질 수 있습니다. 이 설정은 공용 Runner를 사용할 때 더 의미가 있습니다.
완성형 실전 템플릿 — Node.js 여러 버전 병렬 테스트 파이프라인
앞에서는 개별 개념을 살펴봤습니다. 이번에는 그대로 복사해서 사용할 수 있는 완성형 구성 템플릿을 제공합니다.
이 템플릿에는 다음 항목이 포함됩니다.
- Node 버전 3개(16, 18, 20)
- 테스트 2종(unit, integration)
- 운영체제 2종(Ubuntu, Windows)
- 의존성 설치 속도를 높이는 자동 캐시
- Node 16의 Windows 테스트 제외(EOL 버전)
name: Multi-Version Test Matrix
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 6
matrix:
node-version: [16, 18, 20]
test-suite: [unit, integration]
os: [ubuntu-latest, windows-latest]
exclude:
- node-version: 16
os: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ${{ matrix.test-suite }} tests
run: npm run test:${{ matrix.test-suite }}
몇 가지 핵심 부분을 살펴보겠습니다.
runs-on: ${{ matrix.os }}: 운영체제도 동적으로 결정됩니다. 각 작업은 Matrix 조합에 따라 해당 Runner를 선택합니다.
cache: 'npm': setup-node에 내장된 캐시 기능입니다. package-lock.json의 hash를 기준으로 npm 의존성을 캐시해 두었다가 두 번째 실행부터는 다시 다운로드하지 않고 그대로 사용합니다. 실제로 이 캐시를 사용하면 의존성 설치 시간을 50% 이상 줄일 수 있었습니다.
fail-fast: false: 여기서는 여러 버전을 테스트해 모든 문제를 찾는 것이 목적이므로 의도적으로 false로 설정했습니다. 한 버전이 실패해도 다른 버전은 끝까지 계속 실행됩니다.
npm run test:${{ matrix.test-suite }}: package.json에 test:unit과 test:integration 명령 두 개가 정의되어 있다고 가정합니다. Matrix는 각각의 명령을 호출합니다.
이 구성은 총 몇 개의 작업을 만들까요?
버전 3개 x 테스트 2개 x 운영체제 2개 = 작업 12개입니다. 여기서 제외한 Node 16 + Windows 조합(테스트 스위트 2개)을 빼면 10개가 남습니다.
실측 데이터도 공유하겠습니다. 이 구성으로 몇 개 프로젝트를 실행해 보니, 캐시까지 함께 사용했을 때 CI 시간이 기존 직렬 실행 25분에서 약 8분으로 줄었습니다. 시간 단축 효과는 주로 병렬 실행과 의존성 캐시 두 부분에서 나왔습니다.
프로젝트가 더 크고 조합이 더 많다면 다음 방법을 고려할 수 있습니다.
max-parallel상한 늘리기(예: 8 또는 10)- e2e 테스트를 별도 job으로 분리해 전체 실행이 느려지는 문제 방지
continue-on-error로 실험 버전의 실패 처리
이 템플릿을 .github/workflows/test.yml에 복사한 뒤 실제 상황에 맞게 버전 번호와 테스트 스위트 이름을 조정하면 바로 실행할 수 있습니다.
결론
지금까지의 핵심을 정리해 보겠습니다.
Matrix는 본질적으로 하나의 job을 여러 병렬 작업으로 자동 확장합니다. 구성은 간단하고 효과는 분명합니다. 한 번 push하면 세 버전이 동시에 실행되고, CI 시간은 가장 느린 작업의 실행 시간만큼으로 줄어듭니다.
exclude/include는 세밀한 제어 수단입니다. 조합이 너무 많을 때 exclude로 불필요한 작업을 제거하면 Runner 사용 시간을 20% 이상 곧바로 절약할 수 있습니다. include는 특수한 조합을 추가하고 차별화된 처리를 위한 추가 변수도 설정할 수 있습니다.
fail-fast의 기본값은 true이며, 작업 하나가 실패하면 나머지도 멈춥니다. PR 검사에서는 기본값을 사용하고, Nightly 테스트에서는 전체 보고서를 얻기 위해 false로 바꾸면 됩니다. max-parallel은 최대 동시 실행 수를 제어하므로 큰 Matrix에서 주로 신경 쓰면 됩니다.
캐시는 필수입니다. setup-node의 내장 cache 기능은 코드 한 줄만 추가해도 의존성 설치 시간을 절반으로 줄일 수 있습니다.
다음 단계로 위의 완성형 템플릿을 프로젝트의 .github/workflows/ 디렉터리에 복사하고, 먼저 Node.js 세 버전 테스트를 실행해 보세요. 정상적으로 동작하는 것을 확인한 뒤 여러 플랫폼과 테스트 스위트로 점차 확장하면 됩니다. 캐시 구성에 문제가 생긴다면 시리즈 글 《GitHub Actions 캐시 전략: CI/CD 파이프라인 5배 가속하기》에서 더 자세한 팁을 확인할 수 있습니다.
여러 버전 테스트는 일찍 구성할수록 좋습니다. 출시 후 문제가 생기고 나서 후회하지 마세요. 그 하얀 화면 bug는 지금 생각해도 머리가 아픕니다.
GitHub Actions Matrix 여러 버전 테스트 구성하기
Node.js 16/18/20 버전과 Ubuntu/Windows 플랫폼을 아우르는 여러 버전 병렬 테스트 파이프라인을 처음부터 구축합니다
⏱️ Estimated time: 15 min
- 1
Step 1: Workflow 파일 만들기
프로젝트 루트 디렉터리에 `.github/workflows/test.yml` 파일을 만듭니다.
• 디렉터리 구조가 `.github/workflows/`로 올바른지 확인합니다
• 파일명은 자유롭게 정할 수 있으며 `test.yml` 또는 `ci.yml`을 권장합니다 - 2
Step 2: 트리거 조건 구성하기
테스트 실행 시점을 정의합니다.
```yaml
on:
push:
branches: [main]
pull_request:
```
• main 브랜치에 push하면 실행됩니다
• PR을 생성하거나 업데이트하면 실행됩니다 - 3
Step 3: Matrix 정의하기
버전, 플랫폼, 테스트 스위트를 구성합니다.
```yaml
strategy:
fail-fast: false
max-parallel: 6
matrix:
node-version: [16, 18, 20]
test-suite: [unit, integration]
os: [ubuntu-latest, windows-latest]
exclude:
- node-version: 16
os: windows-latest
```
• fail-fast: false로 모든 테스트 결과를 수집합니다
• max-parallel: 6으로 최대 동시 실행 수를 제어합니다
• exclude로 불필요한 조합을 제외합니다 - 4
Step 4: 테스트 단계 구성하기
구체적인 테스트 실행 단계를 정의합니다.
```yaml
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:${{ matrix.test-suite }}
```
• cache: 'npm'으로 의존성 캐시를 활성화합니다
• matrix 변수를 사용해 버전을 동적으로 구성합니다 - 5
Step 5: 커밋하고 검증하기
코드를 push해 테스트를 실행합니다.
• main 브랜치에 커밋하거나 PR을 생성합니다
• GitHub Actions 페이지에서 병렬 작업의 실행 상태를 확인합니다
• 각 버전의 테스트 결과가 정상인지 확인합니다
FAQ
Matrix 빌드는 Runner 사용 시간을 더 많이 소모하나요?
fail-fast는 true와 false 중 무엇으로 설정해야 하나요?
• PR 검사: true 권장(빠르게 실패하고 즉시 피드백)
• Nightly 테스트: false 권장(전체 보고서 수집)
• 실험 버전: false 권장(전체 판단에 미치는 영향 방지)
기본값은 true이며, 대부분의 PR 상황에서는 충분합니다.
exclude와 include 중 무엇이 먼저 적용되나요?
max-parallel은 어느 정도로 설정하는 것이 좋나요?
• 작은 Matrix(<10개 조합): 설정하지 않고 기본값 사용
• 중간 Matrix(10~20개 조합): 6~8로 설정
• 큰 Matrix(>20개 조합): 4~6으로 설정
값이 너무 작으면 전체 대기 시간이 길어지고, 너무 크면 Runner 리소스를 한꺼번에 소진할 수 있습니다.
Matrix에서 캐시를 사용해 속도를 높이려면 어떻게 해야 하나요?
Matrix는 어떤 변수 유형을 지원하나요?
• 배열: `[18, 20, 22]`
• 객체 배열: `[{name: 'a', value: 1}, {name: 'b', value: 2}]`
• 문자열: include를 사용해 추가해야 합니다
가독성이 더 좋은 배열과 객체 배열을 권장합니다.
2분 읽기 · 게시일: 2026년 4월 8일 · 수정일: 2026년 9월 8일
GitHub Actions 완전 가이드
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
GitHub Actions 캐시 전략: CI/CD 파이프라인을 5배 빠르게 만드는 법
GitHub Actions 캐시 전략 실전 가이드입니다. npm부터 Docker까지의 전체 설정 예시, 캐시 키 설계 모범 사례, 성능 최적화 데이터 비교를 다룹니다. 캐시 메커니즘을 익혀 CI/CD 파이프라인을 5배 빠르게 만들고 빌드 비용을 절감해 보세요.
8편 중 3편
다음
GitHub Actions Matrix로 여러 플랫폼과 버전을 병렬 테스트하는 방법
GitHub Actions Matrix의 기본 문법부터 exclude/include, fail-fast, max-parallel까지 설명하고, 여러 플랫폼과 버전을 병렬 테스트할 수 있는 실무용 workflow 템플릿 5개를 제공합니다.
8편 중 5편



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