TDD문화 도입 | (4) Tech 발표, Vitest로 안정적인 프론트엔드 개발하기
📌 이 글은 TDD 시리즈 중 4편입니다.
1편: Vitest 전환과 커버리지 기반 품질 관리
2편: 주간 커버리지 대시보드로 시각화하기
3편: 커버리지 측정 및 자동화 시스템 구축 (GitLab + Vitest)
안녕하세요, 최근 사내 Tech 부문에서 진행한 ‘Vitest로 안정적인 프론트엔드 개발하기’ 발표 내용을 블로그 글로 정리하여 공유합니다.
이 글에서는 테스트 자동화 시스템을 구축하게 된 배경부터 실제 적용 사례, 그리고 CI/CD 연동을 통해 얻은 성과까지의 과정을 담았습니다.
1. 도입 배경: 왜 테스트 자동화인가?
최근 프론트엔드 프로젝트의 복잡도가 증가하면서 코드의 안정성 확보가 그 어느 때보다 중요해졌습니다. 저희 팀 역시 다음과 같은 문제들을 겪고 있었습니다.
- 😰 반복되는 버그: 버그를 수정하면 또 다른 버그가 발생하는 악순환
- ⏰ 수동 테스트의 한계: 배포 전 모든 기능을 수동으로 테스트하는 데 드는 시간과 비용
- 😓 회귀 테스트의 피로감: 반복적인 회귀 테스트로 인한 팀의 피로도 증가
- 🤝 사이드 이펙트 증가: 협업 시 예상치 못한 사이드 이펙트로 인한 불안감
이러한 문제들을 해결하고 지속 가능한 개발 문화를 만들기 위해, Vitest를 활용한 단위 테스트 자동화와 GitLab CI/CD 연동을 도입하기로 결정했습니다.
2. 테스트 인프라 구축
2.1. 기술 스택: 왜 Vitest인가?
기존에 사용하던 Jest의 대안으로 Vitest를 선택했습니다. 이유는 명확했습니다.
| 항목 | 설명 |
|---|---|
| ⚡ 빠른 속도 | Vite 기반으로 동작하여 Jest 대비 최대 10배 빠른 실행 속도 |
| 🧠 쉬운 설정 | Vite 설정을 그대로 활용하며, 별도 트랜스파일 없이 즉시 사용 가능 |
| 💎 좋은 DX | 직관적인 CLI, HMR 지원, 시각적인 UI 제공 |
주요 스택
- 테스트 러너:
Vitest - 테스트 유틸리티:
@vue/test-utils - API 모킹:
MSW (Mock Service Worker) - 테스트 데이터 생성:
Faker.js,MockDataBuilder.js(자체 제작 빌더)
2.2. Vitest 환경 설정
vitest.config.js에 테스트 환경을 설정했습니다. CI 환경에서는 JUnit, Cobertura 등 리포터를 추가하여 GitLab과 연동할 수 있도록 구성했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: "jsdom",
setupFiles: path.resolve(__dirname, "__vitest__/vitest.setup.js"),
include: ["__vitest__/**/*.test.{js,ts,tsx}"],
reporters: process.env.CI ? ["default", "junit"] : ["default"],
coverage: {
enabled: process.env.COVERAGE === "true",
reporter: ["html", "json-summary", "text", "cobertura"],
},
},
resolve: {
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
},
});
2.3. 테스트 데이터 생성 자동화
복잡한 API 응답 데이터를 매번 수동으로 만드는 것은 비효율적입니다. 이를 해결하기 위해 빌더 패턴을 적용한 MockDataBuilder.js를 자체 제작하여 테스트 데이터 준비 시간을 80% 단축했습니다.
AS-IS: 수동으로 데이터 작성
1
2
3
4
5
6
7
8
9
10
// 매번 테스트마다 객체를 수동으로 생성
const mockData = {
moduleInfo: { moduleType: 'product_list', /* ... */ },
productListData: {
product: [
{ prdNo: 'P001', prdNm: '상품1', /* ... */ },
{ prdNo: 'P002', prdNm: '상품2', /* ... */ }
]
}
};
TO-BE: 빌더 패턴으로 데이터 생성
1
2
3
4
5
// 빌더 패턴을 활용한 직관적인 데이터 생성
const mockData = new MockDataBuilder()
.addModule("product_list")
.withProducts(2)
.build();
2.4. MSW를 활용한 API 모킹
MSW(Mock Service Worker)를 사용해 브라우저(개발 환경)와 Node.js(테스트 환경)에서 동일한 Mock API를 사용하도록 환경을 통일했습니다. MockDataBuilder와 연동하여 재사용 가능한 핸들러 팩토리를 만들었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { http, HttpResponse } from "msw";
import MockDataBuilder from "@mock/builder/MockDataBuilder.js";
export const createProductListHandlers = () => ({
default: http.get("/api/display/product_list", () => {
const data = new MockDataBuilder().addModule("product_list").withProducts(2).build();
return HttpResponse.json({ returnCode: "200", data });
}),
// 에러 응답 핸들러
error: http.get("/api/display/product_list", () => {
return HttpResponse.json(null, { status: 500 });
}),
// 상품 개수를 조절할 수 있는 커스텀 핸들러
custom: (count) => http.get("/api/display/product_list", () => {
const data = new MockDataBuilder().addModule("product_list").withProducts(count).build();
return HttpResponse.json({ returnCode: "200", data });
}),
});
3. Vue 컴포넌트 테스트 적용 사례
ProductList.vue 컴포넌트(상품 2개를 나열하는 모듈)를 예시로 테스트 코드를 작성했습니다.
3.1. 테스트 시나리오
| 시나리오 | 설명 |
|---|---|
| 기본 렌더링 | 상품 개수에 따라 모듈이 노출/비노출되는지 검증 |
| API 연동 | MSW로 모킹한 API 응답 기반으로 렌더링되는지 검증 |
| Edge Case | API 실패 및 예외 상황을 올바르게 처리하는지 검증 |
3.2. 테스트 코드 예시
✅ 기본 렌더링 테스트
1
2
3
4
5
6
7
8
9
10
it("상품이 2개 미만이면 모듈이 노출되지 않아야 함", () => {
// Arrange: 상품 1개 데이터 생성
const data = new MockDataBuilder().addModule("product_list").withProducts(1).build();
// Act: 컴포넌트 마운트
const wrapper = mount(ProductList, { propsData: { data } });
// Assert
expect(wrapper.vm.isView).toBe(false);
expect(wrapper.html()).not.toContain("module-wrapper");
});
✅ API 연동 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
it("API로부터 상품 2개를 받아오면 모듈이 노출되어야 함", async () => {
// Arrange: MSW로 API 응답 모킹
server.use(createProductListHandlers().default);
// Act
const response = await fetch("/api/display/product_list");
const result = await response.json();
const wrapper = mount(ProductList, { propsData: { data: result.data } });
// Assert
expect(wrapper.vm.isView).toBe(true);
expect(wrapper.vm.productListData.product.length).toBe(2);
});
4. GitLab CI/CD 연동 및 성과
4.1. CI/CD 파이프라인 자동화
MR(Merge Request) 생성 시 GitLab CI 파이프라인이 자동으로 테스트를 실행하고 커버리지를 측정하도록 .gitlab-ci.yml을 설정했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
coverage:
stage: test
variables:
CI: "true"
COVERAGE: "true"
script:
- npx vitest run
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
4.2. 성과 및 결론
테스트 자동화 시스템을 도입한 결과, 다음과 같은 성과를 얻을 수 있었습니다.
📈 정량적 성과
- 결함 밀도 26% 감소: 작업량 대비 결함 비율이 크게 줄었습니다.
- 버그 사전 차단: CI 단계에서 배포 전 버그를 지난 한달 동안 3건 사전에 차단했습니다.
- 테스트 속도 향상: 720개의 단위 테스트를 90초 이내에 완료합니다.
- 데이터 준비 시간 80% 단축:
MockDataBuilder도입으로 테스트 데이터 준비 시간을 크게 줄였습니다.
✨ 정성적 성과
- 리팩토링 자신감 향상: 테스트 코드가 안전망 역할을 하여 레거시 코드 개선이 가속화되었습니다.
- 코드 리뷰 효율 증대: MR 리뷰 시, 테스트 코드를 통해 기능의 의도를 명확히 파악할 수 있게 되었습니다.
- 개발 문화 개선: 시각적인 성과(뱃지, 대시보드)를 통해 개발 만족도와 동기 부여가 향상되었습니다.
현재 코드 커버리지는 25.64%(10월 5주차 기준)이며, 70% 달성을 목표로 점진적으로 확대하고 있습니다. 주간 리포트는 Bigbro 대시보드에서 확인할 수 있습니다.
Vitest와 GitLab CI/CD를 통해 더 빠르고 안정적인 프론트엔드 개발 문화를 만들어가고 있습니다.
5. 향후 과제
- 테스트 범위 확대: 단위 테스트에서 통합(Integration), E2E 테스트로 점차 확대
- 커버리지 목표 달성: 테스트 커버리지 70% 목표 달성
- 문화 확산: 테스트 작성 문화를 팀을 넘어 전사로 확산
📎 발표 자료
- 온라인 보기: 발표 자료 보기
- 다운로드: GitHub 리포지토리
발표 후기.
발표를 마친 후, 많은 분들이 테스트 자동화에 관심을 보여주셨습니다. 특히 실장님께서 좋은 세미나에 대한 격려와 함께 간식비 지원을 약속해주시며 법인카드를 주셨습니다. 🥳
앞으로도 유익한 기술 공유 문화를 만드는 데 기여하고 싶습니다.


