팀 프로젝트에서 배포를 진행한 직후, 팀원이 당황스러운 소식을 전해왔습니다. "아까 새로 고친 팝업창 버튼은 잘 되는데, 기존에 잘 되던 [로그인]이랑 [장바구니 담기] 기능이 클릭이 안 돼요!"
분명 내 파트 코드만 살짝 고치고 수동으로 클릭해 본 뒤 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();
});

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초 만에 검증할 수 있게 되었습니다.

4. TDD(테스트 주도 개발)에 대한 나의 생각
프로젝트 일정이 빡빡할 때는 모든 코드를 TDD로 짜는 것은 어려웠습니다. 그래서 제가 찾은 타협점은 다음과 같습니다.
- TDD 적용 영역: 정산/결제 금액 계산 로직, 데이터 변환 유틸 함수 (버그 나면 치명적인 곳)
- 일반 개발 후 테스트 적용 영역: 컴포넌트 레이아웃, CSS 스타일링, 단순 안내 페이지
5. GitHub Actions로 자동 배포 검문소 만들기
테스트 코드를 짜두기만 하고 안 돌리면 의미가 없어서, GitHub Actions를 활용해 가상 검문소를 만들었습니다.
팀원이 PR(Pull Request)을 올릴 때마다, GitHub 서버가 자동으로 Jest와 Cypress를 먼저 실행합니다. 여기서 테스트가 단 하나라도 실패하면 메인 브랜치에 Merge 자체가 되지 않도록 막아두었습니다.
6. 글을 마치며
테스트 코드는 단순히 '코드의 품질'을 높이는 수단이 아닙니다. "배포할 때 오는 불안함을 없애주는 최고의 안전장치"입니다.
내가 수정한 코드가 기존 기능을 깨뜨리지 않았다는 확신이 생기니, 새로운 기능을 시도하거나 리팩토링을 진행할 때도 주저 없이 코드를 고칠 수 있게 되었습니다.
팀 프로젝트나 개인 프로젝트를 진행하시면서 배포에 불안함을 느끼셨다면, 오늘 밤 가장 핵심이 되는 유틸 함수에 작은 유닛 테스트 하나부터 작성해 보세요!
'개발' 카테고리의 다른 글
| [Performance] 이미지 용량 다이어트 잔혹사: WebP 변환과 Lazy Loading으로 LCP 점수 극복한 후기 (0) | 2026.05.04 |
|---|---|
| [Tooling] "따옴표 하나로 Git 충돌이?" ESLint와 Prettier로 협업 스트레스 날려버린 후기 (1) | 2026.05.03 |
| [Security] 프론트엔드 보안 가이드: XSS와 CSRF 완벽 방어하기 (1) | 2026.05.01 |
| [Web] 브라우저 렌더링 원리: Critical Rendering Path 최적화 전략 (0) | 2026.04.30 |
| [Next.js] App Router 환경에서 서버 컴포넌트(RSC)와 클라이언트 컴포넌트의 경계 설계하기 (0) | 2026.04.29 |