본문 바로가기
개발자/기술스택 도구

CI/CD 처음 구축했을 때 삽질 기록

by 나무011 2026. 8. 7.
개발자 현직자 솔직 경험담

CI/CD 처음 구축했을 때
삽질 기록

커밋 47개 만에 초록불 — 그 사이에 있었던 일들

Frontend Developer 2026년 6월 약 14분 읽기

왜 CI/CD를 건드리게 됐나

스타트업 입사 4개월 차였다. 배포가 수동이었다. 팀원이 로컬에서 빌드하고, FTP로 서버에 올리고, 서버에 SSH 접속해서 프로세스를 재시작했다. 이게 매 배포마다 반복됐다. 실수도 잦았다. 배포하다가 환경변수를 빠뜨리거나, 빌드 전에 커밋 안 된 파일이 있거나.

팀장이 "CI/CD 구축해볼 수 있어요?"라고 물어봤을 때 속으로 "할 수 있다"고 했다. GitHub Actions 들어본 적은 있었고, 대략 "커밋하면 자동으로 배포되는 거잖아요"라는 수준이었다. 그게 시작이었다. 그 후 3일이 지옥이었다.

CI/CD 처음 구축했을 때 삽질 기록
CI/CD 처음 구축했을 때 삽질 기록
47개 총 커밋 수
3일 걸린 시간
12번 다른 에러 등장
1번 최종 초록불

목표했던 파이프라인

원하는 그림은 단순했다. main 브랜치에 머지되면 자동으로 빌드 → 테스트 → 배포. 말로는 쉬웠다.

📝
Push
main 브랜치
📦
Install
npm ci
🔨
Build
next build
🧪
Test
jest
🚀
Deploy
Vercel / EC2

이걸 3일 만에 구현하리라고 예상했다. 실제로는 각 단계마다 막혔다.

삽질 1 — yml 파일 들여쓰기

💀
첫 번째 삽질
YAML은 탭이 아니라 스페이스다 — 30분짜리 삽질

GitHub Actions 공식 문서에서 yml 예시를 복붙했다. 실행했더니 바로 에러. "yaml: line 5: found character that cannot start any token". 5번째 줄을 보니 멀쩡해 보였다. 20분 동안 내용만 봤다. 결국 구글링으로 알았다. YAML은 탭 문자를 인정하지 않는다. VS Code에서 탭으로 들여쓰기가 됐던 것이다.

에디터 설정을 바꾸고 스페이스 2칸으로 통일하니 해결됐다. 첫 번째 삽질. 내용 문제가 아니라 형식 문제였다는 게 허탈했다.

❌ 에러 발생 — 탭 들여쓰기 (보기엔 같아 보임)
name: CI/CD on: push: # ← 탭 문자 (보이지 않지만 여기가 문제) branches: [main]
✅ 수정 후 — 스페이스 2칸
name: CI/CD on: push: # ← 스페이스 2칸 branches: [main]

삽질 2 — 환경변수 시크릿 설정

 
 
 
GitHub Actions 로그 — 빌드 실패

# 빌드 단계 실행 중...

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 파일 없음

🔑
두 번째 삽질
환경변수가 GitHub Actions에는 없다 — 당연한 걸 몰랐다

로컬에서는 빌드가 됐는데 Actions에서는 안 됐다. 에러 메시지를 보니 환경변수가 없다고 했다. "분명히 .env.local 파일에 있는데?" 그게 문제였다. .env.local은 .gitignore에 포함돼 있어서 GitHub에 올라가지 않는다. GitHub Actions는 완전히 새 환경이라 그 파일이 없다.

해결책은 GitHub Repository → Settings → Secrets and variables → Actions에서 시크릿을 추가하는 것이었다. 그리고 yml 파일에서 그 시크릿을 환경변수로 주입해야 했다. 이걸 모르고 1시간을 헤맸다.

✅ 시크릿을 환경변수로 주입하는 올바른 방법
jobs: build: runs-on: ubuntu-latest steps: - name: Build run: npm run build env: # GitHub Secrets에서 환경변수 주입 NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} # 시크릿 이름은 대소문자 구분함!
⚠️ 시크릿 설정할 때 자주 하는 실수

GitHub Secrets에서 시크릿 이름과 yml 파일에서 참조하는 이름이 정확히 일치해야 한다. 대소문자까지 같아야 한다. "SUPABASE_URL"과 "supabase_url"은 다른 시크릿이다. 이거 때문에 30분 날렸다.

삽질 3 — Node.js 버전 불일치

3
로컬은 되는데 Actions는 안 된다 — Node 버전 문제
에러: "The engine "node" is incompatible with this module"

로컬 Node.js 버전이 20.x였는데 GitHub Actions 기본 ubuntu-latest의 Node 버전이 달랐다. 패키지 중 하나가 특정 Node 버전을 요구했고, 버전이 안 맞아서 install 단계에서 실패했다. 해결책은 yml에 Node 버전을 명시하는 것이었다. 이걸 알기까지 45분.

✅ Node 버전 명시 — setup-node 액션 활용
steps: - uses: actions/checkout@v4 # Node 버전 명시 (로컬과 맞춰야 함) - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' # .nvmrc 파일로 관리하면 더 좋음 cache: 'npm' - name: Install dependencies run: npm ci # npm install 대신 ci 사용 (더 안정적)

삽질 4 — SSH 배포 권한 설정

🔐
네 번째 삽질 (가장 오래 걸림)
EC2 서버에 Actions가 SSH로 접속을 못 한다

빌드까지는 됐는데 배포 단계에서 막혔다. EC2 서버에 SSH로 접속해서 git pull하고 pm2 restart하는 게 목표였는데, "Permission denied (publickey)"가 계속 났다. SSH 키 문제였다.

해결 과정이 길었다. EC2에서 SSH 키 페어를 생성하고, 퍼블릭 키를 authorized_keys에 추가하고, 프라이빗 키를 GitHub Secrets에 저장하고, yml에서 그걸 읽어와서 SSH에 사용해야 했다. 각 단계가 다 낯설었다. 이 하나에만 2시간이 걸렸다.

 
 
 
SSH 접속 에러 — 실제 로그

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: 위 세 가지 모두 해결 → 성공

✅ SSH 배포 단계 — 최종 작동 버전
- name: Deploy to EC2 env: SSH_PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }} EC2_HOST: ${{ secrets.EC2_HOST }} run: | # 시크릿에서 SSH 키 파일 생성 mkdir -p ~/.ssh echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key # 권한 반드시 600 # known_hosts에 서버 추가 (StrictHostKeyChecking 우회) ssh-keyscan -H "$EC2_HOST" >> ~/.ssh/known_hosts # SSH로 배포 명령 실행 ssh -i ~/.ssh/deploy_key ubuntu@"$EC2_HOST" ' cd /var/www/app && git pull origin main && npm ci && npm run build && pm2 restart app '

삽질 5 — 캐시 설정과 빌드 시간

⏱️
다섯 번째 삽질
파이프라인이 매번 8분 걸렸다 — 캐시를 몰랐다

일단 돌아가게 만들었다. 그런데 매 실행마다 8분이 걸렸다. npm install이 매번 4분을 잡아먹었다. 의존성 파일이 바뀌지 않는데도 매번 새로 다운받는 것이었다.

캐시를 써야 했다. setup-node의 cache 옵션이 있다는 걸 그때 알았다. package-lock.json이 바뀌지 않으면 캐시된 node_modules를 쓰도록 설정하니 install 시간이 4분에서 20초로 줄었다. 전체 파이프라인이 8분에서 3분으로 줄었다.

✅ 캐시 적용 전후 비교
# ❌ 캐시 없음 — 매번 node_modules 다운로드 (4분) - uses: actions/setup-node@v4 with: node-version: '20'
# ✅ 캐시 적용 — 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') }}

그 밖의 삽질들 — 짧게

6
브랜치 조건을 잘못 설정했다
push to main이 아니라 모든 push에 트리거됐다

branches 조건 없이 on: push만 썼더니 feature 브랜치 push할 때마다 파이프라인이 돌았다. 불필요한 실행이 쌓여서 GitHub Actions 무료 할당량이 순식간에 줄었다. branches: [main]을 명시해서 해결.

7
TypeScript 에러가 로컬에서 안 보였다
tsconfig의 strict 설정이 달랐다

로컬에서는 빌드가 됐는데 CI에서 TypeScript 에러가 났다. 알고 보니 로컬 tsconfig와 CI 환경의 tsconfig가 달랐다. 팀원이 tsconfig를 수정했는데 내 로컬엔 아직 반영 전이었다. CI가 이걸 잡아준 거였다. 나쁜 게 아니라 오히려 CI가 제 역할을 한 것이었다.

8
배포 후 서버가 안 뜨는 줄 알았다
pm2 start vs pm2 restart 차이를 몰랐다

처음 배포할 땐 pm2 start를 써야 했는데 pm2 restart를 써서 "app not found" 에러가 났다. 배포 스크립트에 pm2 start가 없으면 restart하고, 있으면 restart하는 로직을 추가해야 했다. pm2 start app || pm2 restart app 패턴으로 해결.

9
Slack 알림 단계가 이전 단계 실패를 무시했다
if: always() 조건을 몰랐다

배포 성공/실패를 Slack으로 알리는 단계를 추가했는데, 이전 단계가 실패하면 Slack 알림 단계도 건너뛰었다. if: always()를 설정하지 않아서였다. 실패했을 때 더 알림이 필요한데 안 오는 아이러니한 상황이었다.

47번째 커밋 — 드디어 초록불

 
 
 
GitHub Actions — 최종 성공 로그

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". 그래도 된다. 이 과정이 학습이다. 나중엔 첫 커밋에 성공하는 날이 온다.

✅ 완성된 yml 파일 — 최소한의 검증된 템플릿

빌드 + 테스트 + EC2 배포 + Slack 알림을 포함한 기본 파이프라인이다. 이걸 베이스로 시작하면 환경변수, Node 버전, 캐시는 미리 잡혀 있다. secrets 설정만 각자 환경에 맞게 추가하면 된다.

✅ 최종 완성 버전 — .github/workflows/deploy.yml
name: CI/CD Pipeline
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 등 어떤 도구든 — 처음 구축할 때 겪은 황당한 에러나 의외로 간단했던 해결책을 댓글로 나눠주세요. 지금 같은 삽질 중인 분들에게 가장 도움이 될 정보입니다.


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 나무핀