이번 편의 결과물: PR을 올리면 GitHub Actions가 자동으로 lint·테스트·빌드를 실행하고 결과가 PR에 체크로 표시됩니다. · 다루는 개념: 워크플로 작성(.github/workflows/), lint·단위/통합 테스트·빌드 자동 실행, PR 상태 체크
이 편에서 만드는 파일
bookshelf/
├── .github/
│ └ workflows/
│ └ ci.yml ← + lint·test·build 워크플로
└── package.json ← ~ lint 스크립트 확인, ci 스크립트 추가개념 정리
CI가 검사의 관문이 되는 구조
CI(Continuous Integration, 지속적 통합)는 코드 변경마다 자동으로 빌드·검증하는 관행입니다. GitHub Actions는 저장소 이벤트(push, PR)에 반응해 워크플로(workflow)라는 자동화 스크립트를 실행합니다. 워크플로는 .github/workflows/ 아래 YAML 파일로 정의합니다.
| 개념 | 뜻 |
|---|---|
| workflow | 하나의 YAML 파일. 언제(on) 무엇을(jobs) 실행할지 정의 |
| job | 워크플로 안의 독립된 실행 단위. 기본적으로 병렬 실행 |
| step | job 안에서 순서대로 실행되는 명령 하나하나 |
| action | 재사용 가능한 step 묶음(actions/checkout@v4 등) |
npm ci와 npm install의 차이
CI 환경에서는 npm install 대신 npm ci를 씁니다. npm ci는 package-lock.json에 잠긴 버전을 정확히 그대로 설치하고, node_modules가 이미 있으면 지우고 새로 설치합니다. “내 컴퓨터에서는 되는데 CI에서는 실패하는” 버전 불일치 문제를 줄입니다.
실패하면 병합을 막는다
워크플로의 어떤 step이든 0이 아닌 종료 코드로 끝나면 job이 실패로 표시됩니다. GitHub 저장소 설정에서 Settings → Branches의 필수 상태 체크(required status check)로 이 워크플로를 지정하면, 실패한 PR은 main에 병합할 수 없습니다. 테스트를 작성하는 것과 그 테스트를 병합의 관문으로 세우는 것은 별개의 설정입니다.
실습
1. package.json에 lint·test·build 스크립트 확인하기
// package.json (scripts 발췌)
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --max-warnings=0",
"test": "vitest run",
"test:e2e": "playwright test"
}
}test는 vitest run으로 고정합니다(감시 모드 vitest는 CI에서 종료되지 않아 워크플로가 멈춥니다). test:e2e는 16편에서 만든 Playwright 스위트로, 이번 CI에는 시간이 오래 걸려 넣지 않고 별도 워크플로로 분리합니다(직접 해보기에서 확장).
2. 워크플로 파일 작성하기
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint-test-build:
runs-on: ubuntu-latest
steps:
- name: 저장소 코드 가져오기
uses: actions/checkout@v4
- name: Node 22 설정
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- name: 의존성 설치
run: npm ci
- name: 린트 검사
run: npm run lint
- name: 단위·통합 테스트
run: npm test
- name: 프로덕션 빌드
run: npm run build
env:
VITE_API_BASE_URL: http://localhost:3001on.pull_request에 branches: [main]을 지정해, main으로 향하는 PR마다 이 워크플로가 돈다는 뜻입니다. cache: npm은 setup-node가 package-lock.json 해시를 기준으로 의존성을 캐시해 반복 실행 속도를 높여 줍니다. 빌드 step의 env는 19편에서 만든 환경 변수를 CI 안에서도 주입합니다. 이 값은 빌드 시점에 번들에 박히기만 할 뿐 CI가 실제로 그 주소에 접속하지는 않으므로, 서버가 떠 있지 않아도 빌드는 통과합니다. 로컬 .env.production 파일은 CI에 올라가지 않으므로, 값을 직접 넘기거나 GitHub 저장소의 Secrets and variables 설정에 등록한 값을 참조합니다.
3. 커밋하고 PR 만들어 확인하기
git checkout -b ci/github-actions
git add .github/workflows/ci.yml package.json
git commit -m "ci: lint, test, build 워크플로 추가"
git push -u origin ci/github-actionsGitHub에서 이 브랜치로 PR을 엽니다. PR 화면 하단에 lint-test-build 체크가 나타나고, 잠시 후 성공(초록 체크) 또는 실패(빨간 X)로 바뀝니다. 일부러 npm run lint가 실패하도록 코드에 미사용 변수를 하나 남기고 push해, 체크가 실패로 바뀌는 것과 PR에 “이 브랜치에는 실패한 체크가 있습니다” 경고가 뜨는 것을 확인합니다. 확인 후 되돌립니다.
4. 필수 상태 체크로 지정하기
저장소 Settings → Branches → Branch protection rules에서 main에 대한 규칙을 추가하고, Require status checks to pass before merging을 켠 뒤 lint-test-build를 선택합니다. 이제부터 체크가 실패한 PR은 Merge 버튼이 비활성화됩니다.
직접 해보기
Playwright E2E 테스트를 별도 워크플로(.github/workflows/e2e.yml)로 분리하고, main에 push할 때만 실행되도록 만들어 보세요.
on: push: branches: [main]만 남기고, actions/checkout 이후 npx playwright install --with-deps step을 추가해 브라우저를 설치한 다음 npm run test:e2e를 실행합니다. PR마다 돌리기엔 느리므로 main 반영 시점에만 돌리는 것이 실무에서 흔한 절충입니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
| CI에서 vitest가 끝나지 않고 멈춘다 | test 스크립트가 감시 모드(vitest)로 되어 있음 | vitest run으로 1회 실행 모드 사용 |
| 로컬에서는 되는데 CI 빌드만 실패 | 환경 변수를 CI에 안 넘김 | 워크플로 env 또는 저장소 Secrets에 등록 |
| PR 체크가 안 보인다 | 워크플로의 on 조건에 pull_request가 없음 | on.pull_request.branches에 대상 브랜치 추가 |
확인 문제
react_3 마무리 — 완료 기준 점검
20편으로 bookshelf가 실무 수준으로 완성되었습니다. src/ 정본 파일 목록과 01편 학습 방향에서 정한 완료 기준을 하나씩 확인합니다.
bookshelf/src/
├── main.jsx # RouterProvider + QueryClientProvider + AuthProvider 렌더링
├── router.jsx # createBrowserRouter, 보호 라우트, 세션 만료 레이아웃
├── index.css # 전역 스타일, .sr-only, :focus-visible
├── pages/
│ ├── BookListPage.jsx # TanStack Query로 목록 조회, 정렬·필터·페이지네이션
│ ├── BookDetailPage.jsx
│ ├── NewBookPage.jsx # react-hook-form + zod, 접근성 보정
│ ├── LoginPage.jsx # 로그인 폼, aria-live 에러
│ └── NotFoundPage.jsx
├── components/
│ ├── Header.jsx, ErrorBoundary.jsx, ProtectedRoute.jsx, SessionWatcher.jsx
│ ├── BookTable.jsx, BookRow.jsx # 정렬·필터·페이지네이션, memo 적용
│ ├── Modal.jsx # 포커스 트랩, aria-modal
│ └── BookCard.jsx
├── context/{ThemeContext,AuthContext}.jsx
├── hooks/{useBooks,useBookMutations,useLocalStorage,useDebounce}.js
├── store/useUiStore.js # Zustand, selector 기반 구독
├── api/{client,books,auth}.js # 환경 변수 기반 API 클라이언트(json-server)
├── schemas/bookSchema.js # zod 검증 스키마
├── i18n/index.js, i18n/locales/{ko,en}.json # react-i18next 리소스
├── mocks/{handlers,server,browser}.js # 14~16편 테스트 전용(프로덕션 코드에서 import 안 함)
├── utils/onRenderCallback.js
├── test/ # Vitest + RTL 단위·통합 테스트
└── e2e/ # Playwright E2E| 완료 기준(01편 학습 목표) | 확인 방법 |
|---|---|
| 로그인 후 책을 검색·정렬·페이지네이션으로 조회 | json-server 실행 후 npm run preview에서 로그인 → / 조작 |
| 표지 이미지와 함께 등록·수정·삭제 | 책 등록 폼에서 이미지 업로드 후 저장 확인 |
| 언어를 한국어/영어로 전환 | 헤더의 언어 전환 버튼 클릭 |
| 단위·통합·E2E 테스트 모두 통과 | npm test, npm run test:e2e |
| Vercel(또는 GitHub Pages)에 배포되어 실제 URL로 접속 | 19편에서 발급된 URL 접속(API 기능은 로컬 json-server 필요) |
| GitHub Actions가 PR마다 lint·테스트·빌드 자동 실행 | PR을 열어 체크 상태 확인 |
여섯 항목이 모두 통과하면 bookshelf는 튜토리얼이 아니라 동작하는 배포된 앱 + 테스트 스위트 + CI로 완성된 것입니다. react_1의 컴포넌트·상태부터 react_3의 서버 상태·인증·성능·접근성·배포·CI까지, 20편에 걸쳐 쌓인 코드가 이 표의 근거입니다.