포스트

TDD문화 도입 | (4) Tech 발표, Vitest로 안정적인 프론트엔드 개발하기

TDD문화 도입 | (4) Tech 발표, Vitest로 안정적인 프론트엔드 개발하기

📌 이 글은 TDD 시리즈 중 4편입니다.
1편: Vitest 전환과 커버리지 기반 품질 관리
2편: 주간 커버리지 대시보드로 시각화하기
3편: 커버리지 측정 및 자동화 시스템 구축 (GitLab + Vitest)


안녕하세요, 최근 사내 Tech 부문에서 진행한 ‘Vitest로 안정적인 프론트엔드 개발하기’ 발표 내용을 블로그 글로 정리하여 공유합니다.

이 글에서는 테스트 자동화 시스템을 구축하게 된 배경부터 실제 적용 사례, 그리고 CI/CD 연동을 통해 얻은 성과까지의 과정을 담았습니다.

Tech 부문 발표 일정 슬랙 공지 Tech 부문 발표 일정 슬랙 공지

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와 연동하여 재사용 가능한 핸들러 팩토리를 만들었습니다.

MSW 워크플로우 다이어그램 MSW 워크플로우 다이어그램

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 CaseAPI 실패 및 예외 상황을 올바르게 처리하는지 검증

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% 목표 달성
  • 문화 확산: 테스트 작성 문화를 팀을 넘어 전사로 확산

📎 발표 자료


발표 후기.

발표를 마친 후, 많은 분들이 테스트 자동화에 관심을 보여주셨습니다. 특히 실장님께서 좋은 세미나에 대한 격려와 함께 간식비 지원을 약속해주시며 법인카드를 주셨습니다. 🥳

발표 후 실장님 격려 메시지

앞으로도 유익한 기술 공유 문화를 만드는 데 기여하고 싶습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.