팀 프로젝트에서 배포를 진행한 직후, 팀원이 당황스러운 소식을 전해왔습니다. "아까 새로 고친 팝업창 버튼은 잘 되는데, 기존에 잘 되던 [로그인]이랑 [장바구니 담기] 기능이 클릭이 안 돼요!"

분명 내 파트 코드만 살짝 고치고 수동으로 클릭해 본 뒤 Merge했었는데, 공통으로 쓰이던 유틸리티 함수와 리액트 상태(State)가 꼬이면서 사이드 이펙트(Side Effect)가 발생했던 것입니다. 배포할 때마다 식은땀을 흘리며 깨달았습니다.

"프로젝트 규모가 커질수록, 사람이 수동으로 버튼을 하나씩 눌러보며 버그를 검증하는 것엔 한계가 있구나."

내가 작성하거나 고친 코드가 기존 기능에 영향을 미치지 않는다는 확신을 얻고, 마음 편히 배포하게 만들어준 테스트 자동화 구축기를 정리해 봅니다.

1. 현실적인 테스트 전략 (테스트 피라미드)

모든 코드에 테스트를 만드느라 정작 기능 구현보다 테스트 짜는 시간이 더 오래 걸리면 안 되기에, 저희 팀 프로젝트에 맞게 우선순위를 정해 테스트 피라미드를 구축했습니다.

  • 1단계: 유닛 테스트 (Unit Test - Jest)
    • UI와 상관없는 순수 핵심 로직(할인율 계산, 날짜 포맷팅, 복잡한 데이터 변환 함수)만 빠르게 검증합니다.
  • 2단계: 통합 테스트 (Integration Test - React Testing Library)
    • 내가 만든 컴포넌트에서 "사용자가 폼을 입력하고 버튼을 눌렀을 때 원하는 화면이 잘 나타나는가?"를 검증합니다.
  • 3단계: E2E 테스트 (End-to-End Test - Cypress)
    • 실제 브라우저를 띄워 [메인 ➔ 로그인 ➔ 장바구니 ➔ 결제]로 이어지는 핵심 서비스 동선만 통째로 검증합니다.

2. Jest와 RTL로 단단한 컴포넌트 만들기 (실제 적용 코드)

리액트 프로젝트에 React Testing Library(RTL)를 적용하면서 지킨 원칙은 "컴포넌트의 내부 state가 아니라, 실제 사용자의 행동을 기준으로 테스트하는 것"이었습니다.

 
// LoginForm.test.tsx - 로그인 폼 테스트 코드
import { render, screen, fireEvent } from '@testing-library/react';
import LoginForm from './LoginForm';

test('아이디와 비밀번호를 모두 입력해야만 로그인 버튼이 활성화된다', () => {
  render(<LoginForm />);
  
  const idInput = screen.getByLabelText(/아이디/i);
  const pwInput = screen.getByLabelText(/비밀번호/i);
  const submitButton = screen.getByRole('button', { name: /로그인/i });

  // 1. 처음엔 버튼이 비활성화(disabled) 상태여야 함
  expect(submitButton).toBeDisabled();

  // 2. 사용자가 폼에 타이핑을 진행
  fireEvent.change(idInput, { target: { value: 'yumina' } });
  fireEvent.change(pwInput, { target: { value: 'password123!' } });

  // 3. 두 값이 제대로 들어가면 버튼이 활성화되는지 검증
  expect(submitButton).toBeEnabled();
});

Jest 테스트가 통과된 화면 스크린샷

3. Cypress로 팀의 '핵심 서비스 동선' 구하기

유닛 테스트가 부분 부품을 검사한다면, Cypress는 "진짜 사용자가 들어와서 서비스를 끝까지 이용할 수 있는가"를 브라우저에서 직접 시뮬레이션해 줍니다.

// cypress/e2e/checkout.cy.js - 메인 플로우 검증
describe('핵심 서비스 플로우 검증', () => {
  it('상품 페이지에서 장바구니에 담고 결제 페이지까지 문제없이 이동한다', () => {
    cy.visit('/products/1');
    cy.contains('장바구니 담기').click();
    
    // 장바구니 카운트가 올라갔는지 검증
    cy.get('.cart-badge').should('contain', '1');
    
    cy.visit('/cart');
    cy.contains('주문하기').click();
    
    // 최종 결제 URL로 잘 넘어가는지 검증
    cy.url().should('include', '/checkout');
  });
});

Cypress를 붙여둔 덕분에, 배포 전 버튼들을 일일이 마우스로 클릭해 보지 않고도 명령어 하나로 전체 동작을 5초 만에 검증할 수 있게 되었습니다.

Cypress 테스트 러너 실행 결과 화면 스크린샷

 

4. TDD(테스트 주도 개발)에 대한 나의 생각

프로젝트 일정이 빡빡할 때는 모든 코드를 TDD로 짜는 것은 어려웠습니다. 그래서 제가 찾은 타협점은 다음과 같습니다.

  • TDD 적용 영역: 정산/결제 금액 계산 로직, 데이터 변환 유틸 함수 (버그 나면 치명적인 곳)
  • 일반 개발 후 테스트 적용 영역: 컴포넌트 레이아웃, CSS 스타일링, 단순 안내 페이지

5. GitHub Actions로 자동 배포 검문소 만들기

테스트 코드를 짜두기만 하고 안 돌리면 의미가 없어서, GitHub Actions를 활용해 가상 검문소를 만들었습니다.

팀원이 PR(Pull Request)을 올릴 때마다, GitHub 서버가 자동으로 Jest와 Cypress를 먼저 실행합니다. 여기서 테스트가 단 하나라도 실패하면 메인 브랜치에 Merge 자체가 되지 않도록 막아두었습니다.

6. 글을 마치며

테스트 코드는 단순히 '코드의 품질'을 높이는 수단이 아닙니다. "배포할 때 오는 불안함을 없애주는 최고의 안전장치"입니다.

내가 수정한 코드가 기존 기능을 깨뜨리지 않았다는 확신이 생기니, 새로운 기능을 시도하거나 리팩토링을 진행할 때도 주저 없이 코드를 고칠 수 있게 되었습니다.

팀 프로젝트나 개인 프로젝트를 진행하시면서 배포에 불안함을 느끼셨다면, 오늘 밤 가장 핵심이 되는 유틸 함수에 작은 유닛 테스트 하나부터 작성해 보세요!

매번 코드를 수정할 때마다 서버에 접속해서 git pull을 받고, 빌드를 다시 하고, 프로세스를 재시작하는 과정은 번거롭고 실수하기 쉽습니다. 이러한 반복 작업을 자동화하여 개발자가 오직 '코드'에만 집중할 수 있게 해주는 기술이 바로 CI/CD입니다.

오늘은 별도의 서버 설치 없이 GitHub에서 제공하는 강력한 자동화 도구인 GitHub Actions를 사용하여, 코드를 푸시하면 내 EC2 서버에 즉시 배포되는 환경을 만들어보겠습니다.


1. CI/CD란 무엇인가?

  • CI (Continuous Integration): 지속적 통합. 개발자들이 작업한 코드를 자주 병합하고, 그때마다 자동으로 테스트와 빌드를 수행하여 코드의 품질을 검증하는 과정입니다.
  • CD (Continuous Deployment): 지속적 배포. 검증된 코드를 실제 운영 서버에 자동으로 반영하는 과정입니다.

2. GitHub Actions의 핵심: Workflow 파일

GitHub Actions는 프로젝트 루트의 .github/workflows/ 디렉토리에 있는 YAML 파일을 통해 동작합니다.

2.1 배포 시나리오 (Workflow)

  1. 개발자가 main 브랜치에 코드를 Push합니다.
  2. GitHub Actions가 가상 환경을 실행하여 코드를 **빌드(Build)**합니다.
  3. 빌드가 성공하면 SSH를 통해 내 EC2 서버에 접속합니다.
  4. 서버에서 최신 코드를 받고 서비스를 재시작합니다.

3. 실전! 자동 배포 스크립트 작성하기

먼저 GitHub 저장소의 Settings > Secrets and variables > Actions에서 서버 접속 정보(IP, Username, SSH Key)를 등록해야 안전합니다.

3.1 deploy.yml 예시

YAML
 
name: Deploy to EC2

on:
  push:
    branches: [ main ] # main 브랜치에 푸시될 때 실행

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 저장소 코드 가져오기
        uses: actions/checkout@v3

      - name: SSH 접속 후 배포 명령 실행
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            cd /home/ubuntu/my-project
            git pull origin main
            npm install
            pm2 restart all # Node.js 서비스 재시작 예시

4. CI/CD 도입 시 얻게 되는 효과

  • 빠른 피드백: 코드를 올리자마자 빌드 오류나 테스트 실패 여부를 알 수 있습니다.
  • 배포 실수 방지: 수동으로 명령어를 입력하다 발생할 수 있는 오타나 누락을 원천 차단합니다.
  • 생산성 향상: 배포에 소요되는 시간을 줄여 핵심 로직 개발에 더 많은 시간을 투자할 수 있습니다.
  • 안정적인 릴리즈: 정해진 절차에 따라 기계적으로 배포되므로 서비스의 안정성이 높아집니다.

5. 실무 꿀팁: Slack/Discord 알림 연동

배포가 성공했는지 실패했는지 매번 GitHub 사이트에서 확인할 수는 없습니다. GitHub Actions에 알림 스텝을 추가하면 배포 결과를 실시간으로 메신저로 받아볼 수 있습니다.


6. 결론: 자동화는 선택이 아닌 필수입니다

스무 차례에 걸쳐 리눅스 서버 기초부터 보안, 모니터링, 그리고 자동 배포까지 달려왔습니다. CI/CD 구축은 이 모든 여정의 마침표이자, 진정한 엔지니어로 거듭나는 시작점입니다.

+ Recent posts