본문으로 건너뛰기
Previous
Next
GitHub Actions로 Node.js CI/CD 파이프라인 만들기 | 테스트·빌드·Docker·배포

GitHub Actions로 Node.js CI/CD 파이프라인 만들기 | 테스트·빌드·Docker·배포

GitHub Actions로 Node.js CI/CD 파이프라인 만들기 | 테스트·빌드·Docker·배포

이 글의 핵심

GitHub Actions Node.js 배포 방법: 테스트→빌드→Docker 이미지 푸시→스테이징·프로덕션 배포까지 한 워크플로로 정리합니다. 캐시·시크릿·재사용 워크플로 포함.

들어가며

GitHub Actions는 저장소 이벤트(push, PR, schedule 등)에 반응해 테스트·빌드·배포를 자동화하는 데 널리 쓰입니다. Node.js 서비스는 패키지 설치·린트·유닛 테스트·프런트 빌드·Docker 이미지 생성·레지스트리 푸시·런타임 배포까지 단계가 길어, 이를 한 파이프라인으로 묶어두면 휴먼 에러배포 시간을 동시에 줄일 수 있습니다. 프로덕션에서는 브랜치 보호, 시크릿 관리, 캐시 전략, 실패 시 롤백이 CI/CD 품질을 가릅니다. 이 글은 “로컬에서 되던 것”을 재현 가능한 워크플로 YAML로 옮기는 데 초점을 둡니다. 같은 GitHub Actions 패턴은 언어를 넘어 확장됩니다. 테스트 단계는 Node.js Jest·Python pytest·매트릭스 예제·Go go test와 비교하고, C++ 멀티 OS·캐시는 C++ GitHub Actions를, 배포는 Docker Compose·minikube·C++ Docker와 이어서 읽으면 그림이 맞습니다. 비유로 말씀드리면, CI/CD 파이프라인은 공장 라인에 가깝습니다—원료(clone) → 가공(install·lint·test·build) → 출고(image push) → 납품(deploy) 순서가 문서화되어 있어야 동일 품질이 반복됩니다.

이 글을 읽으면

  • 테스트 → 빌드 → Docker 이미지 → 배포까지 한 흐름으로 설계하는 방법을 익히실 수 있습니다
  • actions/cache, 멀티 스테이지 빌드, 환경별 job 분리 패턴을 배우실 수 있습니다
  • 흔한 실패 원인(권한, 태그, 레지스트리 인증)과 디버깅 팁정리해 두었습니다

실전 경험에서 배운 교훈

이 기술을 실무 프로젝트에 처음 도입했을 때, 공식 문서만으로는 알 수 없었던 많은 함정들이 있었습니다. 특히 프로덕션 환경에서 발생하는 엣지 케이스들은 로컬 개발 환경에서는 재현조차 되지 않았죠.

가장 어려웠던 점은 성능 최적화였습니다. 처음엔 “동작만 하면 되겠지”라고 생각했지만, 실제 사용자 트래픽이 몰리면서 병목 지점들이 하나씩 드러났습니다. 특히 데이터베이스 쿼리 최적화, 캐싱 전략, 에러 핸들링 구조 등은 여러 번의 장애를 겪으면서 개선해 나갔습니다.

이 글에서는 그런 시행착오를 통해 얻은 실전 노하우와, “이렇게 하면 안 된다”는 교훈들을 함께 정리했습니다. 특히 트러블슈팅 섹션은 실제 장애 대응 경험을 바탕으로 작성했으니, 비슷한 문제를 마주했을 때 참고하시면 도움이 될 것입니다.

개념: CI/CD와 GitHub Actions

기본 개념

  • CI(Continuous Integration): 코드 병합 시마다 자동으로 설치·빌드·테스트해 통합 시점의 문제를 빨리 드러냅니다.
  • CD(Continuous Delivery/Deployment): 검증된 산출물을 스테이징·프로덕션으로 자동(또는 승인 후) 배포합니다.
  • Workflow: YAML로 정의된 이벤트 → 잡(job) → 스텝(step) 의 실행 그래프입니다.
  • Runner: GitHub 호스티드 러너(ubuntu-latest 등) 또는 셀프호스티드 머신에서 잡이 돕니다.

왜 Node.js에 특히 필요한가

패키지 생태계는 lockfile, Node 버전, 네이티브 모듈에 민감합니다. 동일한 package-lock.json과 동일한 Node 버전으로 CI에서 돌리지 않으면 “내 PC에서는 됨”이 반복됩니다. Docker까지 포함하면 런타임 OS·의존성까지 고정할 수 있습니다.

실전: 단일 워크플로 템플릿

아래는 PR에서는 테스트만, main 병합 시 이미지 빌드·레지스트리 푸시·배포(SSH 예시)까지 이어지는 예시입니다. 프로젝트 루트에 .github/workflows/ci-cd.yml로 둡니다.

# .github/workflows/ci-cd.yml
name: CI/CD Node.js
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  workflow_dispatch: {}
env:
  NODE_VERSION: '22'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}
jobs:
  test:
    name: Lint & Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'
      - name: Install dependencies
        run: npm ci
      - name: Lint
        run: npm run lint --if-present
      - name: Unit tests
        run: npm test --if-present
      - name: Build (if applicable)
        run: npm run build --if-present
  build-and-push:
    name: Docker Build & Push
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=raw,value=latest,enable={{is_default_branch}}
      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
  deploy:
    name: Deploy (example SSH)
    needs: build-and-push
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy via SSH
        uses: appleboy/[email protected]
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            cd /opt/app && docker compose up -d --no-deps api

동작 요약

  • on.pull_request / on.push: 이벤트·브랜치에 맞춰 워크플로가 돕니다. PR과 main 푸시 모두에서 test 잡이 실행됩니다.
  • workflow_dispatch: 수동 실행 버튼을 열어 배포·재시도에 씁니다.
  • jobs.testbuild-and-pushdeploy 순: needs: test테스트 통과 후에만 빌드가 이어집니다.
  • build-and-push.if: main 브랜치에 push일 때만 이미지를 만들어 PR에선 레지스트리 푸시를 생략합니다.
  • permissions.packages: write: 기본 GITHUB_TOKEN으로 GHCR 푸시가 가능하도록 씁니다.
  • docker/metadata-actiontags: 커밋 SHA 태그latest를 동시에 쓰면 추적·롤백이 쉬워집니다.
  • deploy 잡은 예시로 SSH 후 docker compose up -d --no-deps apiAPI만 재기동합니다(-d는 백그라운드, --no-deps의존 서비스(Postgres 등)는 건드리지 않음). 실제로는 Kubernetes kubectl, PaaS CLI 등으로 바꿉니다.

실행 가능한 최소 앱 가정

package.json에 다음 스크립트가 있다고 가정합니다(없으면 해당 step을 제거하세요).

{
  "scripts": {
    "lint": "eslint .",
    "test": "node --test",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}

Dockerfile 예시 (멀티 스테이지)

# Dockerfile
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
  • deps 스테이지: npm ci --omit=dev프로덕션 의존성만 깔아 최종 이미지에 넣습니다.
  • build 스테이지: devDependencies 포함으로 tsc 등 빌드 도구를 쓰고, 산출물은 dist에만 남깁니다.
  • runner: 런타임에 필요한 파일·모듈만 복사해 이미지 크기와 공격 면을 줄입니다.

고급: 재사용 워크플로·매트릭·캐시

재사용 워크플로

여러 저장소에서 동일한 테스트 단계를 쓰려면 .github/workflows/reusable-test.ymlworkflow_call을 두고 호출합니다.

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: npm
      - run: npm ci && npm test

Node·OS 매트릭 (크로스 테스트)

strategy:
  fail-fast: false
  matrix:
    node: [20, 22]
    os: [ubuntu-latest, windows-latest]

npm 캐시

actions/setup-nodecache: npm만으로도 대부분 충분합니다. 모노레포에서는 cache-dependency-path로 lockfile 경로를 지정합니다.

성능: 캐시와 병렬화

항목효과비고
npm ci + lockfile설치 재현성CI 표준
actions/setup-node cachenode_modules 복원 시간 단축대부분의 Node 워크플로에 적용
Docker BuildKit + 레이어 캐시이미지 빌드 시간 단축cache-from/cache-to 또는 레지스트리 캐시
잡 병렬화PR 피드백 시간 단축lint/test를 분리할 때 유의
트레이드오프: 매트릭을 늘리면 분 단위 요금(유료 한도)큐 대기가 늘 수 있습니다. 릴리스 브랜치에만 넓게 쓰는 편이 경제적일 때가 많습니다.

실무 사례

  • 스테이징 자동 + 프로덕션 수동 승인: environment: productionRequired reviewers를 두면 CD 단계가 승인 후에만 진행됩니다.
  • 태그 릴리스: on: push: tags: ['v*']로 시맨틱 버전 태그에만 이미지 v1.2.3을 박아 배포합니다.
  • 마이그레이션: 배포 직전 잡에서 npm run migrate를 실행하되, 락 타임아웃롤백 전략을 문서화합니다.

트러블슈팅

증상원인해결
permission denied GHCR 푸시GITHUB_TOKEN 권한 부족잡에 permissions: packages: write
npm ci 실패lockfile 불일치로컬에서 npm install 후 lockfile 커밋
다른 Node에서만 실패버전 차이engines 필드 + setup-node 버전 고정
Docker 빌드만 느림캐시 없음BuildKit 캐시 또는 --cache-from 이미지
배포는 됐는데 구 버전태그 latest만 당김SHA 태그 배포 또는 이미지 다이제스트 고정
디버깅 팁: 실패한 잡에서 Re-run with debug logging을 켜고, 동일 컨테이너를 로컬에서 act(도구)로 재현할 수 있는지 확인합니다.

내부 동작과 핵심 메커니즘

이 글의 주제는 「GitHub Actions로 Node.js CI/CD 파이프라인 만들기 | 테스트·빌드·Docker·배포」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 요청 경로와 상태 전이를 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.

처리 파이프라인(개념도)

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]

알고리즘·프로토콜 관점에서의 체크포인트

  • 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.

프로덕션 운영 패턴

실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.

영역운영 관점에서의 질문
관측성요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가
안전성입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가
신뢰성재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가
성능캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가

운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.


문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스 컨디션, 타임아웃, 외부 의존성 불안정최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인
성능 저하N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사상한·TTL·스냅샷 비교(힙 덤프/트레이스)
빌드·배포만 실패환경 변수·권한·플랫폼 차이CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin)

권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.

마무리

  • 한 파이프라인으로 테스트·빌드·이미지·배포를 묶으시면 반복 작업과 실수를 줄일 수 있습니다.
  • npm ci, Node 버전 고정, Docker 멀티 스테이지는 프로덕션 CI/CD의 기본 삼종 세트에 가깝습니다.
  • 다음 단계로는 Docker Compose로 Node API·DB·Redis 한 번에 띄우기로 로컬·스테이징 환경을 맞추고, Node.js 배포 가이드와 함께 읽으면 운영 그림이 완성됩니다.

프로덕션 체크리스트

  • 시크릿: DEPLOY_HOST 등은 저장소 Secrets에만 두고, 로그에 노출되지 않게 합니다.
  • 이미지 참조: 가능하면 latest이 아니라 SHA 또는 다이제스트로 배포해 재현성을 높입니다.
  • 마이그레이션: 배포 직전 DB 마이그레이션 실패 시 롤백 절차를 팀에 공유합니다.
  • 정적 사이트: Astro·Hugo 같은 정적 생성기는 Astro 블로그 가이드Cloudflare Pages로 더 간단하게 배포할 수 있습니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. GitHub Actions Node.js 배포 방법: 테스트→빌드→Docker 이미지 푸시→스테이징·프로덕션 배포까지 한 워크플로로 정리합니다. 캐시·시크릿·재사용 워크플로 포함. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Node.js, GitHub Actions, CI/CD, 배포, Docker, 테스트, 자동화 등으로 검색하시면 이 글이 도움이 됩니다.