Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
needs는 “대기 지점”이며, 늘어날수록 CI는 느려질 수 있다
GitHub Actions의 needs는 job 사이에 의존(대기 지점)을 만드는 키워드입니다. 편리하지만, needs를 쓸 때마다 CI 실행 시간이 늘어날 가능성이 있다는 게 이 글의 출발점입니다. needs는 “안전하게” 쪽으로 기울어 작성되기 쉬워서, 실제로는 병렬로 돌려도 되는 job까지 직렬로 묶어버리는 경우가 자주 발생합니다.
이 글은 needs를 중심으로 한 job 의존관계의 설계 패턴과 안티패턴을 표로 정리한 레퍼런스입니다. 기존 YAML이 어느 패턴에 해당하는지 비교하며 읽어보세요.
패턴 치트시트
| 패턴 | 효과 | 사용 시점 |
|---|---|---|
| 다이아몬드형 분기 | 병렬 실행으로 CI 시간 단축 | lint/test/typecheck가 서로 의존하지 않을 때 |
concurrency 그룹 | 오래된 실행을 자동 취소 | 같은 PR에 연속으로 push할 때 |
fail-fast: false | 하나의 실패가 나머지를 끌고 내려가지 않음 | matrix 실행에서 모든 조합의 결과를 보고 싶을 때 |
재사용 가능한 워크플로우(workflow_call) | 중복 정의 제거 | 여러 저장소·워크플로우에서 같은 job 순서를 쓸 때 |
if: always() 알림 job | 실패해도 확실히 알림 | Slack 알림, 상태 리포트 |
| 부분적 matrix 대기 | 일부 조합만 먼저 배포 | 카나리아식 선행 실행 |
아래에서 각각을 실제 예제로 살펴봅니다.
패턴 1: 다이아몬드형 분기로 직렬을 깨기
가장 흔한 안티패턴은 본래 독립적인 job을 일렬로 늘어세우는 것입니다.
# 안티패턴: lint -> unit-test -> typecheck -> build 가 전부 직렬
jobs:
lint:
runs-on: ubuntu-latest
unit-test:
needs: lint
typecheck:
needs: unit-test
build:
needs: typecheck
unit-test와 typecheck는 “코드가 존재한다” 이상의 전제를 공유하지 않습니다. lint의 완료만 기다리면 충분하다면, 둘 다 lint 바로 아래에 매달아 다이아몬드 형태로 만들 수 있습니다.
# 개선: lint 이후 unit-test와 typecheck가 병렬로 실행됨
jobs:
lint:
runs-on: ubuntu-latest
unit-test:
needs: lint
typecheck:
needs: lint
build:
needs: [unit-test, typecheck] # 둘 다 완료를 기다림
build의 needs를 배열로 만들면 “둘 다 완료되면 실행”을 표현할 수 있습니다. job 수는 같아도 의존 그래프의 가로 폭(병렬도)이 그대로 CI 시간 단축으로 이어집니다. GitHub Actions Visualizer에 워크플로우를 붙여넣으면 이런 “불필요한 일직선”이 그래프 모양에서 바로 드러납니다.
패턴 2: concurrency로 “이전 실행”을 취소하기
needs 설계를 최적화해도, 같은 브랜치에 연속으로 push해서 오래된 실행이 큐에 남아있으면 대기 시간은 그대로입니다. concurrency는 이 낭비를 없애줍니다.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
group에 github.ref(브랜치명이나 PR 번호)를 포함시키면 같은 브랜치에 새 push가 오면 오래된 실행을 자동으로 취소합니다. PR에 3번 연속 push하면, 의미 있는 마지막 1번만 끝까지 실행되는 것이 이상적인 동작입니다.
:::message
group에 job 이름이나 워크플로우 이름을 포함하지 않으면 의도치 않게 다른 워크플로우끼리 서로 취소하는 일이 생길 수 있습니다. ${{ github.workflow }}-${{ github.ref }}처럼 워크플로우 단위로 구분하는 것이 안전합니다.
:::
패턴 3: fail-fast를 상황에 맞게 쓰기
strategy.matrix를 쓰면 하나의 job 정의에서 여러 실행(Node 18/20/22 × OS 3종 등)이 전개됩니다. 여기서 fail-fast 설정이 CI 동작을 크게 좌우합니다.
strategy:
fail-fast: false # 기본값은 true
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest, macos-latest]
| 설정 | 동작 | 적합한 상황 |
|---|---|---|
fail-fast: true(기본) | 하나라도 실패하면 나머지를 즉시 취소 | 빠른 피드백 우선. 실패를 알았다면 더 이상의 정보는 필요 없음 |
fail-fast: false | 하나가 실패해도 나머지 전부를 끝까지 실행 | ”Node 20만 실패한 건가? OS도 관계있나?”를 한 번의 CI로 파악하고 싶을 때 |
신규 기능 릴리스 전처럼 모든 조합의 상태를 한 번에 알고 싶을 때는 false로 설정할 가치가 있습니다. 반대로 “일상적인 PR 체크는 무조건 빠르게”가 목표라면 기본값 true를 유지하며 빠른 실패 감지를 우선하세요.
패턴 4: matrix job을 needs로 기다릴 때의 함정
build가 test(matrix로 전개됨)에 의존할 때, needs: test라고만 써도 matrix의 모든 조합의 완료를 기다려줍니다. 여기까지는 직관적이지만 주의할 점이 두 가지 있습니다.
- 조합 하나만 실패해도
needs쪽도 스킵됩니다(fail-fast: true를 함께 쓰면 특히, 중간 결과만으로 후속이 멈출 수 있음) - 일부 조합만 먼저 진행하고 싶을 때(예: Linux만 먼저 배포하고 Windows/macOS 결과는 나중에 확인)
needs만으로는 표현할 수 없고, job 자체를 분할해야 합니다
# Linux 선행 배포를 위해 job을 나누는 예
jobs:
test-linux:
runs-on: ubuntu-latest
test-other-os:
strategy:
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
deploy:
needs: test-linux # Windows/macOS 결과를 기다리지 않음
“전부 기다릴지, 일부만 기다릴지”는 needs의 작성법이 아니라 job을 어떻게 나누는지로 결정된다는 것이 이 패턴의 핵심입니다.
패턴 5: 재사용 가능한 워크플로우로 의존 그래프째 공유하기
여러 워크플로우(또는 여러 저장소)에서 같은 job 순서를 재사용한다면, workflow_call로 호출 대상을 한 곳에 모을 수 있습니다.
# .github/workflows/ci.yml (호출하는 쪽)
jobs:
call-shared-ci:
uses: ./.github/workflows/shared-test-suite.yml
with:
node-version: '20'
호출되는 쪽(shared-test-suite.yml)의 needs 구조가 통째로 재사용되므로, “lint와 test의 의존관계”를 저장소 간에 복사-붙여넣기 하지 않아도 됩니다. 의존 그래프 설계를 패턴화했다면, 다음은 이 방법으로 설계 자체를 부품화하는 마무리 단계입니다.
알림 job은 needs 바깥에서 “반드시 실행”되도록 설계하기
실패 감지 알림 job은 needs의 의존관계 특성상 의존 대상이 실패하면 자동으로 스킵됩니다. 알림만은 예외적으로 실행하고 싶으므로 if: always()를 명시합니다.
notify:
needs: [lint, test, build]
if: always() # 의존 job의 성공/실패와 무관하게 항상 실행
runs-on: ubuntu-latest
steps:
- run: echo "Result: ${{ needs.build.result }}"
needs.<job>.result로 각 job의 결과(success / failure / cancelled / skipped)를 참조할 수 있으므로, “무엇이 실패했는지” 포함한 알림 문구를 만들 수 있습니다.
의존 그래프 점검 절차
- 워크플로우 YAML을 GitHub Actions Visualizer에 붙여넣어 의존 그래프를 시각화한다
- 일직선으로 이어진 부분을 찾아 정말로 순서가 필요한지 확인한다(패턴 1)
- 연속 push로 인한 중복 실행이 신경 쓰인다면
concurrency를 추가한다(패턴 2) - matrix를 쓰는 job은
fail-fast값이 목적에 맞는지 확인한다(패턴 3) - 여러 워크플로우에서 같은 의존 구조가 반복된다면 재사용 가능한 워크플로우로의 분리를 검토한다(패턴 5)
CI 실행 시간은 스텝 내용을 줄이는 것보다 의존 그래프의 모양을 고치는 쪽이 효과가 큰 경우가 적지 않습니다. 워크플로우를 한 번 그림으로 그려보고 불필요한 직렬화가 없는지 확인해보세요.
자주 묻는 질문
needs를 늘리면 CI가 반드시 느려지나요?
needs 자체가 직접 느리게 만드는 것이 아니라, **“기다릴 필요 없는 의존을 써버리는 것”**이 문제입니다. 정말로 전제가 되는 산출물(빌드된 아티팩트 등)을 기다리는 needs는 필요한 의존이므로 없애면 안 됩니다. 점검해야 할 것은 “lint의 완료만 기다리면 충분한데 이전 job의 완료까지 기다리는” 것 같은 불필요한 연쇄입니다.
concurrency의 cancel-in-progress는 항상 true로 해야 하나요?
PR에 연속으로 push되는 CI에서는 기본적으로 유효합니다. 다만 main 브랜치 병합 후 배포 워크플로우에서는 취소되면 배포가 어중간한 상태로 멈출 위험이 있으므로, cancel-in-progress: false로 하거나 애초에 배포 워크플로우에는 concurrency를 붙이지 않는 것도 고려하세요.
fail-fast를 false로 하면 CI 시간이 늘어나나요?
늘어날 수 있습니다. fail-fast: true(기본값)는 첫 실패에서 나머지 job을 중단하므로 평균 CI 시간이 짧아지는 경향이 있습니다. false는 모든 조합의 결과를 알고 싶은 상황(릴리스 전 전체 환경 확인 등)에 한정해서 쓰는 것이 실무적인 균형입니다.
의존관계 점검에 사용한 워크플로우 YAML은 외부로 전송되나요?
아니요. GitHub Actions Visualizer는 모든 처리를 브라우저 안에서 하므로, 사내 워크플로우 정의를 붙여넣어도 서버로 전송되지 않습니다.