
프로젝트를 진행하면서 코드를 커밋할 때마다 자동으로 검사해주는 도구가 필요했다. 그게 바로
GitHub Actions였다.
보통 CI(Continuous Integration) 는 코드 변경이 있을 때 자동으로 빌드, 테스트, 타입 검사, 린트 등을 돌려서 문제가 없는지 확인하는 과정을 말한다.
CD(Continuous Deployment/Delivery) 는 배포까지 이어지는 개념인데,GitHub Actions자체가 배포를 대신하는 건 아니다. 다만Vercel,AWS같은 외부 서비스와 연동하면 CD 파이프라인도 만들 수 있다.
나는Vercel을 쓰면서GitHub Actions도 함께 사용했다.
물론Vercel자체에도 PR마다 Preview를 만들고 빌드를 돌려주는 기본 CI 기능이 있다.
하지만Vercel은 주로 “배포”에 초점이 맞춰져 있고, 세밀한 단계별 검사(예: 타입 체크 후 빌드, 테스트 후 배포)는 직접 제어하기 어렵다.
반대로GitHub Actions는 워크플로우를 원하는 대로 설계할 수 있었다.
덕분에 나는 코드 품질을Actions에서 관리하고, 최종 배포는Vercel이 담당하는 식으로 역할을 나눴다.그래서 나는 이번 프로젝트에서 GitHub Actions를 통해 CI를 직접 경험해보기로 했다.
나는 프로젝트를 하면서 코드를 올릴 때마다 직접 확인해야 하는 작업들이 있었다.
예를 들어 TypeScript 타입이 제대로 맞는지, Next.js 빌드가 깨지지 않는지, 의존성 설치 과정에서 오류가 없는지 같은 것들이다.
혼자 개발할 때는 로컬에서 한 번씩 확인하면 그만이었다. 하지만 브랜치가 여러 개로 나뉘고, PR을 열어서 코드를 합치는 과정이 생기면 사람이 직접 매번 체크하는 건 금방 실수가 생길 수밖에 없었다.
그래서GitHub Actions를 도입했다.
GitHub Actions는 PR이 열리거나 브랜치에 push가 발생했을 때, 내가 정해둔 순서대로 자동으로 빌드와 검사 과정을 실행해 준다.
덕분에 “코드가 깨지지 않았는가?” 같은 기초적인 검증은 서버가 대신 확인해주고, 나는 기능 개발에만 집중할 수 있게 되었다.무엇보다도
GitHub안에서 바로 동작하기 때문에 별도의 서버를 따로 세팅하지 않아도 됐다.
레포에.github/workflows/ci.yml파일 하나만 만들어두면, 이후부터는 커밋이나 PR마다 자동으로 CI가 돌아갔다.
GitHub Actions는 워크플로우(workflow) 단위로 구성된다.
워크플로우 안에는 여러 개의 잡(job) 이 있고, 각 잡은 여러 개의 스텝(step) 으로 이루어진다.
- 워크플로우(Workflow):
레포 안의.github/workflows/*.yml파일 하나가 곧 워크플로우다.
여기에서 “어떤 이벤트가 발생했을 때 실행할지”를 정한다.
예를 들어on: pull_request라고 적으면 PR이 열릴 때마다 이 워크플로우가 돈다.
- 잡(Job):
워크플로우 안에서 독립적으로 실행되는 작업 단위다.
보통 타입 체크 / 빌드 / 테스트처럼 논리적으로 구분되는 단계별로 잡을 나눈다.
잡은 서로 독립적으로 실행될 수도 있고,needs키워드를 이용해서 앞선 잡이 끝난 뒤에만 실행되도록 순서를 강제할 수도 있다.
- 스텝(Step):
잡을 이루는 세부 단계다.
예를 들어actions/checkout을 통해 코드를 가져오고,
actions/setup-node로 Node.js 환경을 세팅하고,
run: pnpm install로 의존성을 설치하는 과정이 각각 하나의 스텝이다.
스텝 안에서는GitHub에서 제공하는 공식 액션(action) 을 쓸 수도 있고, 단순히run명령어를 적어서 직접 쉘 명령을 실행할 수도 있다.
정리하면, 워크플로우는 전체 설계도, 잡은 큰 작업 단위, 스텝은 실제 실행되는 명령어라고 보면 이해가 쉽다.
처음에는 용어가 복잡해 보였지만, 이 구조를 머릿속에 넣고yaml을 읽으니 훨씬 이해가 빨라졌다.
name: CI Example # 워크플로우 이름 (Actions 탭에서 보이는 이름)
on:
pull_request: # 트리거: PR이 열릴 때 실행
branches: [main, develop] # main, develop 브랜치에 대해 동작
jobs:
build: # 잡(Job) 이름 → 큰 작업 단위
runs-on: ubuntu-latest # 어떤 환경에서 실행할지 (여기선 우분투 최신)
steps: # 스텝(Step) 모음 → 잡을 구성하는 세부 단계들
- name: Checkout repository
uses: actions/checkout@v4 # 공식 액션(Action) → 레포지토리 코드 가져오기
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20 # Node.js 버전 지정
- name: Install dependencies
run: pnpm install --frozen-lockfile # run 명령어 → 의존성 설치
- name: Type check
run: pnpm exec tsc --noEmit # run 명령어 → TypeScript 타입 검사
- name: Build project
run: pnpm build # run 명령어 → Next.js 빌드 실행
이 예시에서 보면:
.github/workflows/ci.yml 파일 전체build (여기서는 하나만 있음)👉 실제 코드와 구조가 연결되니까, 추상적인 개념들이 확실히 이해됐다.
Actions를 쓰기 전에 먼저 어떤 설정들이 필요한지부터 정리해봤다.
Next.js + pnpm프로젝트 기준으로는 대략 이런 것들이 필요하다.
1) 트리거 지정
워크플로우가 언제 실행될지 조건을 정해야 한다.
보통은 PR이 열리거나(pull_request), 특정 브랜치(main,develop,feat/*등)에 push 되었을 때 동작하도록 설정한다.
2) 실행 환경 선택
CI가 돌아갈 가상 머신 환경을 지정한다.
대부분 Node.js 프로젝트는ubuntu-latest로 충분하다. (macOS나 Windows도 가능하지만 특별한 이유가 없으면 우분투가 안정적이다.)
3) Node.js 버전 고정
로컬에서 쓰는
Node.js버전과 CI 환경의 버전이 달라지면 문제가 생기기 쉽다.
그래서actions/setup-node로.nvmrc나package.json의 버전에 맞춰 동일하게 설정해줘야 한다.
4) 패키지 매니저(pnpm) 세팅
CI 환경에는 기본적으로
pnpm이 설치돼 있지 않다.
따라서pnpm/action-setup을 사용해 설치하고,pnpm install --frozen-lockfile로 lockfile 기반 설치를 진행한다.
5) 캐시 설정
CI는 매번 새로운 환경에서 실행되기 때문에, 의존성을 매번 새로 설치하면 속도가 느려진다.
actions/cache를 사용해 pnpm store 디렉터리를 저장해두면 훨씬 빠르게 실행할 수 있다.
6) 실행할 작업 정의
환경이 준비되면 실제로 어떤 검사를 할지 정해야 한다.
- 타입 검사:
pnpm exec tsc --noEmit- 빌드:
pnpm build- 필요하다면
lint나test도 추가할 수 있다.
프론트엔드 초기 세팅은
develop 브랜치를 만들지 않고 바로main 브랜치에push하며 진행했다.
이제GitHub Actions를 구축하면서develop 브랜치를 새로 만들 예정이다.전체적인 흐름은 이렇게 가져가려고 한다.
Feature/*→develop머지
기능 개발은 Feature 브랜치에서 진행한다.
PR을 열어develop으로 머지했을 때는 CI만 실행된다.
즉, 타입 체크, 빌드 검증 같은 자동 검사는 돌지만, 실제 배포는 이루어지지 않는다.
이 단계에서는 기능이 잘 동작하는지 점검하는 데 집중한다.
develop→main머지
develop에서 기능 검증을 마치고 문제가 없다고 판단되면main으로 머지한다.
이때는 CI와 CD가 모두 실행된다.
GitHub Actions에서 코드 검증이 성공해야만 병합이 가능하고, 병합되면 Vercel이 자동으로 Production 배포를 진행한다.정리하면,
develop브랜치는 검증 전용 브랜치로 CI만 담당하고,main브랜치는 배포 전용 브랜치로 CI와 CD를 모두 담당한다.이런 구조라면,
기능 개발 → 검증 → 배포까지의 흐름이 명확하게 구분된다.
나로서는 작은 프로젝트를 진행하면서도 협업 구조를 연습할 수 있고, 실제 서비스 운영 흐름과도 비슷하게 맞출 수 있다.
모든 명령어는 레포지토리 루트
/ or ~(=package.json이 있는 폴더) 에서 실행했다.
A. VS Code에서 열었을 때
- VS Code 상단 메뉴에서 Terminal → New Terminal을 열었다.
- 터미널 프롬프트가 프로젝트 경로인지 확인했다.
pwd # 현재 경로 보기 (macOS/Linux/WSL) ls -la # package.json, pnpm-lock.yaml 보이면 OK.
B. 시스템 터미널에서 열었을 때
레포 폴더로 직접 이동했다.
cd /path/to/your-repo # 예: cd ~/dev/custom-daily-planner git status # On branch main / 파일 목록 보이면 OK
ls -la했을 때package.json과pnpm-lock.yaml이 보여야 루트가 맞았다.
git switch -c develop # Git 2.23 이후 권장되는 방식: # - checkout은 브랜치/파일/커밋까지 포함돼 혼동될 수 있었다. # - switch는 브랜치 관련 동작만 전담해 의도가 더 명확하다. # - 'git checkout -b develop'과 기능은 같지만 switch가 최신 권장 방식이다. -- git push origin develop
이렇게 해서 원격에
develop브랜치를 등록했다.
반드시 레포 루트에서 실행해야
.github/workflows/ci.yml이 올바른 위치에 생성됐다.macOS / Linux / Git-Bash
mkdir -p .github/workflows touch .github/workflows/ci.yml
생성이 잘 되었는지는 아래로 확인했다.ls -la .github/workflows # ci.yml 보이면 OK
ci.yml 작성
lint → test → build순서로 실행되게 설정했다.
PR은main브랜치 대상일 때 실행되고, push는Feature/*,Fix/*,Chore/*,Docs/*,Test/*,Refactor/*브랜치에서만 실행되도록 했다.
PRdevelop브랜치를 추가 안한 이유는 push에서 이미 CI가 실행되기 때문에develop`에서 CI가 한번 더 돌게 되면 시간낭비라고 생각해서 뺐다.
name: Frontend-CI
on:
# 배포 게이트: main으로 가는 PR에서만 CI 실행
pull_request:
branches: [main]
types: [opened, reopened, synchronize, ready_for_review]
# 빠른 피드백: 작업 브랜치에 push될 때 CI 실행 (⚠️ 대문자 네이밍만 허용)
push:
branches:
- "Feature/**"
- "Fix/**"
- "Chore/**"
- "Docs/**"
- "Test/**"
- "Refactor/**"
# (선택) Actions 탭에서 수동 실행 버튼
workflow_dispatch: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
env:
NODE_VERSION: "20.19.2"
CI: "true"
jobs:
lint:
name: Style & Lint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-
- run: pnpm install --frozen-lockfile
- name: Prettier check
run: pnpm run --if-present format:check
- name: ESLint
run: pnpm run --if-present lint
test:
runs-on: ubuntu-latest
needs: lint
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-
- run: pnpm install --frozen-lockfile
- run: pnpm run --if-present test -- --ci
build:
runs-on: ubuntu-latest
needs: test
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- uses: pnpm/action-setup@v4
with:
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-
- run: pnpm install --frozen-lockfile
# (선택) Next.js 빌드 캐시
# - uses: actions/cache@v4
# with:
# path: |
# .next/cache
# key: ${{ runner.os }}-next-${{ hashFiles('pnpm-lock.yaml', 'next.config.*', '**/*.{ts,tsx,js,jsx,css,scss,md,mdx}') }}
# restore-keys: ${{ runner.os }}-next-
- run: pnpm run build
git switch -c "Feature/setup-ci" git add .github/workflows/frontend-ci.yml git commit -m "CDP-27 chore⚙️: add Frontend-CI workflow (lint → test → build)" git push origin "Feature/setup-ci"푸시 후에는 GitHub Actions 탭에서 워크플로우가 생성된 것을 확인할 수 있었다.
A. Feature 브랜치 푸시 (CI 실행)
git add .github/workflows/frontend-ci.yml git commit -m "CDP-27 chore⚙️: add Frontend-CI workflow (lint → test → build)" git push origin "Feature/setup-ci"
Feature 브랜치에 푸시하면CI가 자동으로 실행되었다.
CI가 통과되면develop브랜치로 PR을 열어 머지한다.
이때는PR 템플릿(기능 개발용)을 작성한다.
B. develop → main 머지 (CI + CD)
develop에서 기능 점검이 끝나고 문제가 없으면,main브랜치로 PR을 연다.
이 단계에서는CI가 다시 실행되고, 통과되면 Vercel이 Production 배포를 자동으로 진행한다.
이때는PR 템플릿(배포 점검용)을 작성한다.
pnpm버전을 명확히 지정하지 않아서 CI에서 No pnpm version is specified 오류 발생 →package.json에packageManager필드를 추가해 해결했다.- GitHub에서 기본 브랜치를
develop으로 바꾸지 않아 main으로 직접 머지되는 실수 발생 → 기본 브랜치를develop으로 설정해 해결했다.