19장. CI① — GitHub Actions로 관문을 자동화한다

이 장이 끝나면: 온티켓 저장소에 코드가 올라올 때마다 클라우드가 자동으로 테스트를 돌리는 CI 파이프라인이 서 있다. PR에 초록 체크·빨간 X가 뜨고, 테스트가 실패하면 병합이 막힌다. 워크플로 파일(YAML)을 읽을 줄 알게 된다.


19.1 개념 — 내 컴퓨터를 넘어서는 관문

pre-commit 훅의 한계

앞에서 세운 커밋 훅은 강력하지만 약점이 있다. 내 컴퓨터에서만 작동한다. 누군가 훅을 지우고 커밋하면? 훅 설정이 없는 환경에서 밀어 넣으면? 동료가 합류했는데 그 사람 컴퓨터엔 훅이 없다면? 로컬 훅은 '내 성실함'에 기대는 마지막 한 뼘이 남아 있다.

이 '한 뼘'이 왜 위험한지 온티켓 상황으로 그려 보자. 지금 온티켓은 오대표 혼자 보던 코드에서 사람이 늘어나는 서비스로 커 가는 중이다. 로컬 커밋 훅은 각자의 노트북에 설치되는 장치다. 설치는 각 사람이 자기 컴퓨터에서 해야 하고, 한 번 설치했어도 그 파일을 지우거나 비활성화하면 그만이다. 정리하면 로컬 훅에는 최소 네 개의 구멍이 있다.

즉 로컬 훅은 "모두가 성실하게, 같은 버전으로, 매번 설치해 둔다" 는 전제 위에서만 완전하다. 그 전제는 사람이 하나 늘 때마다 약해진다. 검증을 '개인의 습관'에 맡겨 두는 한, 언젠가는 새는 날이 온다. 온티켓처럼 결제와 좌석이 걸린 서비스에서 그 하루가 하필 초과 판매 사고 (골목 라이브의 그날처럼)로 이어지면 곤란하다.

[짚고 가기] 여기서 "그럼 애써 세운 커밋 훅은 헛수고였나?" 하는 의문이 들 수 있다. 그렇지 않다. 로컬 훅은 커밋되기도 전에 걸러 주는 가장 빠른 그물이다. 다만 그물 하나로는 부족하다는 것이 이 장의 출발점이다. "훅은 나를 위한 것, CI는 팀을 위한 것"이라는 한 문장으로 대비를 세워 두면 두 장치의 관계가 헷갈리지 않는다.

CI — 성실함에 기대지 않는 체계

그 한 뼘을 없애는 것이 CI(Continuous Integration, 지속적 통합) 다. CI는 코드가 저장소(GitHub)에 올라오는 순간, 내 컴퓨터가 아니라 중립적인 클라우드 서버가 테스트를 돌리는 체계다. 누구의 컴퓨터에 무엇이 깔렸든 상관없다 — 모두가 통과해야 하는 단 하나의 관문이 서버에 선다.

핵심 단어는 '중립' 이다. 로컬 훅은 '내 컴퓨터'라는, 사람마다 다르고 사람이 마음대로 바꿀 수 있는 땅 위에 서 있었다. CI가 도는 서버는 모두가 함께 쓰는 공용 공간이다. 오대표의 맥북에서도, 새로 합류한 사람의 윈도우 노트북에서도, 코드는 결국 같은 클라우드 머신에서 똑같은 방식으로 검사받는다. "내 컴퓨터에선 됐는데요"라는 말이 통하지 않는 이유가 여기 있다 — 판정 장소가 개인의 컴퓨터가 아니라 모두에게 동일한 제3의 장소이기 때문이다.

'지속적 통합(Continuous Integration)'이라는 이름도 풀어 보면 개념이 또렷해진다. 여기서 통합(integration) 은 여러 사람이 따로 작업한 코드를 하나로 합치는 일을 말한다. 옛날에는 각자 몇 주씩 따로 개발하다가 마지막에 한꺼번에 합쳤는데, 그러면 충돌과 버그가 산더미처럼 터졌다(이른바 '통합 지옥'). 그래서 나온 발상이 "작게, 자주, 계속 합치자" 였다. 합칠 때마다 바로 검사해서 깨진 곳을 즉시 잡으면, 문제가 쌓이기 전에 발견된다. 지속적(continuous) 은 바로 이 '합칠 때마다 매번, 자동으로'를 가리킨다. CI란 결국 "코드가 합쳐지는 순간마다 자동으로 검사가 돈다" 는 약속이다.

로컬 훅과 CI는 역할이 다르다

둘은 경쟁이 아니라 이중 방어다.

로컬 커밋 훅 (pre-commit) CI (이 장)
어디서 도나 내 컴퓨터 중립 클라우드 서버
언제 커밋 직전 push·PR 시
장점 빠른 피드백(커밋 전에 잡음) 우회 불가, 모두에게 동일
약점 지우거나 없는 환경이면 무력 커밋한 뒤에야 돎(조금 늦음)

로컬 훅은 빠르게 걸러 주고, CI는 그물을 빠져나간 것을 최종적으로 막는다. 빠른 1차 + 확실한 2차. 어느 쪽도 하나만으로는 부족하다.

이 이중 방어를 공항 보안에 비유하면 감이 온다. 로컬 훅은 집에서 짐 쌀 때 스스로 확인하는 단계다 — 빠르고, 내가 알아서 챙기며, 안 하면 그냥 안 한 채로 지나간다. CI는 공항의 X-ray 게이트다 — 누가 왔든, 집에서 뭘 확인했든 안 했든, 모두 똑같이 통과해야 하고 우회로가 없다. 집에서 미리 챙기면 게이트에서 걸릴 일이 줄어 빠르고(1차), 게이트는 집에서 놓친 것을 마지막에 잡는다(2차). 둘 다 있어야 흐름이 매끄럽고 새는 곳도 없다.

두 방어의 순서와 목적을 시간 축으로 정리하면 이렇다.

로컬 커밋 훅(빠른 1차)과 CI(확실한 2차)의 이중 방어 파이프라인

같은 검사(예: pnpm test)가 두 곳에서 돈다는 점이 중요하다. 낭비가 아니라 의도된 중복이다. 1차에서 놓쳐도 2차가 잡고, 1차를 통째로 건너뛴 사람도 2차는 못 건너뛴다. 안전장치는 겹칠수록 좋다 — 특히 되돌리기 어려운 일(잘못된 코드가 main에 합쳐지는 것)을 막는 장치라면.

[짚고 가기] 자주 하는 오해가 하나 있다 — "CI가 있으면 로컬 훅은 지워도 되잖아?" 지우지 않는 편이 좋다. 로컬 훅이 없으면 깨진 코드를 push한 뒤 CI 결과(보통 수십 초~수 분)를 기다렸다가 다시 고쳐 push하는 왕복이 반복된다. 로컬 훅은 그 왕복을 커밋 전에 없애 준다. "CI만 믿으면 느려지고, 훅만 믿으면 샌다"가 정확한 정리다.

이것이 '자동화'의 진짜 의미

지금까지 우리는 검증을 만들었다 — 테스트, 리뷰어, 보안 점검. CI의 역할은 그 검증들을 사람이 기억하지 않아도 매번 돌게 만드는 것이다. 좋은 자동화의 기준은 하나다: "안 하면 어떻게 되지?"의 답이 '자동으로 막힌다'인가. 잊을 수 있는 규칙은 규칙이 아니라 권고다. CI는 권고를 규칙으로 바꾼다.

이 기준을 조금 더 실전용으로 벼려 두자. 팀에서 "앞으로 배포 전엔 꼭 테스트 돌립시다" 같은 다짐을 회의에서 정하는 경우가 많은데, 이런 다짐의 운명을 가르는 질문은 딱 하나다.

"그걸 깜빡한 사람에게 무슨 일이 일어나는가?"

좋은 자동화와 나쁜 자동화를 이 잣대로 갈라 보면 판단이 쉬워진다.

흔한 방식 "안 하면?"의 답 판정
"커밋 전에 테스트 돌리자"고 회의에서 약속 깜빡해도 아무 일 없음 권고 (샌다)
README에 "배포 전 체크리스트" 적어 둠 안 읽어도 그만 권고 (샌다)
로컬 커밋 훅 설치 훅 지우거나 미설치면 무력 절반의 규칙
CI + 브랜치 보호 빨간 X면 병합 버튼이 잠김 규칙 (못 샌다)

핵심은 자동화의 가치를 '편해지는가'가 아니라 '못 빠져나가는가'로 잰다는 것이다. 알림을 띄우는 자동화, 리포트를 이메일로 보내는 자동화는 편리하지만, 무시할 수 있으면 결국 권고에 머문다. CI에 브랜치 보호를 얹는 이 장의 작업이 특별한 이유는, 그것이 무시할 수 없는 자동화이기 때문이다. 사람이 "그냥 합치자"고 해도 시스템이 "안 됩니다"라고 막는다.

[짚고 가기] 이 "안 하면 자동으로 막히는가" 잣대는 이 장의 한 줄 요약이자, 실무에서 두고두고 쓰는 사고 도구다. 내가 속한 회사·학교의 규칙 중 사실은 권고인 것을 하나씩 떠올려 보면 개념이 확 붙는다(예: "PR에 설명 꼭 쓰기" 같은 것 대부분은 권고다). 그 권고를 어떻게 규칙으로 바꿀지 상상해 보면, CI/자동화의 본질이 손에 잡힌다.


19.2 용어 정리

이 장에서 처음 만나는 용어들이다. 앞의 개념을 실제로 세우려면 이 단어들이 눈에 익어야 한다. 완벽히 외울 필요는 없다 — 워크플로 파일(YAML)을 읽을 때 "아, 이게 그거구나" 하고 알아보는 정도면 충분하다. 어차피 작성은 AI가 하고, 우리는 읽고 판단한다.

CI / CD

CI가 "합쳐도 안전한가"를 묻는다면, CD는 "안전하니 내보내자"를 실행한다. 오늘은 CI — 관문을 세우는 데 집중한다.

두 글자를 조금 더 풀어 두자. CI는 항상 Continuous Integration(지속적 통합) 이다. 그런데 CD는 두 가지 뜻이 섞여 쓰인다 — Continuous Delivery(지속적 전달)와 Continuous Deployment(지속적 배포)다. 둘의 차이는 "마지막에 사람이 버튼을 누르느냐"다.

약자 풀네임 무엇을 사람 개입
CI Continuous Integration 합칠 때마다 자동 검사 검사 통과/실패를 사람이 확인
CD Continuous Delivery(전달) 배포 직전까지 자동 준비 배포 버튼은 사람이 누름
CD Continuous Deployment(배포) 검사 통과하면 배포까지 자동 사람 개입 없음

이 장에서 우리가 세우는 건 그중 CI 하나다. "검사가 자동으로 돈다"까지만 온티켓에 붙인다. "통과한 코드를 실제 서버로 내보낸다"는 그다음 이야기이고, 배포 자체를 다룰 때 이어 붙인다. 지금 이 순서가 중요하다 — 검사도 못 믿는 코드를 자동으로 배포하는 것만큼 위험한 게 없다. 그래서 CI(믿을 수 있는 검사)를 먼저 세우고, CD(자동 배포)는 그 위에 얹는다.

GitHub과 저장소 — 배경 먼저

'CI가 저장소에 올라오면 돈다'고 했는데, 그 저장소GitHub부터 짚고 가자.

CI가 "코드가 저장소에 올라오는 순간 돈다"고 할 때, 그 '올라오는 순간'이란 바로 이 push 또는 PR 이 일어나는 순간을 말한다. GitHub이 그 사건을 감지해서 CI를 켜 준다.

GitHub Actions

GitHub이 제공하는 CI/CD 도구. 저장소에 워크플로 파일을 넣어 두면, 지정한 사건(push, PR 등)이 일어날 때 GitHub이 클라우드에서 그 작업을 실행해 준다. 별도 서버 계약이 필요 없고, 공개 저장소나 소규모 사용은 무료 사용량 안에서 돈다 — 우리 온티켓에 충분하다.

이름을 그대로 읽으면 'GitHub의 행동들'이다. 저장소에서 어떤 사건이 일어났을 때 GitHub이 대신 취해 주는 일련의 행동을 묶어 놓은 것이다. 왜 이게 편한지 대안과 비교하면 분명해진다.

요금 감각도 잡아 두자. GitHub Actions는 '검사에 쓴 컴퓨터 시간(분)'으로 과금하는데, 공개(public) 저장소는 이 시간이 완전 무료이고, 비공개(private) 저장소도 매달 일정 무료 사용량(개인 계정 기준 월 2,000분 수준)이 주어진다. 온티켓 CI 한 번은 대개 1~3분 남짓이라, 하루에 수십 번을 돌려도 소규모 프로젝트는 무료 범위를 넘기기 어렵다. "돈이 새는 것 아니냐"는 걱정은 이 단계에선 접어 두어도 된다.

[짚고 가기] "GitHub Actions 말고 다른 CI도 있나?" 물론 있다. CircleCI, GitLab CI, Jenkins 등 대안이 있지만, 온티켓 코드가 이미 GitHub에 있으니 같은 곳에서 검사까지 되는 GitHub Actions가 가장 마찰이 적다. "코드가 사는 집에 검사 설비가 붙어 있는 격"이라고 정리하면 왜 이걸 고르는지 납득된다.

워크플로, 잡, 스텝

GitHub Actions의 구조는 3층이다.

온티켓 예
워크플로(workflow) 하나의 자동화 전체. YAML 파일 하나 "PR이 오면 테스트한다"
잡(job) 워크플로 안의 독립된 작업 단위 "api 테스트" 잡
스텝(step) 잡 안의 개별 명령 코드 받기 → 의존성 설치 → pnpm test

이 3층을 회사 업무에 빗대면 한 번에 들어온다.

용어 하나 더 — 스텝에는 두 종류의 실행 방식이 있다. YAML에서 run:으로 시작하면 직접 명령을 치는 스텝이고(예: pnpm test), uses:로 시작하면 남이 미리 만들어 둔 행동 묶음을 가져다 쓰는 스텝이다(예: actions/checkout — "코드 받아오기"라는 흔한 작업을 누군가 포장해 둔 것). uses:는 "이 작업은 바퀴를 새로 발명하지 말고 만들어진 걸 쓰자"는 뜻으로 읽으면 된다.

YAML — 설정을 적는 언어

YAML은 설정을 사람이 읽기 쉽게 적는 형식이다. 들여쓰기로 구조를 표현하는 것이 특징이자 함정이다 — 들여쓰기 한 칸이 문법이라, 공백이 어긋나면 동작을 안 한다. 다행히 작성은 AI가 하고, 우리는 읽는다. 워크플로 파일은 위에서 아래로 "무슨 사건에 → 무슨 잡을 → 어떤 스텝 순서로"를 읽어 내면 된다.

'YAML'이라는 이름부터 재미있다. 처음엔 'Yet Another Markup Language(또 하나의 마크업 언어)' 였다가, 나중에 'YAML Ain't Markup Language(YAML은 마크업 언어가 아니다)'로 뜻을 바꿔 달았다. 자기 이름 안에 자기 이름이 들어가는 말장난인데, 요지는 "복잡한 태그 언어가 아니라, 사람이 그냥 읽을 수 있는 설정 형식을 지향한다" 는 선언이다. 실제로 YAML은 개발자가 아니어도 대충 눈으로 읽어 낼 수 있을 만큼 생김새가 순하다.

YAML을 읽는 세 가지 규칙만 알면 이 장의 워크플로 파일은 다 읽힌다.

  1. 이름: 값 — 콜론은 "이것은 이것이다" name: CI는 "name이라는 항목의 값은 CI다"로 읽는다. 콜론 왼쪽은 항목 이름, 오른쪽은 값이다.

  2. 들여쓰기(공백)는 "안에 속함"을 뜻한다 — 이게 핵심 문법 더 깊이 들여 쓴 줄은 바로 위 항목에 딸린 하위 항목이다. 아래를 보자. yaml jobs: test: runs-on: ubuntu-latest "jobs 안에 test라는 잡이 있고, 그 test 안에 runs-on 설정이 있다"로 읽는다. 왼쪽 여백이 깊어질수록 '더 안쪽'이다. 탭(Tab) 문자는 쓰지 않고 반드시 공백(스페이스) 으로 들여쓴다 — 이것이 YAML의 가장 흔한 함정이다.

  3. - (붙임표+공백)는 "목록의 한 항목" 줄 맨 앞의 -는 "여기부터 목록의 한 개"라는 표시다. ```yaml steps:

    • uses: actions/checkout@v4
    • run: pnpm install `` "steps라는 목록에 항목이 둘 있는데, 첫째는 checkout을 쓰고, 둘째는 pnpm install을 실행한다"로 읽는다.-`가 세 개면 순서대로 실행되는 스텝이 세 개다.

곁들여 알아 둘 부호 두 가지: #으로 시작하는 줄은 주석(사람 메모, 실행에 영향 없음)이고, [main]처럼 대괄호로 묶은 것은 목록을 한 줄로 짧게 쓴 것이다(branches: [main]은 "branches 목록에 main 하나"라는 뜻).

이제 앞에서 배운 규칙으로 실제 워크플로를 읽어 보자.

# .github/workflows/ci.yml — 읽기용 예시
name: CI
on:                       # 언제 도나
  pull_request:           #  - PR이 올라올 때
  push:
    branches: [main]      #  - main에 push될 때
jobs:
  test:                   # test 라는 잡
    runs-on: ubuntu-latest   # 클라우드의 리눅스 머신에서
    steps:
      - uses: actions/checkout@v4     # 1) 코드를 받아온다
      - uses: pnpm/action-setup@v4    # 2) pnpm 준비
      - run: pnpm install             # 3) 의존성 설치
      - run: pnpm test                # 4) 테스트 실행 ← 실패하면 잡 실패

이 파일을 소리 내어 읽어 보라 — "PR이나 main push가 있으면, 리눅스에서, 코드 받고, pnpm 깔고, 테스트를 돌린다." 그게 전부다. CI는 어렵지 않다.

소리 내어 읽는 법을 한 줄씩 더 천천히 밟아 보자. 워크플로 파일은 크게 on(언제), jobs(무엇을) 두 덩어리로 나뉜다. 이 두 단어만 눈으로 찾으면 나머지는 딸림 정보다.

YAML 줄 소리 내어 읽으면
name: CI "이 워크플로의 이름은 CI다" (Actions 탭에 이 이름으로 뜬다)
on: "언제 이걸 돌릴지 지금부터 적는다"
pull_request: "PR이 올라오면 돈다"
push: branches: [main] "그리고 main 브랜치에 push되면 돈다"
jobs: "이제 무슨 일을 할지 적는다"
test: "test라는 이름의 일감(잡)이 하나 있다"
runs-on: ubuntu-latest "그 일은 클라우드의 최신 우분투(리눅스) 머신을 빌려서 한다"
steps: "그 일의 손동작을 순서대로 적는다"
- uses: actions/checkout@v4 "1. 저장소 코드를 이 머신으로 받아온다"
- uses: pnpm/action-setup@v4 "2. pnpm(패키지 관리 도구)을 설치·준비한다"
- run: pnpm install "3. 프로젝트가 필요로 하는 부품(의존성)을 설치한다"
- run: pnpm test "4. 테스트를 돌린다. 여기서 실패하면 이 잡 전체가 실패한다"

@v4 같은 꼬리표는 "그 도구의 4번째 버전을 쓴다"는 버전 고정 표시다. 버전을 박아 두면 남이 만든 도구가 나중에 바뀌어도 우리 CI가 갑자기 다르게 돌지 않는다. 읽을 때는 "checkout, 그중 v4 버전" 정도로 넘어가면 된다.

ubuntu-latest우분투(Ubuntu) 는 리눅스 운영체제의 한 종류다. CI 서버로 리눅스를 쓰는 이유는 가볍고 빠르고 (특히 공개 저장소에선) 무료 사용량이 넉넉하기 때문이다. 내 노트북이 윈도우든 맥이든 상관없다 — 검사는 이 중립 리눅스 머신에서 도니까, 앞서 말한 '중립'이 YAML의 이 한 줄로 실현되는 셈이다.

[짚고 가기] YAML 들여쓰기는 가장 많이 데는 지점이다. 하지만 이 장에서 우리는 YAML을 쓰지 않고 읽기만 한다는 점을 기억해 두면 좋다. AI가 만든 파일을 열어, 위 표처럼 "on은 언제, jobs는 무엇"만 짚어 소리 내어 읽어 보면 충분하다. 굳이 직접 공백을 세며 편집할 필요는 없다 — 그 순간 '읽는 사람'에서 '디버깅하는 사람'으로 떨어지고, 이 장의 리듬이 무너진다. 들여쓰기가 틀렸으면 AI에게 "이 YAML 문법 검사해서 고쳐줘"라고 시키면 된다.

상태 체크와 브랜치 보호

CI가 돌면 결과가 PR 화면에 초록 체크(통과) 또는 빨간 X(실패) 로 표시된다. 여기에 브랜치 보호 규칙을 걸면 — "체크가 초록이 아니면 main에 병합 불가"를 GitHub이 강제한다. 이것이 관문의 자물쇠다. 사람이 "그냥 합치자"고 해도, 빨간 X면 병합 버튼이 잠긴다.

두 용어를 분리해서 잡아 두자. 헷갈리면 관문이 왜 잠기는지가 흐려진다.

상태 체크는 '경고등', 브랜치 보호는 '경고등이 빨가면 문을 잠그는 장치' 다. 경고등만 있으면 사람이 무시하고 지나갈 수 있다(권고). 잠금 장치를 걸어야 무시할 수 없게 된다(규칙). 이 장 첫머리에서 본 "권고를 규칙으로 바꾼다"가 바로 이 두 번째 설정에서 완성된다. 그래서 이 장의 실습은 CI를 세우고 시험하는 데서 끝나지 않고, 반드시 브랜치 보호까지 가야 관문이 진짜 잠긴다.


19.3 개념 — 무엇을 관문에 세울까

CI에 넣을 검사는 로컬에서 이미 만든 것들이다. CI는 새 검사를 발명하는 게 아니라, 기존 검사를 우회 불가능한 위치로 옮기는 것이다.

여기서 '무엇을 관문에 세울지'를 고르는 기준을 다시 이 장 첫머리의 "안 하면 자동으로 막히는가" 잣대로 정리하면 명확하다. "이 검사를 사람이 깜빡했을 때, 자동으로 막혔으면 하는가?" 답이 '그렇다'인 것부터 관문에 올린다. 온티켓에서 그 답이 가장 분명한 것이 테스트다. 예매·결제·좌석 계산 로직이 깨진 채로 main에 합쳐지면 곧바로 사고이므로, "테스트 실패 시 자동으로 막힘"의 가치가 가장 크다.

각 검사가 관문에서 무엇을 지키는지 온티켓 맥락으로 붙여 보자.

관문에 세울 검사 CI에서 하는 일 온티켓에서 막는 사고
테스트 (pnpm test) 핵심 로직이 의도대로 도는지 확인 좌석 수 계산·결제 금액 로직이 깨진 채 병합
시크릿 스캔 결제 키·.env 내용이 코드에 섞였는지 확인 토스페이먼츠 실제 키가 저장소에 노출
빌드 확인(이후) 배포 가능한 형태로 만들어지는지 확인 "내 PC에선 됐는데" 빌드가 서버에서 깨짐
린트(이후) 코드 스타일·명백한 실수 자동 점검 자잘한 오류가 리뷰 시간을 잡아먹음

처음부터 다 넣지 않는다. 테스트 하나로 시작해 초록/빨강이 도는 것을 확인하고, 관문이 작동하면 검사를 하나씩 얹는다. CI도 MVP처럼 키운다 — 안 도는 거대한 파이프라인보다, 도는 작은 관문이 낫다.

"테스트 하나로 시작" 을 강조하는 데는 실전 이유가 있다. 처음부터 테스트·빌드·린트·시크릿 스캔을 다 욱여넣으면, 무언가 빨간 X가 떴을 때 어느 스텝이 왜 실패했는지 가려내기가 어렵다. 게다가 CI를 처음 세울 땐 워크플로 파일 자체의 사소한 문제(도구 버전, 설치 순서 등)로 실패하기 쉬운데, 스텝이 하나뿐이면 원인이 뻔히 보인다. 작동하는 관문 하나를 먼저 확실히 세운 뒤, 그 위에 스텝을 한 개씩 얹으며 매번 "여전히 잘 도는가"를 확인하는 것이 결국 가장 빠르다. 이것이 '도는 작은 관문에서 시작한다'는 말의 실전적 의미다.

[짚고 가기] "MVP처럼 키운다"는 말은 온티켓 전체를 관통하는 태도다. 소개 페이지 한 장에서 시작해 예매·결제로 키워 온 것과 똑같이, CI도 테스트 스텝 하나에서 시작해 검사를 얹으며 키운다. "완성된 거대한 것을 한 번에"가 아니라 "도는 작은 것을 계속"이 이 강의의 일관된 리듬이다. 이 점을 기억해 두면 CI를 특별하고 어려운 무언가로 겁낼 이유가 없다.


19.4 프롬프트 작성법 — 파이프라인을 세우고 시험한다

CI 작업의 프롬프트도 지금까지와 원칙이 같다 — 목표 + 완료 기준 + (제약). 다만 CI는 "만들고 끝"이 아니라 "만들고, 일부러 실패시켜 감시가 작동하는지 확인" 까지가 한 세트다. 그래서 이 절의 프롬프트는 (1) 워크플로 생성, (2) 관문 시험(빨간 X 만들기), (3) 브랜치 보호 안내 — 세 종류로 나뉜다. 순서대로 보자.

CI 워크플로 생성 지시

프롬프트
GitHub Actions CI를 만들어줘. .github/workflows/ci.yml 로:
- 트리거: main 브랜치로의 push, 그리고 모든 PR
- 잡: 우분투에서 코드 체크아웃 → pnpm 설치 → 의존성 설치 → api 테스트(pnpm test) 실행
- 테스트가 실패하면 잡이 실패하도록
작성한 뒤, YAML 각 줄이 무슨 뜻인지 주석 없이 설명으로 풀어줘.

이 지시가 왜 좋은지 뜯어 보자. 목표(GitHub Actions CI 생성), 완료 기준(테스트 실패 시 잡 실패), 제약(파일 위치·트리거·스텝 순서를 명시)이 다 들어 있다. 특히 마지막 줄 "YAML 각 줄이 무슨 뜻인지 설명으로 풀어줘" 가 이 장의 핵심 습관이다 — 우리는 YAML을 쓰지 않고 읽는 사람이므로, AI가 만든 파일을 사람이 이해할 수 있게 풀이까지 시키는 것이다.

프롬프트 강도를 비교해 보면 왜 이렇게 적는지 보인다.

약한 지시 강한 지시 무엇을 바꿨나
CI 좀 만들어줘 위 생성 지시 전체 파일 위치·트리거·스텝·실패 조건을 못 박음
테스트 자동으로 돌게 해줘 main push와 모든 PR에서 pnpm test가 돌고, 실패하면 잡이 실패하게 '자동으로'를 구체적 사건(push·PR)과 판정(실패)으로
YAML 만들어줘 …작성한 뒤 각 줄이 무슨 뜻인지 설명으로 풀어줘 결과물뿐 아니라 '내가 읽을 수 있게'까지 요구

실전 팁 몇 가지를 덧붙인다.

관문 시험 지시 — 빨간 X를 일부러 만든다

CI가 진짜 감시하는지는, 역시 일부러 실패시켜 확인한다.

프롬프트
CI가 작동하는지 시험하자. 테스트를 하나 실패하게 만든 브랜치를 만들고,
PR을 올려줘. GitHub Actions가 빨간 X를 띄우는지 확인하려는 거야.
확인되면 테스트를 고쳐서 초록으로 바뀌는 것까지 보여줘.

이 "일부러 실패시켜 본다"가 왜 중요한가. 초록 체크는 CI가 작동한다는 증거가 아니다. 아무것도 검사하지 않는 워크플로도 항상 초록이 뜬다. 관문이 진짜 감시하는지는 빨간 X를 띄울 수 있는가로만 증명된다. 화재경보기를 설치한 뒤 연기를 한번 피워 봐야 "울리는 경보기"인지 아는 것과 같다. 그래서 CI를 세우면 반드시 한 번은 일부러 깨뜨려 빨간 X를 확인한다.

이 시험 지시도 강도를 올릴 수 있다.

약한 지시 강한 지시
CI 잘 되나 테스트해줘 테스트를 하나 일부러 실패하게 만든 브랜치를 파서 PR을 올리고, Actions에 빨간 X가 뜨는지, 실패 로그에 어느 테스트가 왜 실패했는지 나오는지 확인해줘. 그다음 고쳐서 초록으로 되돌려줘

"실패 로그에 어느 테스트가 왜 실패했는지 나오는지"까지 확인하라고 시키는 이유가 있다. 좋은 CI는 빨간 X만 던지지 않고 실패 원인을 로그로 알려 준다. 그 로그를 읽는 감각을 여기서 익혀 두면, 나중에 진짜로 CI가 빨개졌을 때 당황하지 않고 로그부터 열어 보게 된다.

브랜치 보호 안내 요청

브랜치 보호 규칙은 GitHub 웹 화면의 설정이라, 사람이 클릭해야 하는 부분이 있다. AI에게 절차를 안내받아 사람이 실행한다.

프롬프트
main 브랜치에 "CI 통과해야 병합 가능" 보호 규칙을 걸고 싶어.
GitHub 웹에서 어디를 어떻게 설정하는지 순서대로 알려줘.

(설정 변경은 사람의 일이다 — 인프라를 세울 때 정한 원칙. gh CLI로 가능한 부분이 있으면 그것도 알려 달라고 해도 좋다.)

왜 이건 "만들어줘"가 아니라 "안내해줘"인가. 코드 파일은 AI가 저장소에 직접 만들 수 있지만, 브랜치 보호 규칙은 GitHub 계정 소유자의 웹 설정 화면에서 바꾸는 것이라 사람이 직접 클릭해야 하는 경우가 많다. 게다가 "누가 main을 잠글 수 있는가"는 되돌리기 어렵고 권한이 걸린 결정이므로, 사람이 화면을 보며 확인하고 누르는 편이 안전하다. 그래서 AI에게는 손이 아니라 안내서를 요청한다.

안내를 받을 때 이렇게 덧붙이면 더 실전적이다.

[짚고 가기] "왜 이건 AI가 그냥 안 해주지?" 싶을 수 있다. 일부러 안 시키는 것이다. main을 잠그는 권한 설정은 서비스의 안전벨트라, 사람이 화면을 보고 이해한 뒤 자기 손으로 채우는 경험을 한 번은 해 보는 게 좋다. "AI가 대신 눌러 주면 편하지만, 이 버튼만큼은 내가 무엇을 잠그는지 알고 눌러야 한다" — 이 태도를 몸에 새기는 자리다.


19.5 실습 — 클라우드에 관문을 세운다

이 실습의 목표는 온티켓 저장소에 "코드가 올라오면 자동으로 테스트가 돌고, 실패하면 병합이 잠기는" 관문을 세우는 것이다. 앞 절의 세 프롬프트를 순서대로 실행하며, 매 단계 결과를 눈으로 확인한다. 준비물은 하나 — 온티켓 코드가 GitHub 저장소에 올라와 있어야 한다(push된 상태).

따라하기 1 — CI 워크플로 생성과 push

아래 생성 지시를 실행하고, 만들어진 .github/workflows/ci.yml을 연다. 앞의 용어 정리에서 읽어 본 예시와 비교하며 "무슨 사건에 무슨 스텝"인지 읽어 낸다. 그리고 커밋·push.

프롬프트
GitHub Actions CI를 만들어줘. .github/workflows/ci.yml 로:
- 트리거: main 브랜치로의 push, 그리고 모든 PR
- 잡: 우분투에서 코드 체크아웃 → pnpm 설치 → 의존성 설치 → api 테스트(pnpm test) 실행
- 테스트가 실패하면 잡이 실패하도록
작성한 뒤, YAML 각 줄이 무슨 뜻인지 주석 없이 설명으로 풀어줘.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

여기서 파일이 놓이는 위치를 한 번 짚자. 워크플로 파일은 반드시 저장소의 .github/workflows/ 폴더 안에 있어야 GitHub이 인식한다. 폴더 이름이 점(.)으로 시작하는 '숨김 폴더'라 파일 탐색기에선 안 보일 수 있는데, VS Code 탐색기에는 정상적으로 나타난다. 경로가 정확한지 — .github/workflows/ci.yml — 를 확인하는 것이 첫 관문이다.

파일을 열었으면, 앞의 용어 정리에 있는 '소리 내어 읽는 법' 표를 옆에 두고 실제 파일을 한 줄씩 읽어 본다. on은 어디 있고 무슨 사건들이 적혔는지, jobs 아래 잡 이름은 무엇인지, steps-가 몇 개이고 마지막이 pnpm test인지. "내가 이 파일을 읽고 설명할 수 있는가" 가 이 따라하기의 진짜 완료 조건이다(완료 기준 체크리스트의 첫 항목이 바로 이것이다).

따라하기 2 — Actions가 도는 것을 본다

GitHub 저장소의 Actions 탭을 연다. 방금 push로 워크플로가 실행되는 것이 보인다 — 클릭하면 스텝이 하나씩 진행되는 로그가 뜬다. 내 컴퓨터가 아닌 클라우드에서 우리 테스트가 도는 장면이다. 초록 체크로 끝나는지 확인한다.

이 장면을 학생이 직접 눈으로 보는 것이 이 장 첫머리의 '중립 클라우드' 개념을 체감하는 순간이다. 내 노트북에서는 아무 명령도 치지 않았는데, GitHub의 어떤 리눅스 머신이 우리 코드를 받아 pnpm installpnpm test를 대신 돌리고 있다. 로그를 펼치면 스텝별로 초록 체크가 하나씩 켜지는 것이 보이는데, 이게 바로 YAML의 - 목록이 위에서 아래로 실행되는 모습이다. 파일에서 읽은 순서와 화면에서 도는 순서가 같다는 것을 확인시키면 개념이 딱 맞물린다.

[짚고 가기] 첫 실행이 빨간 X로 끝나는 경우가 꽤 있다 — 대개 코드 버그가 아니라 워크플로 설정 문제다(pnpm 버전, lock 파일 유무, 테스트 스크립트 이름 등). 당황하지 말고 로그의 빨간 스텝을 펼쳐 마지막 에러 몇 줄을 복사해서 AI에게 "이 CI 로그를 보고 워크플로를 고쳐줘"라고 주면 된다. 이 과정 자체가 좋은 경험이다 — "CI가 실패를 어떻게 알려 주는지, 그리고 그 로그로 어떻게 고치는지"를 직접 겪고 나면, 바로 다음의 '일부러 실패' 시험이 자연스레 이어진다.

따라하기 3 — 빨간 X를 만든다 (관문 시험)

아래 시험 지시를 실행한다. 실패 브랜치로 PR을 올리고, PR 화면 하단의 체크 영역에 빨간 X가 뜨는 것을 본다. 로그를 열어 "어느 테스트가 왜 실패했는지"까지 CI가 알려 주는 것을 확인한다. 그다음 고쳐서 초록으로.

프롬프트
CI가 작동하는지 시험하자. 테스트를 하나 실패하게 만든 브랜치를 만들고,
PR을 올려줘. GitHub Actions가 빨간 X를 띄우는지 확인하려는 거야.
확인되면 테스트를 고쳐서 초록으로 바뀌는 것까지 보여줘.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

여기서 잠깐 멈춰 이 단계의 의미를 새기자. 지금 우리는 관문이 작동하는지 시험하고 있다. 초록만 보고 "됐다"고 넘어가면, 사실은 아무것도 검사 안 하는 관문일 수도 있다(위 '프롬프트 작성법'에서 본 화재경보기 이야기). 그래서 일부러 테스트 하나를 깨뜨려 빨간 X를 만들어 본다. 빨간 X가 뜨고, 로그에 실패한 테스트 이름과 이유가 나온다면 — 관문이 진짜로 눈을 뜨고 있다는 증거다. 확인했으면 바로 고쳐서 초록으로 되돌린다(저장소를 깨진 채로 두지 않는다).

이때 PR 화면 하단의 체크 영역을 눈여겨보게 한다. 아직 브랜치 보호를 안 걸었으므로, 빨간 X가 떠 있어도 병합 버튼은 여전히 눌러진다. "경고등은 켜졌는데 문은 안 잠긴" 상태다. 이 어색함을 학생이 느끼는 것이 중요하다 — 그 어색함을 해소하는 것이 바로 다음 따라하기다.

따라하기 4 — 병합을 잠근다

아래 안내 요청으로 절차를 받아, GitHub 웹에서 main 브랜치 보호 규칙을 설정한다 (Settings → Branches → 보호 규칙 추가 → 상태 체크 필수). 설정 후, 빨간 X 상태의 PR에서 병합 버튼이 잠기는지 확인한다. 관문에 자물쇠가 걸렸다.

프롬프트
main 브랜치에 "CI 통과해야 병합 가능" 보호 규칙을 걸고 싶어.
GitHub 웹에서 어디를 어떻게 설정하는지 순서대로 알려줘.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

설정의 큰 흐름은 이렇다(화면 문구는 GitHub 업데이트에 따라 조금씩 다를 수 있으니, AI에게 받은 최신 안내를 함께 본다).

  1. 저장소 상단 Settings(설정) 탭 → 왼쪽 메뉴에서 Branches(브랜치)
  2. 브랜치 보호 규칙 추가(Add rule / Add branch ruleset). 대상 브랜치 이름에 main
  3. "Require status checks to pass before merging"(병합 전 상태 체크 통과 필수)를 켠다
  4. 목록에서 우리 CI의 상태 체크(ci.yml에 만든 잡, 예: test)를 필수로 지정
  5. 저장

이 설정을 켜는 순간, 관문 시험 때 봤던 "빨간 X인데도 눌러지던 병합 버튼"이 잠긴다. 같은 빨간 X, 같은 PR인데 이제는 병합이 막힌다. 무엇이 달라졌나 — 상태 체크(경고등)는 그대로이고, 거기에 브랜치 보호(잠금 장치) 를 얹었을 뿐이다. 앞의 용어 정리에서 나눠 둔 두 용어가 여기서 하나로 맞물린다. 이 전후 대비를 학생이 직접 보는 것이 이 장 전체의 클라이맥스다.

[짚고 가기] 이 순간이 이 장에서 가장 강렬한 장면이다. 같은 빨간 X 상태의 PR을 두고, 브랜치 보호를 켜기 전(병합 버튼 활성)과 켠 후(병합 버튼 잠김)를 나란히 비교해 보면 "권고가 규칙으로 바뀌는" 순간이 눈에 박힌다. 참고로 "관리자는 강제로 병합할 수 있게 둘 수도, 관리자조차 못 하게 막을 수도 있다"는 옵션 차이가 있는데, 팀 규모에 따라 얼마나 조이는지가 정책 선택임을 알아두면 좋다. 무료 개인 저장소에서 일부 옵션이 안 보일 수 있는데, "상태 체크 필수" 핵심 기능은 대개 되므로 그걸 중심으로 진행하면 된다.

따라하기 5 — 관문을 하나 더 (선택)

테스트 관문이 돌면, 시크릿 스캔 스텝을 얹어 본다.

프롬프트
ci.yml 에 스텝을 추가해줘: 코드에 test_sk_ 가 아닌 결제 키나 .env 내용이
섞여 들어왔는지 검사하고, 발견되면 잡을 실패시켜.

위 프롬프트를 그대로 복사해 입력하면 된다. 다른 내용으로 바꿔서 해 보고 싶다면 자유롭게 바꿔도 좋다.

CI는 이렇게 스텝을 얹으며 자란다 — 도는 작은 관문에서 시작해서.

이 시크릿 스캔이 온티켓에서 왜 중요한지 짚어 두자. 온티켓은 토스페이먼츠를 테스트 모드로 쓴다. 테스트 키는 보통 test_로 시작해 노출돼도 실제 돈이 오가지 않지만, 실제 서비스 키 (라이브 키)나 .env에 담긴 데이터베이스 접속 정보가 실수로 코드에 섞여 저장소에 올라가면 그건 즉시 사고다. 로컬 커밋 훅에서 이미 걸러 왔지만, 앞서 본 대로 로컬 훅은 샐 수 있다. 그래서 같은 검사를 CI에도 얹어 서버에서 한 번 더 막는다 — 이것이 '기존 검사를 우회 불가능한 위치로 옮긴다'는 이 장의 원칙을 구체적으로 실천한 것이다.

여기서 얻는 감각이 이 장의 마무리다. 관문은 한 번에 완성되는 게 아니라 스텝을 얹으며 자란다. 테스트라는 도는 작은 관문 하나를 세워 두었기에, 시크릿 스캔을 얹을 때도 "관문이 여전히 잘 도는가"만 확인하면 된다. 나중에 빌드 확인·린트를 더 얹을 때도 리듬은 같다. 새 스텝을 추가하고, 초록으로 도는 걸 확인하고, 필요하면 일부러 깨뜨려 빨간 X까지 확인한다.

완료 기준 체크리스트

이 네 줄은 각각 이 장의 핵심 하나씩과 맞물린다. 첫째 줄은 YAML 읽기(쓰기가 아니라 읽고 설명하기), 둘째 줄은 중립 클라우드(내 컴퓨터 밖에서 도는 검사), 셋째 줄은 상태 체크 (초록/빨강 판정), 넷째 줄은 브랜치 보호(권고를 규칙으로). 네 개가 다 체크되면 온티켓에는 '사람의 성실함에 기대지 않는' 관문이 실제로 서 있는 것이다.


19.6 정리

오명운 · macro@prag-ai.com