CI/CD 처음 구축했을 때
삽질 기록
커밋 47개 만에 초록불 — 그 사이에 있었던 일들
왜 CI/CD를 건드리게 됐나
스타트업 입사 4개월 차였다. 배포가 수동이었다. 팀원이 로컬에서 빌드하고, FTP로 서버에 올리고, 서버에 SSH 접속해서 프로세스를 재시작했다. 이게 매 배포마다 반복됐다. 실수도 잦았다. 배포하다가 환경변수를 빠뜨리거나, 빌드 전에 커밋 안 된 파일이 있거나.
팀장이 "CI/CD 구축해볼 수 있어요?"라고 물어봤을 때 속으로 "할 수 있다"고 했다. GitHub Actions 들어본 적은 있었고, 대략 "커밋하면 자동으로 배포되는 거잖아요"라는 수준이었다. 그게 시작이었다. 그 후 3일이 지옥이었다.

목표했던 파이프라인
원하는 그림은 단순했다. main 브랜치에 머지되면 자동으로 빌드 → 테스트 → 배포. 말로는 쉬웠다.
이걸 3일 만에 구현하리라고 예상했다. 실제로는 각 단계마다 막혔다.
삽질 1 — yml 파일 들여쓰기
GitHub Actions 공식 문서에서 yml 예시를 복붙했다. 실행했더니 바로 에러. "yaml: line 5: found character that cannot start any token". 5번째 줄을 보니 멀쩡해 보였다. 20분 동안 내용만 봤다. 결국 구글링으로 알았다. YAML은 탭 문자를 인정하지 않는다. VS Code에서 탭으로 들여쓰기가 됐던 것이다.
에디터 설정을 바꾸고 스페이스 2칸으로 통일하니 해결됐다. 첫 번째 삽질. 내용 문제가 아니라 형식 문제였다는 게 허탈했다.
삽질 2 — 환경변수 시크릿 설정
# 빌드 단계 실행 중...
Run npm run build
warn - Loaded env from .env.local
Error: Missing required environment variable: NEXT_PUBLIC_SUPABASE_URL
Error: Missing required environment variable: NEXT_PUBLIC_SUPABASE_ANON_KEY
✕ Build failed with exit code 1
─────────────────────────────────────────
# 원인: .env.local은 .gitignore에 포함됨
# GitHub Actions 환경에는 로컬 .env 파일 없음
로컬에서는 빌드가 됐는데 Actions에서는 안 됐다. 에러 메시지를 보니 환경변수가 없다고 했다. "분명히 .env.local 파일에 있는데?" 그게 문제였다. .env.local은 .gitignore에 포함돼 있어서 GitHub에 올라가지 않는다. GitHub Actions는 완전히 새 환경이라 그 파일이 없다.
해결책은 GitHub Repository → Settings → Secrets and variables → Actions에서 시크릿을 추가하는 것이었다. 그리고 yml 파일에서 그 시크릿을 환경변수로 주입해야 했다. 이걸 모르고 1시간을 헤맸다.
GitHub Secrets에서 시크릿 이름과 yml 파일에서 참조하는 이름이 정확히 일치해야 한다. 대소문자까지 같아야 한다. "SUPABASE_URL"과 "supabase_url"은 다른 시크릿이다. 이거 때문에 30분 날렸다.
삽질 3 — Node.js 버전 불일치
로컬 Node.js 버전이 20.x였는데 GitHub Actions 기본 ubuntu-latest의 Node 버전이 달랐다. 패키지 중 하나가 특정 Node 버전을 요구했고, 버전이 안 맞아서 install 단계에서 실패했다. 해결책은 yml에 Node 버전을 명시하는 것이었다. 이걸 알기까지 45분.
삽질 4 — SSH 배포 권한 설정
빌드까지는 됐는데 배포 단계에서 막혔다. EC2 서버에 SSH로 접속해서 git pull하고 pm2 restart하는 게 목표였는데, "Permission denied (publickey)"가 계속 났다. SSH 키 문제였다.
해결 과정이 길었다. EC2에서 SSH 키 페어를 생성하고, 퍼블릭 키를 authorized_keys에 추가하고, 프라이빗 키를 GitHub Secrets에 저장하고, yml에서 그걸 읽어와서 SSH에 사용해야 했다. 각 단계가 다 낯설었다. 이 하나에만 2시간이 걸렸다.
Run ssh -i ~/.ssh/deploy_key ubuntu@{서버IP} 'cd /app && git pull'
Warning: Permanently added '{서버IP}' (ECDSA) to the list of known hosts.
ubuntu@{서버IP}: Permission denied (publickey).
─────────────────────────────────────────
# 시도 1: 키 경로 오타 → 실패
# 시도 2: 키 권한 문제 (chmod 600 필요) → 실패
# 시도 3: authorized_keys에 키 추가 안 함 → 실패
# 시도 4: 위 세 가지 모두 해결 → 성공
삽질 5 — 캐시 설정과 빌드 시간
일단 돌아가게 만들었다. 그런데 매 실행마다 8분이 걸렸다. npm install이 매번 4분을 잡아먹었다. 의존성 파일이 바뀌지 않는데도 매번 새로 다운받는 것이었다.
캐시를 써야 했다. setup-node의 cache 옵션이 있다는 걸 그때 알았다. package-lock.json이 바뀌지 않으면 캐시된 node_modules를 쓰도록 설정하니 install 시간이 4분에서 20초로 줄었다. 전체 파이프라인이 8분에서 3분으로 줄었다.
# ✅ 캐시 적용 — package-lock.json 변경 없으면 캐시 사용 (20초) - uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' # ← 이 한 줄로 4분 → 20초
# Next.js 빌드 캐시도 추가하면 더 빠름 - name: Cache Next.js build uses: actions/cache@v4 with: path: .next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}
그 밖의 삽질들 — 짧게
branches 조건 없이 on: push만 썼더니 feature 브랜치 push할 때마다 파이프라인이 돌았다. 불필요한 실행이 쌓여서 GitHub Actions 무료 할당량이 순식간에 줄었다. branches: [main]을 명시해서 해결.
로컬에서는 빌드가 됐는데 CI에서 TypeScript 에러가 났다. 알고 보니 로컬 tsconfig와 CI 환경의 tsconfig가 달랐다. 팀원이 tsconfig를 수정했는데 내 로컬엔 아직 반영 전이었다. CI가 이걸 잡아준 거였다. 나쁜 게 아니라 오히려 CI가 제 역할을 한 것이었다.
처음 배포할 땐 pm2 start를 써야 했는데 pm2 restart를 써서 "app not found" 에러가 났다. 배포 스크립트에 pm2 start가 없으면 restart하고, 있으면 restart하는 로직을 추가해야 했다. pm2 start app || pm2 restart app 패턴으로 해결.
배포 성공/실패를 Slack으로 알리는 단계를 추가했는데, 이전 단계가 실패하면 Slack 알림 단계도 건너뛰었다. if: always()를 설정하지 않아서였다. 실패했을 때 더 알림이 필요한데 안 오는 아이러니한 상황이었다.
47번째 커밋 — 드디어 초록불
Run: CI/CD Pipeline
Triggered by: push to main (commit: fix: ssh key permission)
─────────────────────────────────────────
✓ Checkout 0s
✓ Setup Node.js 2s (캐시 히트)
✓ Install deps 18s (캐시 사용)
✓ TypeScript check 12s
✓ Build 48s (Next.js 캐시 사용)
✓ Run tests 22s
✓ Deploy to EC2 14s
✓ Notify Slack 1s
─────────────────────────────────────────
✓ All jobs passed Total: 2m 57s
초록불이 떴을 때 실제로 자리에서 일어났다. 혼자였는데도 "됐다"를 소리 내서 말했다. 3일 동안 빨간 불만 보다가 처음으로 전체가 초록색인 걸 보니 이상하게 눈물이 나올 것 같았다. 개발에서 이런 감정이 드는 경우가 흔치 않은데.
처음 CI/CD를 구축하는 것은 빌드 자동화가 아니라 삽질 자동화였다. 매 삽질마다 하나씩 배웠고, 47번째 커밋이 됐을 때 그 삽질들이 전부 지식이 돼 있었다.
3일 삽질이 가르쳐준 것들
블로그 글만 보고 따라하면 버전이 달라서 안 되는 경우가 많다. GitHub Actions 공식 문서와 사용하는 액션의 최신 버전을 먼저 확인하는 습관이 생겼다.
처음엔 에러 메시지 첫 줄만 보고 구글링했다. 나중에 보니 에러 메시지 아래에 원인과 힌트가 있는 경우가 많았다. 끝까지 읽는 습관이 생겼다.
한 커밋에 세 가지를 바꾸면 어느 게 문제인지 모른다. 하나씩 바꾸고 실행해보는 게 느린 것 같지만 결국 빠르다. 이진 탐색처럼 범위를 좁혀가야 한다.
세 달 후 비슷한 환경을 구축할 때 내가 했던 삽질을 또 했다. 기록해두지 않았기 때문이었다. 이제는 성공한 설정을 팀 노션에 바로 정리한다.
CI/CD 구축 처음 도전하는 분들에게
첫째, 목표를 가장 단순하게 시작한다. "push하면 빌드만 되게 만든다"부터. 배포까지 한 번에 하려다 복잡도가 올라가서 어디서 막히는지 모르게 된다. 빌드 → 테스트 → 배포 순서로 단계를 나눠서 하나씩 붙인다.
둘째, 로컬에서 같은 명령을 먼저 실행해본다. CI에서 npm run build가 안 된다면, 로컬 터미널에서 환경변수 없이 같은 명령을 실행해보면 같은 에러가 난다. 로컬에서 재현할 수 있으면 디버깅이 훨씬 빠르다.
셋째, 47번의 커밋을 두려워하지 않는다. 처음엔 매 커밋이 부끄럽게 느껴질 수 있다. "또 fix: typo", "또 fix: env var". 그래도 된다. 이 과정이 학습이다. 나중엔 첫 커밋에 성공하는 날이 온다.
빌드 + 테스트 + EC2 배포 + Slack 알림을 포함한 기본 파이프라인이다. 이걸 베이스로 시작하면 환경변수, Node 버전, 캐시는 미리 잡혀 있다. secrets 설정만 각자 환경에 맞게 추가하면 된다.
on: push: branches: [main]
jobs: deploy: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v4
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'
- name: Install & Build env: NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} run: | npm ci npm run build
- name: Run tests run: npm test
- name: Deploy if: success() env: SSH_KEY: ${{ secrets.EC2_SSH_KEY }} HOST: ${{ secrets.EC2_HOST }} run: | mkdir -p ~/.ssh echo "$SSH_KEY" > ~/.ssh/key && chmod 600 ~/.ssh/key ssh-keyscan -H "$HOST" >> ~/.ssh/known_hosts ssh -i ~/.ssh/key ubuntu@"$HOST" ' cd /var/www/app && git pull && npm ci && npm run build && pm2 start app || pm2 restart app '
- name: Notify if: always() # 성공/실패 모두 알림 uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} webhook_url: ${{ secrets.SLACK_WEBHOOK }}
지금도 가끔 그때 GitHub Actions 히스토리를 열어본다. 빨간 X가 줄줄이 이어지다가 마지막에 초록 체크가 있는 그 화면. 47번째 커밋 메시지가 "fix: please just work"였다. 그 절박함이 지금도 웃기다.
CI/CD는 구축하고 나면 당연한 것처럼 느껴지지만, 처음 구축할 때는 그게 전혀 당연하지 않다. 그 삽질들이 지금의 인프라 감각을 만들었다. 프론트엔드 개발자라도 배포 파이프라인을 이해하면 팀에서 완전히 다른 역할을 할 수 있다는 걸 그때 배웠다.
💬 CI/CD 구축하면서 특별히 오래 걸렸던 삽질이 있으신가요?
GitHub Actions, GitLab CI, Jenkins 등 어떤 도구든 — 처음 구축할 때 겪은 황당한 에러나 의외로 간단했던 해결책을 댓글로 나눠주세요. 지금 같은 삽질 중인 분들에게 가장 도움이 될 정보입니다.
'개발자 > 기술스택 도구' 카테고리의 다른 글
| Notion vs Confluence, 팀에서 실제로 써보니 (0) | 2026.07.31 |
|---|---|
| 개발자가 AI 도구를 실무에서 쓰는 현실 (1) | 2026.07.24 |
| 모노레포 도입 후 실제 팀에 생긴 변화 (0) | 2026.07.17 |
| 상태 관리 라이브러리 바꾸면서 겪은 마이그레이션 고통 (0) | 2026.07.10 |
| Figma와 협업하면서 생긴 프론트엔드 개발자의 고충 (0) | 2026.07.03 |